Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Jan 21, 2026

Fixes the issue where calling Abort() on HTTP-based WCF channels doesn't cancel in-progress stream reads after HttpClient.SendAsync returns. The abort signal now properly propagates to the response body reader.

Problem

HTTP-based transports don't abort an in-progress request if the reply has already started. After HttpClient.SendAsync returns an HttpResponseMessage, calling Abort() on the WCF channel doesn't propagate to the code receiving the response body, causing stream reads to hang until timeout.

Solution

The fix follows the same pattern already used for the send phase:

Modified HttpResponseMessageHelper:

  • Added optional CancellationToken abortToken parameter to constructor to receive the abort signal
  • Created GetCombinedCancellationTokenAsync() helper method that combines the timeout token with the abort token using CancellationTokenSource.CreateLinkedTokenSource()
  • Cached the combined token to avoid repeated allocations
  • Updated all stream reading operations to use the combined cancellation token:
    • GetStreamAsync()
    • ReadChunkedBufferedMessageAsync()
    • ReadBufferedMessageAsync()
    • DecodeBufferedMessageAsync()

Modified HttpClientChannelAsyncRequest.ReceiveReplyAsync():

  • Now passes _httpSendCts.Token to HttpResponseMessageHelper constructor
  • When Abort() is called, _httpSendCts is cancelled via the existing Cleanup() method
  • This cancellation now propagates to all ongoing stream read operations

Test Implementation

  • Created: HttpStreamingAbortTests.4.1.0.cs with sync and async variants
  • Approach: Uses CustomBinding with TransferMode.StreamedResponse, reads partial data (1KB from 500KB stream), calls Abort(), then attempts continued reading
  • Validation: If abort works, throws CommunicationObjectAbortedException, IOException, CommunicationException, or OperationCanceledException. If broken, hangs until ReceiveTimeout expires.
// Start streaming response
responseStream = serviceProxy.GetStreamFromString(largeData);
byte[] buffer = new byte[1024];
int bytesRead = responseStream.Read(buffer, 0, buffer.Length);

// Abort while receiving
((ICommunicationObject)serviceProxy).Abort();

// Should throw exception, not hang
responseStream.Read(buffer, 0, buffer.Length); 

Tests use existing CustomTextEncoderStreamed endpoint. Both synchronous and async patterns covered.

Original prompt

This section details on the original issue you should resolve

<issue_title>HTTP based transports don't abort an in progress request if the reply has already started</issue_title>
<issue_description>If you are using an Http based binding, and you call Abort() on the client channel proxy after the call to HttpClient.SendAsync has returned an HttpResponseMessage, it doesn't propagate aborting of the WCF channel to the code that's receiving the response body. This means if the response body is being read, and something happens where the server stops responding, calling Abort() on the channel doesn't cancel the ReadAsync call on the response stream.

Relying on the binding SendTimeout or the channel OperationTimeout still works as we create a CancellationToken based on the remaining time left in the operation and that will be honored.

Calling Abort() while the request is being sent does get honored as the cancellation token we create for timing out calling HttpClient.SendAsync isn't directly passed, but instead a callback is registered against it which cancels the request wide CancellationTokenSource, the CancellationToken for which is then passed to the SendAsync call. That code looks similar to this:

var timeoutToken = await _timeoutHelper.GetCancellationTokenAsync();

using (timeoutToken.UnsafeRegister(s_cancelCts, _httpSendCts))
{
    _httpResponseMessage = await _httpClient.SendAsync(httpRequestMessage, HttpCompletionOption.ResponseHeadersRead, _httpSendCts.Token);
}

The callback s_cancelCts will cancel _httpSendCts. The CancellationTokenSource _httpSendCts gets cancelled when Abort is called on the request, so this pattern enables cancelling the call when the timeout hits, or when Abort() is manually called. This code is in System.ServiceModel.Channels.HttpChannelFactory<TChannel>.HttpClientRequestChannel.HttpClientChannelAsyncRequest which is in the file src\System.ServiceModel.Http\src\System\ServiceModel\Channels\HttpChannelFactory.cs

In the ReceiveReplyAsync method, a timeoutHelper is being created and passed along to a helper class/method HttpResponseMessageHelper.ParseIncomingResponse. This needs to be checked/validated, but I believe we could use the same registering of the callback with the CancellationToken from the TimeoutHelper, and pass _httpSendCts.Token to HttpResponseMessageHelper.ParseIncomingResponse instead. The potential things which could cause a problem are if the remaining or original timeout need to be retrieved from the TimeoutHelper in a transitive call inside ParseIncomingResponse, e.g. for formatting a string used in an exception message.</issue_description>

<agent_instructions>Write a unit test to validate the fix. We might already have a test service endpoint which purposefully delays the response to test for these types of scenarios. The test service would need to be using either TransferMode.StreamedResponse or TransferMode.Streamed and would return a Stream for the operation which has a delay in completing the response. That way we can be sure the client is in the receiving code when calling Abort(). Set up the test so that if Abort() doesn't work, the client call will have a TimeoutException thrown, but if the Abort() does work, a different exception gets thrown. I'm not sure which exception that would be, but I think it would be CommunicationObjectAbortedException.</agent_instructions>

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


💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

Copilot AI and others added 3 commits January 21, 2026 01:45
Co-authored-by: mconnew <8648390+mconnew@users.noreply.github.com>
Co-authored-by: mconnew <8648390+mconnew@users.noreply.github.com>
Co-authored-by: mconnew <8648390+mconnew@users.noreply.github.com>
@mconnew
Copy link
Member

mconnew commented Jan 21, 2026

@copilot, you didn't make any changes to address the problem described in the issue. You only wrote the test that was described in the additional instructions.

Copilot AI and others added 3 commits January 21, 2026 01:49
Co-authored-by: mconnew <8648390+mconnew@users.noreply.github.com>
Co-authored-by: mconnew <8648390+mconnew@users.noreply.github.com>
Co-authored-by: mconnew <8648390+mconnew@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix HTTP transport abort behavior for in-progress requests Add unit tests for HTTP streaming Abort() during response reception Jan 21, 2026
Copilot AI requested a review from mconnew January 21, 2026 01:56
Copy link
Contributor Author

Copilot AI commented Jan 21, 2026

@copilot, you didn't make any changes to address the problem described in the issue. You only wrote the test that was described in the additional instructions.

You're absolutely right - I misunderstood the original instructions. I only created the unit test but didn't implement the actual fix for the issue. I'm now implementing the fix to propagate Abort() to the response stream reading operations.

Copilot AI and others added 3 commits January 21, 2026 02:02
- Added CancellationToken parameter to HttpResponseMessageHelper constructor
- Created GetCombinedCancellationTokenAsync helper to combine timeout and abort tokens
- Updated all stream reading operations to use combined cancellation token
- Pass _httpSendCts.Token from HttpClientChannelAsyncRequest to enable abort during response reception

Co-authored-by: mconnew <8648390+mconnew@users.noreply.github.com>
- Added detailed comments explaining why the linked CTS is not explicitly disposed
- Clarified that HttpResponseMessageHelper has short lifetime and CTS will be garbage collected
- Fixed CancellationToken default comparison to use CanBeCanceled check

Co-authored-by: mconnew <8648390+mconnew@users.noreply.github.com>
- Added _linkedCts and _cachedCombinedToken fields to HttpResponseMessageHelper
- Modified GetCombinedCancellationTokenAsync to cache the combined token on first call
- This avoids creating multiple linked CancellationTokenSource instances
- Improves resource management by creating only one CTS per helper instance

Co-authored-by: mconnew <8648390+mconnew@users.noreply.github.com>
Copilot AI changed the title Add unit tests for HTTP streaming Abort() during response reception Fix HTTP streaming Abort() not canceling response reception and add unit tests Jan 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

HTTP based transports don't abort an in progress request if the reply has already started

2 participants