Skip to content

Commit

Permalink
Rework HttpClient content buffering (#109642)
Browse files Browse the repository at this point in the history
* Rework HttpClient response buffering

* Fix string preamble detection order

* Less var

* Lower initial buffer size for chunked responses to 16 KB

* Apply some style changes
  • Loading branch information
MihaZupan authored Dec 1, 2024
1 parent e7d837d commit 5b9b8d3
Show file tree
Hide file tree
Showing 6 changed files with 638 additions and 443 deletions.
35 changes: 14 additions & 21 deletions src/libraries/System.Net.Http/src/System/Net/Http/HttpClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -199,9 +199,12 @@ private async Task<string> GetStringAsyncCore(HttpRequestMessage request, Cancel

// Since the underlying byte[] will never be exposed, we use an ArrayPool-backed
// stream to which we copy all of the data from the response.
using Stream responseStream = c.TryReadAsStream() ?? await c.ReadAsStreamAsync(cts.Token).ConfigureAwait(false);
using var buffer = new HttpContent.LimitArrayPoolWriteStream(_maxResponseContentBufferSize, (int)c.Headers.ContentLength.GetValueOrDefault());
using var buffer = new HttpContent.LimitArrayPoolWriteStream(
_maxResponseContentBufferSize,
c.Headers.ContentLength.GetValueOrDefault(),
getFinalSizeFromPool: true);

using Stream responseStream = c.TryReadAsStream() ?? await c.ReadAsStreamAsync(cts.Token).ConfigureAwait(false);
try
{
await responseStream.CopyToAsync(buffer, cts.Token).ConfigureAwait(false);
Expand All @@ -211,14 +214,8 @@ private async Task<string> GetStringAsyncCore(HttpRequestMessage request, Cancel
throw HttpContent.WrapStreamCopyException(e);
}

if (buffer.Length > 0)
{
// Decode and return the data from the buffer.
return HttpContent.ReadBufferAsString(buffer.GetBuffer(), c.Headers);
}

// No content to return.
return string.Empty;
// Decode and return the data from the buffer.
return HttpContent.ReadBufferAsString(buffer, c.Headers);
}
catch (Exception e)
{
Expand Down Expand Up @@ -272,17 +269,16 @@ private async Task<byte[]> GetByteArrayAsyncCore(HttpRequestMessage request, Can
responseContentTelemetryStarted = true;
}

// If we got a content length, then we assume that it's correct and create a MemoryStream
// to which the content will be transferred. That way, assuming we actually get the exact
// amount we were expecting, we can simply return the MemoryStream's underlying buffer.
// If we got a content length, then we assume that it's correct. If that's the case,
// we can opportunistically allocate the exact-sized buffer while buffering the content.
// If we didn't get a content length, then we assume we're going to have to grow
// the buffer potentially several times and that it's unlikely the underlying buffer
// at the end will be the exact size needed, in which case it's more beneficial to use
// ArrayPool buffers and copy out to a new array at the end.
long? contentLength = c.Headers.ContentLength;
using Stream buffer = contentLength.HasValue ?
new HttpContent.LimitMemoryStream(_maxResponseContentBufferSize, (int)contentLength.GetValueOrDefault()) :
new HttpContent.LimitArrayPoolWriteStream(_maxResponseContentBufferSize);
using var buffer = new HttpContent.LimitArrayPoolWriteStream(
_maxResponseContentBufferSize,
c.Headers.ContentLength.GetValueOrDefault(),
getFinalSizeFromPool: false);

using Stream responseStream = c.TryReadAsStream() ?? await c.ReadAsStreamAsync(cts.Token).ConfigureAwait(false);
try
Expand All @@ -294,10 +290,7 @@ private async Task<byte[]> GetByteArrayAsyncCore(HttpRequestMessage request, Can
throw HttpContent.WrapStreamCopyException(e);
}

return
buffer.Length == 0 ? Array.Empty<byte>() :
buffer is HttpContent.LimitMemoryStream lms ? lms.GetSizedBuffer() :
((HttpContent.LimitArrayPoolWriteStream)buffer).ToArray();
return buffer.ToArray();
}
catch (Exception e)
{
Expand Down
Loading

0 comments on commit 5b9b8d3

Please sign in to comment.