Skip to content

Conversation

@thomhurst
Copy link

@thomhurst thomhurst commented Dec 30, 2025

Summary

Improves Unix socket connection handling for better proxy compatibility (e.g., Docker socket proxies in CI environments like custom GitHub runners with DinD) while preserving full support for hijacked streams required by exec, attach, and log operations.

Background

The initial approach used SocketsHttpHandler for .NET 8+ to leverage modern socket handling. However, testing revealed that SocketsHttpHandler cannot support stream hijacking because:

  • It manages connections internally with pooling
  • Response content doesn't expose the raw transport stream
  • The HttpConnectionResponseContent.HijackStream() pattern requires direct transport access

This caused failures in exec, attach, logs, and port forwarding operations with:

System.NotSupportedException: message handler does not support hijacked streams

Solution

Instead of replacing ManagedHandler, we modernize it with configurable socket options for better proxy compatibility:

  1. New SocketConfiguration class - Configurable socket options:

    • KeepAlive (default: true)
    • KeepAliveTime (default: 30s) - .NET 5.0+
    • KeepAliveInterval (default: 10s) - .NET 5.0+
    • KeepAliveRetryCount (default: 3) - .NET 7.0+
    • NoDelay (default: true)
    • SendBufferSize / ReceiveBufferSize
  2. Updated ManagedHandler - Accepts SocketConfiguration and applies modern socket options

  3. Modern .NET APIs - Uses Socket.ConnectAsync with CancellationToken on .NET 5.0+

Why This Works

  • Preserves hijacking: ManagedHandlerHttpConnectionHttpConnectionResponseContent chain maintains direct transport stream access
  • Better proxy compatibility: TCP keep-alive settings help maintain connections through proxies
  • Configurable: Users can tune settings for their specific proxy environment

Files Changed

File Change
SocketConfiguration.cs NEW - Configurable socket options class
ManagedHandler.cs Accept SocketConfiguration, use modern socket APIs
DockerClientConfiguration.cs Add SocketConfiguration property, reorganize timeouts
DockerClient.cs Remove SocketsHttpHandler, use ManagedHandler with config

Test Plan

  • Unit tests pass
  • Hijacked stream tests pass (exec, attach, logs)
  • Multiplexed stream tests pass
  • Build succeeds on all target frameworks (netstandard2.0/2.1, net8.0/9.0/10.0)

Usage

// Default configuration (good for most cases)
var config = new DockerClientConfiguration();

// Custom socket configuration for specific proxy environments
var config = new DockerClientConfiguration(
    socketConfiguration: new SocketConfiguration
    {
        KeepAlive = true,
        KeepAliveTime = 15,      // More aggressive keepalive
        KeepAliveInterval = 5,
        NoDelay = true
    }
);

🤖 Generated with Claude Code

@thomhurst thomhurst force-pushed the feat/improve-unix-socket-handling branch from 771ee02 to 09cd6c3 Compare December 30, 2025 11:05
- Use SocketsHttpHandler with ConnectCallback for .NET 8+ (more robust)
- Add KeepAlive socket option for better proxy socket compatibility
- Add configurable UnixSocketConnectTimeout (default 30s)
- Improve error handling with proper socket disposal
- Add unit tests for DockerClientConfiguration

This change addresses issues with Docker socket proxy wrappers (like those
used in some CI environments) that may not work with the legacy ManagedHandler.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@thomhurst thomhurst force-pushed the feat/improve-unix-socket-handling branch from 09cd6c3 to f329d87 Compare December 30, 2025 11:12
Copilot AI review requested due to automatic review settings January 2, 2026 10:34
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR improves Unix socket connection handling to better support Docker socket proxy wrappers commonly used in CI environments like custom GitHub runners with Docker-in-Docker (DinD). The implementation adds configurable connection timeouts and switches to modern SocketsHttpHandler for .NET 8+.

Key Changes:

  • Introduced SocketConnectTimeout configuration property with a 30-second default timeout
  • Migrated .NET 8+ Unix socket connections from ManagedHandler to SocketsHttpHandler with ConnectCallback
  • Added KeepAlive socket option for improved proxy compatibility across all platforms

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

File Description
DockerClientConfiguration.cs Adds SocketConnectTimeout parameter/property with default value of 30 seconds
DockerClient.cs Implements modern SocketsHttpHandler for .NET 8+ and timeout handling with KeepAlive for all platforms
DockerClientConfigurationTests.cs New unit tests validating default and custom timeout configuration values
IContainerOperationsTests.cs Changes ThrowsAsync to ThrowsAnyAsync for more accurate cancellation exception assertions

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@thomhurst
Copy link
Author

@HofmeisterAn are you free to take a look?

Copy link
Collaborator

@HofmeisterAn HofmeisterAn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. I need to test the changes locally as well using Testcontainers. Once everything works and we've addressed the suggestions, we can merge the PR.

…arity and improve Unix socket connection timeout documentation
@HofmeisterAn
Copy link
Collaborator

I did some brief tests on macOS. Most things look good, but everything related to running commands inside the container (exec), and probably retrieving logs does not work. At least these tests fail:

tests
  Testcontainers.Platform.Linux.Tests
  Testcontainers.Tests
    ExecResultExtensionsTest
    ExecAsyncShouldSucceedWhenCommandReturnsZeroExitCode
    ExecAsyncShouldThrowExecFailedExceptionWhenCommandFails
    PortForwardingTest
    PortForwardingDefaultConfiguration
      PortForwardingTest.EstablishesHostConnection
    PortForwardingNetworkConfiguration
      PortForwardingTest.EstablishesHostConnection
    SocatContainerTest
    RequestTargetContainer
      RequestTargetContainer(containerPort: 8080)
      RequestTargetContainer(containerPort: 8081)
    TarOutputMemoryStreamTest
    FromResourceMapping
      TestFileExistsInContainer
      TestFileHasExpectedOwnerGroupMode

with (which is the same failure in this CI run):

System.NotSupportedException
message handler does not support hijacked streams

@thomhurst
Copy link
Author

Thanks for looking @HofmeisterAn !
I've just changed it for a 'dual-mode' approach for best of both worlds. ManagedHandler for hijacked streams, and native Sockets handler for everything else

@HofmeisterAn
Copy link
Collaborator

Thanks for looking @HofmeisterAn ! I've just changed it for a 'dual-mode' approach for best of both worlds. ManagedHandler for hijacked streams, and native Sockets handler for everything else

I don't think this is the right approach. Hijacking the stream is part of Docker.DotNet 1), 2). We should investigate why we're encountering this issue, gain a better understanding of SocketsHttpHandler, and support hijacking and multiplexing streams properly, as they are important.

I've just changed it for a 'dual-mode' approach for best of both worlds.

Sorry, this seems like a quick fix without really understanding the issue or considering a proper solution.

@bruegth
Copy link

bruegth commented Jan 20, 2026

@thomhurst For Proxy support some more settings were required for SocketsHttpHandler nor?
This is what AI says:

        // Use it in SocketsHttpHandler
        var handler = new SocketsHttpHandler
        {
            Proxy = WebRequest.DefaultWebProxy,
            UseProxy = true,
            PooledConnectionIdleTimeout = TimeSpan.FromMinutes(5),
            KeepAlivePingPolicy = HttpKeepAlivePingPolicy.Always,
            KeepAlivePingDelay = TimeSpan.FromSeconds(30),
            KeepAlivePingTimeout = TimeSpan.FromSeconds(15)
        };

        using var client = new HttpClient(handler);
        var response = await client.GetAsync("https://httpbin.org/get");
        Console.WriteLine(await response.Content.ReadAsStringAsync());

💡 Best Practice in 2026:
If you’re writing new code, don’t rely on WebRequest.DefaultWebProxy — explicitly set the proxy in SocketsHttpHandler for clarity and future compatibility.

…erving hijacked streams

Remove dual-mode SocketsHttpHandler approach as SocketsHttpHandler cannot support
stream hijacking (required for exec/attach/logs). Instead, modernize ManagedHandler
with configurable socket options for better proxy compatibility.

Changes:
- Add SocketConfiguration class for configurable socket options (KeepAlive,
  KeepAliveTime, KeepAliveInterval, KeepAliveRetryCount, NoDelay, buffer sizes)
- Update ManagedHandler to accept SocketConfiguration and apply modern socket
  options including TCP keep-alive settings (.NET 5+/7+)
- Remove SocketsHttpHandler and _hijackClient from DockerClient
- Use ManagedHandler for all operations to preserve HttpConnectionResponseContent
  hijacking capability
- Reorganize DockerClientConfiguration properties to group timeouts together
- Use modern Socket.ConnectAsync with CancellationToken on .NET 5+

This addresses PR feedback that the dual-mode approach was incorrect and that
we should properly understand why SocketsHttpHandler cannot support hijacking.
@thomhurst thomhurst changed the title feat: Improve Unix socket connection handling for proxy compatibility feat: Modernize ManagedHandler for proxy compatibility while preserving hijacked streams Jan 20, 2026
thomhurst and others added 8 commits January 20, 2026 10:14
The overload accepting HttpClient parameter was only needed for the dual-mode
approach with _hijackClient. Since we now use only _client, the indirection
is unnecessary.
… pooling, and modern .NET features

- Add Memory<byte>/ReadOnlyMemory<byte> overloads and IAsyncDisposable to stream classes
- Implement modern TLS with SslClientAuthenticationOptions and ConnectTimeout property
- Add RetryPolicy with exponential backoff and jitter for transient connection failures
- Implement Happy Eyeballs (RFC 8305) for parallel IPv6/IPv4 connection racing
- Add ConnectionPool for HTTP connection reuse with idle timeout and lifetime limits
- Use HttpClient.DefaultProxy for modern proxy resolution on NET6+
- Add PipeReader-based line reading with automatic fallback
- Add comprehensive test coverage in ManagedHandlerTests.cs

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…m interference

The PipeReader implementation was bypassing the internal buffer and causing
issues with chunked stream reading. Removed for now - can be re-added later
with proper integration if needed.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Remove RetryPolicy class and related retry wrapper around socket connections.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Remove connection pooling feature to simplify the codebase:
- Remove ConnectionPool.cs
- Simplify ProcessRequestAsync in ManagedHandler.cs
- Remove pooling code from HttpConnectionResponseContent.cs
- Remove connection pooling tests from ManagedHandlerTests.cs

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Remove the Memory<byte>/ReadOnlyMemory<byte> overloads and IAsyncDisposable
implementations as they are not directly related to improving proxy
functionality:

- BufferedReadStream.cs: Remove IAsyncDisposable, DisposeAsync, Memory APIs
- ChunkedReadStream.cs: Remove IAsyncDisposable, DisposeAsync, Memory APIs
- ChunkedWriteStream.cs: Remove IAsyncDisposable, DisposeAsync, Memory APIs
- ContentLengthReadStream.cs: Remove IAsyncDisposable, DisposeAsync, Memory APIs
- HttpConnection.cs: Remove IAsyncDisposable, DisposeAsync, Memory API usage
- ManagedHandlerTests.cs: Remove Phase 1 Memory API tests

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Local Docker connections via Unix sockets and named pipes should not
use proxy resolution as they connect directly to the local Docker daemon.
This change explicitly sets UseProxy = false for these connection types,
avoiding unnecessary proxy resolution overhead and potential issues in
restricted environments like Docker-in-Docker GitHub Actions runners.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Rename for clarity - the class configures socket connection behavior,
not sockets themselves. This is a new class introduced in this PR,
so no breaking change concerns.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@thomhurst
Copy link
Author

@bruegth Looks like SocketsHttpHandler isn't the approach to go due to the hijacked streams limitation
@HofmeisterAn Thanks for the feedback. What about this now? Modernising the ManagedHandler?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants