Skip to content

Commit

Permalink
Add CancellationToken support for LoadIntoBufferAsync (#103991)
Browse files Browse the repository at this point in the history
* 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 <mihazupan.zupan1@gmail.com>
  • Loading branch information
manandre and MihaZupan committed Jul 8, 2024
1 parent 8181785 commit edab50b
Show file tree
Hide file tree
Showing 3 changed files with 114 additions and 2 deletions.
2 changes: 2 additions & 0 deletions src/libraries/System.Net.Http/ref/System.Net.Http.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<byte[]> ReadAsByteArrayAsync() { throw null; }
public System.Threading.Tasks.Task<byte[]> ReadAsByteArrayAsync(System.Threading.CancellationToken cancellationToken) { throw null; }
public System.IO.Stream ReadAsStream() { throw null; }
Expand Down
27 changes: 25 additions & 2 deletions src/libraries/System.Net.Http/src/System/Net/Http/HttpContent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -476,10 +476,33 @@ public Task LoadIntoBufferAsync() =>
public Task LoadIntoBufferAsync(long maxBufferSize) =>
LoadIntoBufferAsync(maxBufferSize, CancellationToken.None);

internal Task LoadIntoBufferAsync(CancellationToken cancellationToken) =>
/// <summary>
/// Serialize the HTTP content to a memory buffer as an asynchronous operation.
/// </summary>
/// <param name="cancellationToken">The cancellation token to cancel the operation.</param>
/// <returns>The task object representing the asynchronous operation.</returns>
/// <remarks>
/// This operation will not block. The returned <see cref="Task"/> 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 <see cref="CopyToAsync(Stream)"/> methods will copy the content of the memory buffer to the target stream.
/// </remarks>
/// <exception cref="OperationCanceledException">The cancellation token was canceled. This exception is stored into the returned task.</exception>
/// <exception cref="ObjectDisposedException">The object has already been disposed.</exception>
public Task LoadIntoBufferAsync(CancellationToken cancellationToken) =>
LoadIntoBufferAsync(MaxBufferSize, cancellationToken);

internal Task LoadIntoBufferAsync(long maxBufferSize, CancellationToken cancellationToken)
/// <summary>
/// Serialize the HTTP content to a memory buffer as an asynchronous operation.
/// </summary>
/// <param name="maxBufferSize">The maximum size, in bytes, of the buffer to use.</param>
/// <param name="cancellationToken">The cancellation token to cancel the operation.</param>
/// <returns>The task object representing the asynchronous operation.</returns>
/// <remarks>
/// This operation will not block. The returned <see cref="Task"/> 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 <see cref="CopyToAsync(Stream)"/> methods will copy the content of the memory buffer to the target stream.
/// </remarks>
/// <exception cref="OperationCanceledException">The cancellation token was canceled. This exception is stored into the returned task.</exception>
/// <exception cref="ObjectDisposedException">The object has already been disposed.</exception>
public Task LoadIntoBufferAsync(long maxBufferSize, CancellationToken cancellationToken)
{
CheckDisposed();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -509,6 +509,93 @@ public async Task LoadIntoBufferAsync_ThrowIOExceptionInOverriddenAsyncMethod_Th
Assert.IsType<IOException>(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<TaskCanceledException>(() => 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<TaskCanceledException>(() => 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)]
Expand Down

0 comments on commit edab50b

Please sign in to comment.