Skip to content

[Repo Assist] fix: reuse SocketsHttpHandler to prevent SNAT port exhaustion#216

Draft
github-actions[bot] wants to merge 1 commit intomasterfrom
repo-assist/fix-issue-198-port-exhaustion-2026-03-31058b633a04e3ab
Draft

[Repo Assist] fix: reuse SocketsHttpHandler to prevent SNAT port exhaustion#216
github-actions[bot] wants to merge 1 commit intomasterfrom
repo-assist/fix-issue-198-port-exhaustion-2026-03-31058b633a04e3ab

Conversation

@github-actions
Copy link
Contributor

🤖 This pull request was created by Repo Assist, an automated AI assistant.

Closes #198

Root cause

Defaults.defaultHttpClientFactory was called once per request, creating a fresh SocketsHttpHandler each time. SocketsHttpHandler holds the TCP connection pool, so discarding it after every request meant no connection reuse. On Azure (and any environment using SNAT), this exhausts outbound ports quickly.

Fix

Introduce a process-wide lazy singleton SocketsHttpHandler for the common (default) configuration. Each request still creates its own HttpClient wrapper, but uses HttpClient(handler, disposeHandler=false) so the handler — and its connection pool — is never torn down.

The shared handler is used only when:

  • certErrorStrategy = Default
  • No httpClientHandlerTransformers are registered
  • No proxy is configured
  • defaultDecompressionMethods matches the built-in default

In all other cases the factory falls back to creating a fresh handler per request (existing behaviour), so users with custom configurations are unaffected.

Why this design?

  • Backward compatible: Config.httpClientFactory signature unchanged; httpClientTransformers and per-request Timeout still work.
  • No new API surface: purely an internal change to the default factory.
  • Matches Microsoft guidance: [HttpClient guidelines]((learn.microsoft.com/redacted) recommend a long-lived handler; PooledConnectionLifetime = 5 min already ensures DNS changes are respected.

Trade-offs

  • Custom-config paths (proxy, cert override, handler transformers) still create a fresh handler per request. A per-config handler cache could be added in a follow-up.
  • The singleton handler is never disposed (application lifetime). This is intentional and standard practice for SocketsHttpHandler.

Test Status

Build: ✅ dotnet build src/FsHttp/FsHttp.fsproj — succeeded, 0 warnings, 0 errors.

Tests: ⚠️ Infrastructure issue — the CI runner does not have .NET 6.0 installed (available: 8.0, 9.0, 10.0), so dotnet test aborts before running any test. This is a pre-existing environment issue unrelated to this change. There is also a pre-existing NU1504 duplicate FsUnit reference warning in Tests.fsproj caused by open Dependabot PRs (#204, #208).

Generated by Repo Assist ·

To install this agentic workflow, run

gh aw add githubnext/agentics/workflows/repo-assist.md@346204513ecfa08b81566450d7d599556807389f

…198)

The default HttpClient factory created a fresh SocketsHttpHandler on
every request, which discarded the TCP connection pool immediately.
This caused SNAT port exhaustion on Azure and similar environments.

Introduce a process-wide lazy-singleton SocketsHttpHandler for the
common case (no proxy, no custom handler transformers, default cert
strategy, default decompression).  Each request still gets its own
HttpClient wrapper (using HttpClient(handler, disposeHandler=false))
so per-request Timeout and httpClientTransformers continue to work
correctly.  The handler — and its connection pool — is never disposed,
giving the same behaviour as a recommended DI-managed IHttpClientFactory.

When any handler customisation is present the factory falls back to
creating a fresh handler per request (existing behaviour), so there is
no regression for users who already configure a custom factory.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Port exhaustion regression

0 participants