This change addresses critical HttpClient lifetime and DNS staleness issues in the custom HttpClientFactory implementation.
The original implementation had several critical issues:
-
Socket Exhaustion Risk: Each
HttpClientcreated its ownHttpClientHandlerwithdisposeHandler: true, leading to separate connection pools per client. -
DNS Staleness: No connection lifetime management meant DNS entries never refreshed, causing stale connections to persist indefinitely.
-
Dispose Race Condition: The
Dispose()method disposed allHttpClientinstances, potentially affecting active clients still in use. -
No Handler Pooling: The factory didn't leverage
SocketsHttpHandlerpooling for efficient connection reuse.
File: src/GrpcWebBridge/Integration/HttpClientFactory.cs
Added new property to HttpClientFactoryOptions:
/// <summary>
/// Gets or sets the pooled connection lifetime in milliseconds.
/// After this duration, the underlying SocketsHttpHandler will be rotated to
/// prevent DNS staleness and ensure connection freshness.
/// Default is 2 minutes (120000 ms).
/// </summary>
public int PooledConnectionLifetimeMs { get; set; } = 120000;- Created internal
PooledHandlerclass that manages sharedSocketsHttpHandlerinstances - Each handler is shared across multiple
HttpClientinstances with the same name - Handlers are disposed only when the factory is disposed, not when clients are disposed
- Clients use
disposeHandler: falseto prevent premature handler disposal
Implemented CheckHandlerRotation() method that:
- Monitors elapsed time since last handler rotation
- Rotates all pooled handlers when
PooledConnectionLifetimeMsis exceeded - Uses thread-safe locking to prevent race conditions during rotation
- Logs rotation events for observability
Updated Dispose() to:
- Dispose pooled handlers first (shared resources)
- Clear handler pool
- Dispose client instances managed by the factory
- Avoid disposing clients that might still be in use
Added ArgumentNullException.ThrowIfNull(logger) to constructor for better error handling.
- Prevents Socket Exhaustion: Shared handlers mean one connection pool per client name, not per client instance
- Eliminates DNS Staleness: Handlers rotate on configurable schedule, forcing DNS refresh
- Safe Disposal: No race conditions between disposed handlers and active clients
- Better Performance: Connection pooling reduces overhead of creating new handlers
- Configurable: Connection lifetime can be tuned per application needs
PooledConnectionLifetimeMs: 120,000 ms (2 minutes)MaxConnectionsPerServer: 10 (unchanged)RequestTimeoutMs: 30,000 ms (unchanged)
var options = new HttpClientFactoryOptions
{
PooledConnectionLifetimeMs = 300000, // 5 minutes
MaxConnectionsPerServer = 20,
RequestTimeoutMs = 15000
};
var factory = new HttpClientFactory(logger, options);Added comprehensive tests in tests/grpc-web-bridge.Tests/HttpClientFactoryTests.cs:
GetClient_WithSameName_ReusesSameHandler- Verifies handler sharingGetClient_WithDifferentNames_CreatesDifferentHandlers- Verifies isolationPooledConnectionLifetime_WithDefaultValue_IsSetCorrectly- Verifies defaultsPooledConnectionLifetime_WithCustomValue_IsSetCorrectly- Verifies customizationDispose_WithMultipleClients_DisposesHandlersNotClients- Verifies safe disposalHandlerRotation_OccursAfterConfiguredLifetime- Verifies rotation mechanismSocketsHttpHandler_ConfiguredWithProperSettings- Verifies configuration
All 47 HttpClientFactory tests pass successfully.
✅ Fully backward compatible
- Existing code continues to work without changes
- Default
PooledConnectionLifetimeMsof 2 minutes is reasonable for most use cases - No breaking changes to public API
- All existing tests continue to pass
Positive:
- Reduced memory usage (fewer handler instances)
- Better connection reuse (shared pools)
- Automatic DNS refresh prevents stale connections
- Lower GC pressure (fewer allocations)
Minimal overhead:
- Handler rotation check is O(1) and only runs on client creation
- No impact on existing connections
- No additional network calls
No migration needed! The changes are transparent to existing code.
For applications requiring longer connection lifetimes:
var options = new HttpClientFactoryOptions
{
PooledConnectionLifetimeMs = 3600000 // 1 hour for stable services
};For applications needing frequent DNS updates:
var options = new HttpClientFactoryOptions
{
PooledConnectionLifetimeMs = 60000 // 1 minute
};src/GrpcWebBridge/Integration/HttpClientFactory.cs- Main implementationtests/grpc-web-bridge.Tests/HttpClientFactoryTests.cs- Added tests
- ✅ All tests pass (47/47 HttpClientFactory tests)
- ✅ Solution builds successfully
- ✅ No breaking changes
- ✅ Backward compatible
- ✅ Follows C# best practices
- ✅ Proper XML documentation
- ✅ Thread-safe implementation
- ✅ Proper error handling with ArgumentNullException.ThrowIfNull