Skip to content

System.ObjectDisposedException in HttpClient Before Task.Delay Completes #50143

@kacperpikacz

Description

@kacperpikacz

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    untriagedRequest triage from a team member

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions