From cd3afd3e313690e89b2d7a661fffebee19d1cdbc Mon Sep 17 00:00:00 2001 From: Emmanuel ANDRE <2341261+manandre@users.noreply.github.com> Date: Tue, 25 Jun 2024 20:46:32 +0200 Subject: [PATCH 1/5] Add CancellationToken support for LoadIntoBufferAsync --- .../System.Net.Http/ref/System.Net.Http.cs | 2 + .../src/System/Net/Http/HttpContent.cs | 4 +- .../tests/FunctionalTests/HttpContentTest.cs | 92 +++++++++++++++++++ 3 files changed, 96 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..4c1a1657cc00b 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,10 @@ public Task LoadIntoBufferAsync() => public Task LoadIntoBufferAsync(long maxBufferSize) => LoadIntoBufferAsync(maxBufferSize, CancellationToken.None); - internal Task LoadIntoBufferAsync(CancellationToken cancellationToken) => + public Task LoadIntoBufferAsync(CancellationToken cancellationToken) => LoadIntoBufferAsync(MaxBufferSize, cancellationToken); - internal Task LoadIntoBufferAsync(long maxBufferSize, CancellationToken cancellationToken) + 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 74fb09117bd85..f37b18df2912e 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/HttpContentTest.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpContentTest.cs @@ -509,6 +509,98 @@ 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); + + var cts = new CancellationTokenSource(); + cts.Cancel(); + + await response.Content.LoadIntoBufferAsync(cts.Token); + }, + 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); + + var cts = new CancellationTokenSource(); + cts.Cancel(); + + await Assert.ThrowsAsync(() => response.Content.LoadIntoBufferAsync(cts.Token)); + }, + async server => + { + try + { + await server.AcceptConnectionSendResponseAndCloseAsync(); + } + catch (Exception ex) + { + _output.WriteLine($"Ignored exception:{Environment.NewLine}{ex}"); + } + }); + } + + [Fact] + public async Task LoadIntoBufferAsync_Unbuffered_CanBeCanceled() + { + var cts = new CancellationTokenSource(); + + await LoopbackServer.CreateClientAndServerAsync( + async uri => + { + using HttpClient httpClient = CreateHttpClient(); + + HttpResponseMessage response = await httpClient.GetAsync( + uri, + HttpCompletionOption.ResponseHeadersRead); + + await Assert.ThrowsAsync(() => response.Content.LoadIntoBufferAsync(cts.Token)); + }, + 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); + try + { + await connection.SendResponseAsync(new string('a', 100)); + } + catch (Exception ex) + { + _output.WriteLine($"Ignored exception:{Environment.NewLine}{ex}"); + } + }); + }); + } + [Theory] [InlineData(true)] [InlineData(false)] From 09ea276f4c9466e7c205192c1f31136a46b65814 Mon Sep 17 00:00:00 2001 From: Emmanuel ANDRE <2341261+manandre@users.noreply.github.com> Date: Wed, 26 Jun 2024 20:41:11 +0200 Subject: [PATCH 2/5] Add documentation --- .../src/System/Net/Http/HttpContent.cs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) 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 4c1a1657cc00b..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,9 +476,32 @@ public Task LoadIntoBufferAsync() => public Task LoadIntoBufferAsync(long maxBufferSize) => LoadIntoBufferAsync(maxBufferSize, CancellationToken.None); + /// + /// 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); + /// + /// 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(); From 70ec5c1033ad5d5392f031bc9991730670b3c1b1 Mon Sep 17 00:00:00 2001 From: Emmanuel ANDRE <2341261+manandre@users.noreply.github.com> Date: Thu, 27 Jun 2024 21:01:36 +0200 Subject: [PATCH 3/5] Rework cancellation token in tests --- .../tests/FunctionalTests/HttpContentTest.cs | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/HttpContentTest.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpContentTest.cs index f37b18df2912e..e323870fa4d9f 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/HttpContentTest.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpContentTest.cs @@ -523,10 +523,9 @@ await LoopbackServer.CreateClientAndServerAsync( uri, HttpCompletionOption.ResponseContentRead); - var cts = new CancellationTokenSource(); - cts.Cancel(); + CancellationToken cancellationToken = new CancellationToken(canceled: true); - await response.Content.LoadIntoBufferAsync(cts.Token); + await response.Content.LoadIntoBufferAsync(cancellationToken); }, async server => { @@ -546,10 +545,13 @@ await LoopbackServer.CreateClientAndServerAsync( uri, HttpCompletionOption.ResponseHeadersRead); - var cts = new CancellationTokenSource(); - cts.Cancel(); + CancellationToken cancellationToken = new CancellationToken(canceled: true); + + Task task = response.Content.LoadIntoBufferAsync(cancellationToken); - await Assert.ThrowsAsync(() => response.Content.LoadIntoBufferAsync(cts.Token)); + var exception = await Assert.ThrowsAsync(() => task); + + Assert.Equal(cancellationToken, exception.CancellationToken); }, async server => { @@ -572,13 +574,19 @@ public async Task LoadIntoBufferAsync_Unbuffered_CanBeCanceled() await LoopbackServer.CreateClientAndServerAsync( async uri => { - using HttpClient httpClient = CreateHttpClient(); + using HttpClient httpClient = base.CreateHttpClient(); HttpResponseMessage response = await httpClient.GetAsync( uri, HttpCompletionOption.ResponseHeadersRead); - await Assert.ThrowsAsync(() => response.Content.LoadIntoBufferAsync(cts.Token)); + CancellationToken cancellationToken = cts.Token; + + Task task = response.Content.LoadIntoBufferAsync(cancellationToken); + + var exception = await Assert.ThrowsAsync(() => task); + + Assert.Equal(cancellationToken, exception.CancellationToken); }, async server => { From f08c166dcfa271636de2817a9d69dadbec91ef6a Mon Sep 17 00:00:00 2001 From: Emmanuel ANDRE <2341261+manandre@users.noreply.github.com> Date: Thu, 27 Jun 2024 21:03:21 +0200 Subject: [PATCH 4/5] Move to outerloop test using delays --- .../System.Net.Http/tests/FunctionalTests/HttpContentTest.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/HttpContentTest.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpContentTest.cs index e323870fa4d9f..741d7712ed989 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/HttpContentTest.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpContentTest.cs @@ -566,6 +566,7 @@ await LoopbackServer.CreateClientAndServerAsync( }); } + [OuterLoop("Uses Task.Delay")] [Fact] public async Task LoadIntoBufferAsync_Unbuffered_CanBeCanceled() { From 9d36763d004e8add9560cc5f5b80360ff751c713 Mon Sep 17 00:00:00 2001 From: Miha Zupan Date: Mon, 8 Jul 2024 05:58:51 -0700 Subject: [PATCH 5/5] Use IgnoreExceptions helper in tests --- .../tests/FunctionalTests/HttpContentTest.cs | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/HttpContentTest.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpContentTest.cs index 741d7712ed989..8279129fdfb0a 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/HttpContentTest.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpContentTest.cs @@ -555,14 +555,7 @@ await LoopbackServer.CreateClientAndServerAsync( }, async server => { - try - { - await server.AcceptConnectionSendResponseAndCloseAsync(); - } - catch (Exception ex) - { - _output.WriteLine($"Ignored exception:{Environment.NewLine}{ex}"); - } + await IgnoreExceptions(server.AcceptConnectionSendResponseAndCloseAsync()); }); } @@ -598,14 +591,7 @@ await server.AcceptConnectionAsync(async connection => await Task.Delay(250); cts.Cancel(); await Task.Delay(500); - try - { - await connection.SendResponseAsync(new string('a', 100)); - } - catch (Exception ex) - { - _output.WriteLine($"Ignored exception:{Environment.NewLine}{ex}"); - } + await IgnoreExceptions(connection.SendResponseAsync(new string('a', 100))); }); }); }