From e650f3fe372df4e26fdc9a96402f805a65770178 Mon Sep 17 00:00:00 2001 From: campersau Date: Mon, 2 Mar 2026 17:10:49 +0100 Subject: [PATCH 1/3] Remove cancellation workaround --- src/Docker.DotNet/DockerClient.cs | 13 +++---------- src/Microsoft.Net.Http.Client/HttpConnection.cs | 8 ++++---- .../HttpConnectionResponseContent.cs | 15 ++++++++++++--- 3 files changed, 19 insertions(+), 17 deletions(-) diff --git a/src/Docker.DotNet/DockerClient.cs b/src/Docker.DotNet/DockerClient.cs index a93141a0..245e7c4b 100644 --- a/src/Docker.DotNet/DockerClient.cs +++ b/src/Docker.DotNet/DockerClient.cs @@ -364,20 +364,13 @@ private async Task PrivateMakeRequestAsync( if (Timeout.InfiniteTimeSpan == timeout) { - var tcs = new TaskCompletionSource( - TaskCreationOptions.RunContinuationsAsynchronously); - - using var registration = cancellationToken.Register( - () => tcs.TrySetCanceled(cancellationToken)); - - return await await Task.WhenAny(tcs.Task, _client.SendAsync(request, completionOption, cancellationToken)) + return await _client.SendAsync(request, completionOption, cancellationToken) .ConfigureAwait(false); } else { - using var timeoutCts = new CancellationTokenSource(timeout); - - using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(timeoutCts.Token, cancellationToken); + using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + linkedCts.CancelAfter(timeout); return await _client.SendAsync(request, completionOption, linkedCts.Token) .ConfigureAwait(false); diff --git a/src/Microsoft.Net.Http.Client/HttpConnection.cs b/src/Microsoft.Net.Http.Client/HttpConnection.cs index 5098ac9f..01ae75f6 100644 --- a/src/Microsoft.Net.Http.Client/HttpConnection.cs +++ b/src/Microsoft.Net.Http.Client/HttpConnection.cs @@ -2,7 +2,7 @@ namespace Microsoft.Net.Http.Client; internal sealed class HttpConnection : IDisposable { - private static readonly ISet DockerStreamHeaders = new HashSet{ "application/vnd.docker.raw-stream", "application/vnd.docker.multiplexed-stream" }; + private static readonly ISet DockerStreamHeaders = new HashSet { "application/vnd.docker.raw-stream", "application/vnd.docker.multiplexed-stream" }; public HttpConnection(BufferedReadStream transport) { @@ -41,7 +41,7 @@ public async Task SendAsync(HttpRequestMessage request, Can List responseLines = await ReadResponseLinesAsync(cancellationToken); // Receive body and determine the response type (Content-Length, Transfer-Encoding, Opaque) - return CreateResponseMessage(responseLines); + return CreateResponseMessage(responseLines, cancellationToken); } catch (Exception ex) { @@ -117,7 +117,7 @@ private async Task> ReadResponseLinesAsync(CancellationToken cancel return lines; } - private HttpResponseMessage CreateResponseMessage(List responseLines) + private HttpResponseMessage CreateResponseMessage(List responseLines, CancellationToken cancellationToken) { string responseLine = responseLines.First(); // HTTP/1.1 200 OK @@ -141,7 +141,7 @@ private HttpResponseMessage CreateResponseMessage(List responseLines) { response.ReasonPhrase = responseLineParts[2]; } - var content = new HttpConnectionResponseContent(this); + var content = new HttpConnectionResponseContent(this, cancellationToken); response.Content = content; foreach (var rawHeader in responseLines.Skip(1)) diff --git a/src/Microsoft.Net.Http.Client/HttpConnectionResponseContent.cs b/src/Microsoft.Net.Http.Client/HttpConnectionResponseContent.cs index 01a8ea16..99407dd4 100644 --- a/src/Microsoft.Net.Http.Client/HttpConnectionResponseContent.cs +++ b/src/Microsoft.Net.Http.Client/HttpConnectionResponseContent.cs @@ -3,11 +3,13 @@ namespace Microsoft.Net.Http.Client; public class HttpConnectionResponseContent : HttpContent { private readonly HttpConnection _connection; + private readonly CancellationToken _cancellationToken; private Stream _responseStream; - internal HttpConnectionResponseContent(HttpConnection connection) + internal HttpConnectionResponseContent(HttpConnection connection, CancellationToken cancellationToken) { _connection = connection; + _cancellationToken = cancellationToken; } internal void ResolveResponseStream(bool chunked, bool isConnectionUpgrade) @@ -40,11 +42,18 @@ public WriteClosableStream HijackStream() return _connection.Transport; } - protected override Task SerializeToStreamAsync(Stream stream, System.Net.TransportContext context) + protected override Task SerializeToStreamAsync(Stream stream, TransportContext context) { - return _responseStream.CopyToAsync(stream); + return _responseStream.CopyToAsync(stream, 81920, _cancellationToken); } +#if NET6_0_OR_GREATER + protected override Task SerializeToStreamAsync(Stream stream, TransportContext context, CancellationToken cancellationToken) + { + return _responseStream.CopyToAsync(stream, cancellationToken); + } +#endif + protected override Task CreateContentReadStreamAsync() { return Task.FromResult(_responseStream); From ff756bb99b3b0860ce1f767d1255145669816927 Mon Sep 17 00:00:00 2001 From: campersau Date: Mon, 2 Mar 2026 18:43:09 +0100 Subject: [PATCH 2/3] Keep TaskCompletionSource for netstandard --- src/Docker.DotNet/DockerClient.cs | 11 +++++++++++ src/Microsoft.Net.Http.Client/HttpConnection.cs | 14 +++++++++++--- .../HttpConnectionResponseContent.cs | 6 ++---- 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/src/Docker.DotNet/DockerClient.cs b/src/Docker.DotNet/DockerClient.cs index 245e7c4b..a55ad4f8 100644 --- a/src/Docker.DotNet/DockerClient.cs +++ b/src/Docker.DotNet/DockerClient.cs @@ -364,8 +364,19 @@ private async Task PrivateMakeRequestAsync( if (Timeout.InfiniteTimeSpan == timeout) { +#if NET6_0_OR_GREATER return await _client.SendAsync(request, completionOption, cancellationToken) .ConfigureAwait(false); +#else + var tcs = new TaskCompletionSource( + TaskCreationOptions.RunContinuationsAsynchronously); + + using var registration = cancellationToken.Register( + () => tcs.TrySetCanceled(cancellationToken)); + + return await await Task.WhenAny(tcs.Task, _client.SendAsync(request, completionOption, cancellationToken)) + .ConfigureAwait(false); +#endif } else { diff --git a/src/Microsoft.Net.Http.Client/HttpConnection.cs b/src/Microsoft.Net.Http.Client/HttpConnection.cs index 01ae75f6..2c2e7dfc 100644 --- a/src/Microsoft.Net.Http.Client/HttpConnection.cs +++ b/src/Microsoft.Net.Http.Client/HttpConnection.cs @@ -24,14 +24,22 @@ public async Task SendAsync(HttpRequestMessage request, Can { if (request.Content.Headers.ContentLength.HasValue) { +#if NET6_0_OR_GREATER + await request.Content.CopyToAsync(Transport, cancellationToken); +#else await request.Content.CopyToAsync(Transport); +#endif } else { // The length of the data is unknown. Send it in chunked mode. using (var chunkedStream = new ChunkedWriteStream(Transport)) { +#if NET6_0_OR_GREATER + await request.Content.CopyToAsync(chunkedStream, cancellationToken); +#else await request.Content.CopyToAsync(chunkedStream); +#endif await chunkedStream.EndContentAsync(cancellationToken); } } @@ -41,7 +49,7 @@ public async Task SendAsync(HttpRequestMessage request, Can List responseLines = await ReadResponseLinesAsync(cancellationToken); // Receive body and determine the response type (Content-Length, Transfer-Encoding, Opaque) - return CreateResponseMessage(responseLines, cancellationToken); + return CreateResponseMessage(responseLines); } catch (Exception ex) { @@ -117,7 +125,7 @@ private async Task> ReadResponseLinesAsync(CancellationToken cancel return lines; } - private HttpResponseMessage CreateResponseMessage(List responseLines, CancellationToken cancellationToken) + private HttpResponseMessage CreateResponseMessage(List responseLines) { string responseLine = responseLines.First(); // HTTP/1.1 200 OK @@ -141,7 +149,7 @@ private HttpResponseMessage CreateResponseMessage(List responseLines, Ca { response.ReasonPhrase = responseLineParts[2]; } - var content = new HttpConnectionResponseContent(this, cancellationToken); + var content = new HttpConnectionResponseContent(this); response.Content = content; foreach (var rawHeader in responseLines.Skip(1)) diff --git a/src/Microsoft.Net.Http.Client/HttpConnectionResponseContent.cs b/src/Microsoft.Net.Http.Client/HttpConnectionResponseContent.cs index 99407dd4..985e7f61 100644 --- a/src/Microsoft.Net.Http.Client/HttpConnectionResponseContent.cs +++ b/src/Microsoft.Net.Http.Client/HttpConnectionResponseContent.cs @@ -3,13 +3,11 @@ namespace Microsoft.Net.Http.Client; public class HttpConnectionResponseContent : HttpContent { private readonly HttpConnection _connection; - private readonly CancellationToken _cancellationToken; private Stream _responseStream; - internal HttpConnectionResponseContent(HttpConnection connection, CancellationToken cancellationToken) + internal HttpConnectionResponseContent(HttpConnection connection) { _connection = connection; - _cancellationToken = cancellationToken; } internal void ResolveResponseStream(bool chunked, bool isConnectionUpgrade) @@ -44,7 +42,7 @@ public WriteClosableStream HijackStream() protected override Task SerializeToStreamAsync(Stream stream, TransportContext context) { - return _responseStream.CopyToAsync(stream, 81920, _cancellationToken); + return _responseStream.CopyToAsync(stream); } #if NET6_0_OR_GREATER From 94d4e8d7c6c61e614023dcf75c0e1e9929ca1160 Mon Sep 17 00:00:00 2001 From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> Date: Mon, 2 Mar 2026 19:59:12 +0100 Subject: [PATCH 3/3] chore: Set configure await to false --- .../HttpConnection.cs | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/Microsoft.Net.Http.Client/HttpConnection.cs b/src/Microsoft.Net.Http.Client/HttpConnection.cs index 2c2e7dfc..fb8c1ab2 100644 --- a/src/Microsoft.Net.Http.Client/HttpConnection.cs +++ b/src/Microsoft.Net.Http.Client/HttpConnection.cs @@ -18,16 +18,20 @@ public async Task SendAsync(HttpRequestMessage request, Can // Serialize headers & send string rawRequest = SerializeRequest(request); byte[] requestBytes = Encoding.ASCII.GetBytes(rawRequest); - await Transport.WriteAsync(requestBytes, 0, requestBytes.Length, cancellationToken); + + await Transport.WriteAsync(requestBytes, 0, requestBytes.Length, cancellationToken) + .ConfigureAwait(false); if (request.Content != null) { if (request.Content.Headers.ContentLength.HasValue) { #if NET6_0_OR_GREATER - await request.Content.CopyToAsync(Transport, cancellationToken); + await request.Content.CopyToAsync(Transport, cancellationToken) + .ConfigureAwait(false); #else - await request.Content.CopyToAsync(Transport); + await request.Content.CopyToAsync(Transport) + .ConfigureAwait(false); #endif } else @@ -36,17 +40,21 @@ public async Task SendAsync(HttpRequestMessage request, Can using (var chunkedStream = new ChunkedWriteStream(Transport)) { #if NET6_0_OR_GREATER - await request.Content.CopyToAsync(chunkedStream, cancellationToken); + await request.Content.CopyToAsync(chunkedStream, cancellationToken) + .ConfigureAwait(false); #else - await request.Content.CopyToAsync(chunkedStream); + await request.Content.CopyToAsync(chunkedStream) + .ConfigureAwait(false); #endif - await chunkedStream.EndContentAsync(cancellationToken); + await chunkedStream.EndContentAsync(cancellationToken) + .ConfigureAwait(false); } } } // Receive headers - List responseLines = await ReadResponseLinesAsync(cancellationToken); + List responseLines = await ReadResponseLinesAsync(cancellationToken) + .ConfigureAwait(false); // Receive body and determine the response type (Content-Length, Transfer-Encoding, Opaque) return CreateResponseMessage(responseLines);