-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Description
Title
ObjectDisposedException
from NetworkStream
during background read-ahead after idle timeout (before any second request)
Description
An unexpected ObjectDisposedException
is thrown about 5 seconds after the first request completes, even though no additional HTTP requests have been made. The exception appears to originate from a background read-ahead operation attempting to access a NetworkStream
that has already been disposed due to PooledConnectionIdleTimeout
.
Repro code
using Microsoft.Extensions.DependencyInjection;
var services = new ServiceCollection();
services.AddHttpClient("Primary")
.ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler
{
PooledConnectionIdleTimeout = TimeSpan.FromSeconds(5)
});
var provider = services.BuildServiceProvider();
var clientFactory = provider.GetRequiredService<IHttpClientFactory>();
var client = clientFactory.CreateClient("Primary");
var foo = await client.GetAsync("http://google.com");
// Delay to simulate idle time; exception occurs BEFORE this completes
Task.Delay(20000).Wait();
var bar = await client.GetAsync("http://google.com");
Observed behavior
After approximately 5 seconds (matching the idle timeout), this exception is thrown:
System.ObjectDisposedException: Cannot access a disposed object.
Object name: 'System.Net.Sockets.NetworkStream'.
at System.Net.Sockets.NetworkStream.ReadAsync(Memory`1 buffer, CancellationToken cancellationToken)
at System.Net.Http.HttpConnection.<EnsureReadAheadTaskHasStarted>g__ReadAheadWithZeroByteReadAsync|40_0()
Important notes:
- The exception occurs before the 20-second delay completes.
- The second HTTP request is never executed.
- The stack trace points to an internal async task still trying to read from a disposed stream.
Expected behavior
If the connection is being cleaned up due to idle timeout, internal tasks such as read-ahead should not attempt to use the stream after disposal. If disposal races do occur, they should be caught and handled gracefully — not throw to the user.
Environment
- .NET version: .NET 8
- OS: **Sequoia 15.6 **
- Reproducible: Consistently, ~5 seconds after app starts
Hypothesis
This may be a race condition between:
SocketsHttpHandler
disposing the idle connection, and- An internal read-ahead operation (
g__ReadAheadWithZeroByteReadAsync
) still referencing the stream without verifying it's not disposed.
This causes a fatal exception even when no follow-up request is made, which breaks the expected behavior of idle timeout cleanup.
Suggested resolution
Ensure internal connection read-ahead logic checks for or safely handles disposed NetworkStream
objects during cleanup triggered by PooledConnectionIdleTimeout
.