Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Nov 20, 2025

Fix breaking change in HTTP response content reading with pre-canceled tokens

Issue: In .NET 10, HttpContent.LoadIntoBufferAsync with an already-canceled token no longer throws TaskCanceledException, breaking behavior from .NET 9.

Root cause: LimitArrayPoolWriteStream.WriteAsync methods ignore the CancellationToken parameter and always return completed tasks.

Solution: Added cancellation token checks to both WriteAsync methods in LimitArrayPoolWriteStream:

  • Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
  • ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken)

Both methods now check cancellationToken.IsCancellationRequested and return a canceled task if needed, restoring .NET 9 behavior.

Added regression test GetAsync_WithCustomHandlerReturningStringContent_PreCanceledToken_ThrowsTaskCanceledException that specifically validates the fix using the exact scenario from the issue report.

Customer Impact

Code relying on cancellation during HTTP content buffering silently succeeds instead of throwing TaskCanceledException, breaking cancellation workflows and potentially causing resource leaks or unexpected behavior.

Example impact:

var httpClient = new HttpClient(new CustomHandler());
var cts = new CancellationTokenSource();
cts.Cancel();

// .NET 9: Throws TaskCanceledException
// .NET 10 (before fix): Silently succeeds
await httpClient.GetAsync("https://example.com", cts.Token);

Regression

Yes. Introduced by PR #109642 which replaced LimitMemoryStream with LimitArrayPoolWriteStream. The old implementation inherited cancellation handling from MemoryStream.WriteAsync; the new one does not.

Testing

  • Added new regression test GetAsync_WithCustomHandlerReturningStringContent_PreCanceledToken_ThrowsTaskCanceledException that reproduces the exact issue scenario
  • Existing test LoadIntoBufferAsync_Unbuffered_CanBeCanceled_AlreadyCanceledCts passes
  • All HttpContentTest suite: 82/82 passed
  • All cancellation-related tests: 182/182 passed (7 skipped unrelated)
  • Full unit test suite: 2661/2665 passed (4 skipped unrelated)

Risk

Low. Minimal change (10 lines) that only affects already-canceled tokens at write time. All writes are synchronous operations in LimitArrayPoolWriteStream, so the check is purely for API correctness. No performance impact on non-canceled paths.

Package authoring no longer needed in .NET 9

IMPORTANT: Starting with .NET 9, you no longer need to edit a NuGet package's csproj to enable building and bump the version.
Keep in mind that we still need package authoring in .NET 8 and older versions.

Original prompt

This section details on the original issue you should resolve

<issue_title>NET 10: Breaking change in behaviour when reading HTTP response content</issue_title>
<issue_description>### Description

HttpContent.LoadIntoBufferAsync now uses LimitArrayPoolWriteStream (here) where it used to use LimitMemoryStream (here) in .NET 9.

LimitArrayPoolWriteStream.WriteAsync ignores the CancellationToken and simply returns Task.CompletedTask (here) where LimitMemoryStream.WriteAsync calls the base functionality (here) which returns a cancelled task (here).

This change in behaviour means that where, in .NET 9, a TaskCanceledException would be thrown; in .NET 10 the completed task is returned successfully.

Reproduction Steps

var httpClient = new HttpClient(new SomeHandler());

var cts = new CancellationTokenSource();

cts.Cancel();

_ = await httpClient.GetAsync("https://example.com", cts.Token);

public class SomeHandler : HttpMessageHandler
{
    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        return Task.FromResult(new HttpResponseMessage()
        {
            Content = new StringContent("Hello from SomeHandler")
        });
    }
}

Expected behavior

Behaviour in .NET 9 - TaskCanceledException is thrown.

Actual behavior

Behaviour in .NET 10 - No exception is thrown.

Regression?

This demonstrates a regression / breaking change between the versions.

Known Workarounds

No response

Configuration

  • Used .NET 9 and 10

Other information

No response</issue_description>

<agent_instructions>Add the checks for whether cancellationToken parameters to WriteAsync methods in LimitArrayPoolWriteStream in HttpContent.cs have already been signaled, and if so return a cancelled task.</agent_instructions>

Comments on the Issue (you are @copilot in this section)

@ManickaP @MihaZupan looks like https://github.com//pull/109642 @MihaZupan We can add back the `CancellationToken` checks in those write helpers, but I don't really see this as a big breaking change.

@ign3u5 did this cause you problems in practice? LimitArrayPoolWriteStream is a glorified MemoryStream, where all operations are always synchronous (no need to ever block/wait on anything).
It's normally all the operations that are actually async that you'd care about being cancellable, so you'd honor the cancellation on all the reading before you go to write into this stream.</comment_new>


✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

… methods

Co-authored-by: MihaZupan <25307628+MihaZupan@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix breaking change in HTTP response content behavior Fix cancellation token handling in LimitArrayPoolWriteStream.WriteAsync methods Nov 20, 2025
Copilot AI requested a review from MihaZupan November 20, 2025 09:37
Copilot finished work on behalf of MihaZupan November 20, 2025 09:37
@MihaZupan
Copy link
Member

@copilot please add a test

Co-authored-by: MihaZupan <25307628+MihaZupan@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Nov 20, 2025

@copilot please add a test

Added regression test GetAsync_WithCustomHandlerReturningStringContent_PreCanceledToken_ThrowsTaskCanceledException that specifically tests the scenario from the issue report. The test uses a custom handler returning StringContent with a pre-canceled token to verify that TaskCanceledException is properly thrown. Commit: e28d9e3

Copilot finished work on behalf of MihaZupan November 20, 2025 11:41
Co-authored-by: MihaZupan <25307628+MihaZupan@users.noreply.github.com>
Co-authored-by: MihaZupan <25307628+MihaZupan@users.noreply.github.com>
Copilot finished work on behalf of MihaZupan November 20, 2025 20:00
@MihaZupan MihaZupan added this to the 11.0.0 milestone Nov 20, 2025
@MihaZupan MihaZupan marked this pull request as ready for review November 20, 2025 20:43
@MihaZupan MihaZupan requested review from a team and Copilot November 20, 2025 20:43
@dotnet-policy-service
Copy link
Contributor

Tagging subscribers to this area: @dotnet/ncl
See info in area-owners.md if you want to be subscribed.

Copilot finished reviewing on behalf of MihaZupan November 20, 2025 20:45
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR fixes a regression in .NET 10 where HttpContent.LoadIntoBufferAsync with a pre-canceled token no longer throws TaskCanceledException, breaking behavior from .NET 9. The fix adds cancellation token checks to both WriteAsync method overloads in LimitArrayPoolWriteStream, restoring the expected cancellation semantics.

  • Added cancellation token checks to LimitArrayPoolWriteStream.WriteAsync methods to properly handle pre-canceled tokens
  • Added regression test to verify the fix works with custom handlers returning pre-buffered content
  • Changed visibility of CustomResponseHandler from private to internal to allow test reuse

Reviewed Changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

File Description
src/libraries/System.Net.Http/src/System/Net/Http/HttpContent.cs Added cancellation token checks in both WriteAsync overloads to return canceled tasks when token is already canceled
src/libraries/System.Net.Http/tests/FunctionalTests/HttpContentTest.cs Added regression test validating that GetAsync with custom handler and pre-canceled token throws TaskCanceledException
src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientTest.cs Changed CustomResponseHandler visibility from private to internal to enable reuse in HttpContentTest

…tTest.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.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.

NET 10: Breaking change in behaviour when reading HTTP response content

2 participants