System.ObjectDisposedException in HttpClient Before Task.Delay Completes
- Dominant language
- C#
- Stars
- 3.2k
- Forks
- 1.3k
- PR merge metrics
- PR metrics pending
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
```csharp
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();
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.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`.
Contributor guide
No contributing guide indexed for this repository
Assessment
This issue has not been assessed yet.