Skip to content

Commit edab50b

Browse files
manandreMihaZupan
andauthored
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 <mihazupan.zupan1@gmail.com>
1 parent 8181785 commit edab50b

3 files changed

Lines changed: 114 additions & 2 deletions

File tree

src/libraries/System.Net.Http/ref/System.Net.Http.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,9 @@ public void CopyTo(System.IO.Stream stream, System.Net.TransportContext? context
186186
public void Dispose() { }
187187
protected virtual void Dispose(bool disposing) { }
188188
public System.Threading.Tasks.Task LoadIntoBufferAsync() { throw null; }
189+
public System.Threading.Tasks.Task LoadIntoBufferAsync(System.Threading.CancellationToken cancellationToken) { throw null; }
189190
public System.Threading.Tasks.Task LoadIntoBufferAsync(long maxBufferSize) { throw null; }
191+
public System.Threading.Tasks.Task LoadIntoBufferAsync(long maxBufferSize, System.Threading.CancellationToken cancellationToken) { throw null; }
190192
public System.Threading.Tasks.Task<byte[]> ReadAsByteArrayAsync() { throw null; }
191193
public System.Threading.Tasks.Task<byte[]> ReadAsByteArrayAsync(System.Threading.CancellationToken cancellationToken) { throw null; }
192194
public System.IO.Stream ReadAsStream() { throw null; }

src/libraries/System.Net.Http/src/System/Net/Http/HttpContent.cs

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -476,10 +476,33 @@ public Task LoadIntoBufferAsync() =>
476476
public Task LoadIntoBufferAsync(long maxBufferSize) =>
477477
LoadIntoBufferAsync(maxBufferSize, CancellationToken.None);
478478

479-
internal Task LoadIntoBufferAsync(CancellationToken cancellationToken) =>
479+
/// <summary>
480+
/// Serialize the HTTP content to a memory buffer as an asynchronous operation.
481+
/// </summary>
482+
/// <param name="cancellationToken">The cancellation token to cancel the operation.</param>
483+
/// <returns>The task object representing the asynchronous operation.</returns>
484+
/// <remarks>
485+
/// 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.
486+
/// 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.
487+
/// </remarks>
488+
/// <exception cref="OperationCanceledException">The cancellation token was canceled. This exception is stored into the returned task.</exception>
489+
/// <exception cref="ObjectDisposedException">The object has already been disposed.</exception>
490+
public Task LoadIntoBufferAsync(CancellationToken cancellationToken) =>
480491
LoadIntoBufferAsync(MaxBufferSize, cancellationToken);
481492

482-
internal Task LoadIntoBufferAsync(long maxBufferSize, CancellationToken cancellationToken)
493+
/// <summary>
494+
/// Serialize the HTTP content to a memory buffer as an asynchronous operation.
495+
/// </summary>
496+
/// <param name="maxBufferSize">The maximum size, in bytes, of the buffer to use.</param>
497+
/// <param name="cancellationToken">The cancellation token to cancel the operation.</param>
498+
/// <returns>The task object representing the asynchronous operation.</returns>
499+
/// <remarks>
500+
/// 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.
501+
/// 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.
502+
/// </remarks>
503+
/// <exception cref="OperationCanceledException">The cancellation token was canceled. This exception is stored into the returned task.</exception>
504+
/// <exception cref="ObjectDisposedException">The object has already been disposed.</exception>
505+
public Task LoadIntoBufferAsync(long maxBufferSize, CancellationToken cancellationToken)
483506
{
484507
CheckDisposed();
485508

src/libraries/System.Net.Http/tests/FunctionalTests/HttpContentTest.cs

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -509,6 +509,93 @@ public async Task LoadIntoBufferAsync_ThrowIOExceptionInOverriddenAsyncMethod_Th
509509
Assert.IsType<IOException>(ex.InnerException);
510510
}
511511

512+
[Fact]
513+
public async Task LoadIntoBufferAsync_Buffered_IgnoresCancellationToken()
514+
{
515+
string content = Guid.NewGuid().ToString();
516+
517+
await LoopbackServer.CreateClientAndServerAsync(
518+
async uri =>
519+
{
520+
using HttpClient httpClient = CreateHttpClient();
521+
522+
HttpResponseMessage response = await httpClient.GetAsync(
523+
uri,
524+
HttpCompletionOption.ResponseContentRead);
525+
526+
CancellationToken cancellationToken = new CancellationToken(canceled: true);
527+
528+
await response.Content.LoadIntoBufferAsync(cancellationToken);
529+
},
530+
async server =>
531+
{
532+
await server.AcceptConnectionSendResponseAndCloseAsync(content: content);
533+
});
534+
}
535+
536+
[Fact]
537+
public async Task LoadIntoBufferAsync_Unbuffered_CanBeCanceled_AlreadyCanceledCts()
538+
{
539+
await LoopbackServer.CreateClientAndServerAsync(
540+
async uri =>
541+
{
542+
using HttpClient httpClient = CreateHttpClient();
543+
544+
HttpResponseMessage response = await httpClient.GetAsync(
545+
uri,
546+
HttpCompletionOption.ResponseHeadersRead);
547+
548+
CancellationToken cancellationToken = new CancellationToken(canceled: true);
549+
550+
Task task = response.Content.LoadIntoBufferAsync(cancellationToken);
551+
552+
var exception = await Assert.ThrowsAsync<TaskCanceledException>(() => task);
553+
554+
Assert.Equal(cancellationToken, exception.CancellationToken);
555+
},
556+
async server =>
557+
{
558+
await IgnoreExceptions(server.AcceptConnectionSendResponseAndCloseAsync());
559+
});
560+
}
561+
562+
[OuterLoop("Uses Task.Delay")]
563+
[Fact]
564+
public async Task LoadIntoBufferAsync_Unbuffered_CanBeCanceled()
565+
{
566+
var cts = new CancellationTokenSource();
567+
568+
await LoopbackServer.CreateClientAndServerAsync(
569+
async uri =>
570+
{
571+
using HttpClient httpClient = base.CreateHttpClient();
572+
573+
HttpResponseMessage response = await httpClient.GetAsync(
574+
uri,
575+
HttpCompletionOption.ResponseHeadersRead);
576+
577+
CancellationToken cancellationToken = cts.Token;
578+
579+
Task task = response.Content.LoadIntoBufferAsync(cancellationToken);
580+
581+
var exception = await Assert.ThrowsAsync<TaskCanceledException>(() => task);
582+
583+
Assert.Equal(cancellationToken, exception.CancellationToken);
584+
},
585+
async server =>
586+
{
587+
await server.AcceptConnectionAsync(async connection =>
588+
{
589+
await connection.ReadRequestHeaderAsync();
590+
await connection.SendResponseAsync(LoopbackServer.GetHttpResponseHeaders(contentLength: 100));
591+
await Task.Delay(250);
592+
cts.Cancel();
593+
await Task.Delay(500);
594+
await IgnoreExceptions(connection.SendResponseAsync(new string('a', 100)));
595+
});
596+
});
597+
}
598+
512599
[Theory]
513600
[InlineData(true)]
514601
[InlineData(false)]

0 commit comments

Comments
 (0)