From edab50b38e8aebc52228599109e755ddeb2849a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emmanuel=20Andr=C3=A9?= <2341261+manandre@users.noreply.github.com> Date: Mon, 8 Jul 2024 23:23:45 +0200 Subject: [PATCH] Add CancellationToken support for LoadIntoBufferAsync (#103991) * Add CancellationToken support for LoadIntoBufferAsync * Add documentation * Rework cancellation token in tests * Move to outerloop test using delays * Use IgnoreExceptions helper in tests --------- Co-authored-by: Miha Zupan --- .../System.Net.Http/ref/System.Net.Http.cs | 2 + .../src/System/Net/Http/HttpContent.cs | 27 +++++- .../tests/FunctionalTests/HttpContentTest.cs | 87 +++++++++++++++++++ 3 files changed, 114 insertions(+), 2 deletions(-) diff --git a/src/libraries/System.Net.Http/ref/System.Net.Http.cs b/src/libraries/System.Net.Http/ref/System.Net.Http.cs index 1261932527cca..2a7f27d96be35 100644 --- a/src/libraries/System.Net.Http/ref/System.Net.Http.cs +++ b/src/libraries/System.Net.Http/ref/System.Net.Http.cs @@ -186,7 +186,9 @@ public void CopyTo(System.IO.Stream stream, System.Net.TransportContext? context public void Dispose() { } protected virtual void Dispose(bool disposing) { } public System.Threading.Tasks.Task LoadIntoBufferAsync() { throw null; } + public System.Threading.Tasks.Task LoadIntoBufferAsync(System.Threading.CancellationToken cancellationToken) { throw null; } public System.Threading.Tasks.Task LoadIntoBufferAsync(long maxBufferSize) { throw null; } + public System.Threading.Tasks.Task LoadIntoBufferAsync(long maxBufferSize, System.Threading.CancellationToken cancellationToken) { throw null; } public System.Threading.Tasks.Task ReadAsByteArrayAsync() { throw null; } public System.Threading.Tasks.Task ReadAsByteArrayAsync(System.Threading.CancellationToken cancellationToken) { throw null; } public System.IO.Stream ReadAsStream() { throw null; } diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/HttpContent.cs b/src/libraries/System.Net.Http/src/System/Net/Http/HttpContent.cs index 2523829550e51..97edad575a8f5 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/HttpContent.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/HttpContent.cs @@ -476,10 +476,33 @@ public Task LoadIntoBufferAsync() => public Task LoadIntoBufferAsync(long maxBufferSize) => LoadIntoBufferAsync(maxBufferSize, CancellationToken.None); - internal Task LoadIntoBufferAsync(CancellationToken cancellationToken) => + /// + /// Serialize the HTTP content to a memory buffer as an asynchronous operation. + /// + /// The cancellation token to cancel the operation. + /// The task object representing the asynchronous operation. + /// + /// This operation will not block. The returned object will complete after all of the content has been serialized to the memory buffer. + /// After content is serialized to a memory buffer, calls to one of the methods will copy the content of the memory buffer to the target stream. + /// + /// The cancellation token was canceled. This exception is stored into the returned task. + /// The object has already been disposed. + public Task LoadIntoBufferAsync(CancellationToken cancellationToken) => LoadIntoBufferAsync(MaxBufferSize, cancellationToken); - internal Task LoadIntoBufferAsync(long maxBufferSize, CancellationToken cancellationToken) + /// + /// Serialize the HTTP content to a memory buffer as an asynchronous operation. + /// + /// The maximum size, in bytes, of the buffer to use. + /// The cancellation token to cancel the operation. + /// The task object representing the asynchronous operation. + /// + /// This operation will not block. The returned object will complete after all of the content has been serialized to the memory buffer. + /// After content is serialized to a memory buffer, calls to one of the methods will copy the content of the memory buffer to the target stream. + /// + /// The cancellation token was canceled. This exception is stored into the returned task. + /// The object has already been disposed. + public Task LoadIntoBufferAsync(long maxBufferSize, CancellationToken cancellationToken) { CheckDisposed(); diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/HttpContentTest.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpContentTest.cs index 6cb10e6ca7460..01c364f87175a 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/HttpContentTest.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpContentTest.cs @@ -509,6 +509,93 @@ public async Task LoadIntoBufferAsync_ThrowIOExceptionInOverriddenAsyncMethod_Th Assert.IsType(ex.InnerException); } + [Fact] + public async Task LoadIntoBufferAsync_Buffered_IgnoresCancellationToken() + { + string content = Guid.NewGuid().ToString(); + + await LoopbackServer.CreateClientAndServerAsync( + async uri => + { + using HttpClient httpClient = CreateHttpClient(); + + HttpResponseMessage response = await httpClient.GetAsync( + uri, + HttpCompletionOption.ResponseContentRead); + + CancellationToken cancellationToken = new CancellationToken(canceled: true); + + await response.Content.LoadIntoBufferAsync(cancellationToken); + }, + async server => + { + await server.AcceptConnectionSendResponseAndCloseAsync(content: content); + }); + } + + [Fact] + public async Task LoadIntoBufferAsync_Unbuffered_CanBeCanceled_AlreadyCanceledCts() + { + await LoopbackServer.CreateClientAndServerAsync( + async uri => + { + using HttpClient httpClient = CreateHttpClient(); + + HttpResponseMessage response = await httpClient.GetAsync( + uri, + HttpCompletionOption.ResponseHeadersRead); + + CancellationToken cancellationToken = new CancellationToken(canceled: true); + + Task task = response.Content.LoadIntoBufferAsync(cancellationToken); + + var exception = await Assert.ThrowsAsync(() => task); + + Assert.Equal(cancellationToken, exception.CancellationToken); + }, + async server => + { + await IgnoreExceptions(server.AcceptConnectionSendResponseAndCloseAsync()); + }); + } + + [OuterLoop("Uses Task.Delay")] + [Fact] + public async Task LoadIntoBufferAsync_Unbuffered_CanBeCanceled() + { + var cts = new CancellationTokenSource(); + + await LoopbackServer.CreateClientAndServerAsync( + async uri => + { + using HttpClient httpClient = base.CreateHttpClient(); + + HttpResponseMessage response = await httpClient.GetAsync( + uri, + HttpCompletionOption.ResponseHeadersRead); + + CancellationToken cancellationToken = cts.Token; + + Task task = response.Content.LoadIntoBufferAsync(cancellationToken); + + var exception = await Assert.ThrowsAsync(() => task); + + Assert.Equal(cancellationToken, exception.CancellationToken); + }, + async server => + { + await server.AcceptConnectionAsync(async connection => + { + await connection.ReadRequestHeaderAsync(); + await connection.SendResponseAsync(LoopbackServer.GetHttpResponseHeaders(contentLength: 100)); + await Task.Delay(250); + cts.Cancel(); + await Task.Delay(500); + await IgnoreExceptions(connection.SendResponseAsync(new string('a', 100))); + }); + }); + } + [Theory] [InlineData(true)] [InlineData(false)]