Skip to content

Commit d2d5ad3

Browse files
authored
Improve HTTP/1 response header parsing (#74393)
* Improve HTTP/1 response header parsing * Improve worst-case performance * Account for line folds in FillForHeadersAsync * PR feedback * Extend header trickle test for line folds * RIP goto * Clarify the meaning of 'valueIter' * Handle Scavenge zero-byte reads in test
1 parent 15aeb77 commit d2d5ad3

File tree

4 files changed

+515
-221
lines changed

4 files changed

+515
-221
lines changed

src/libraries/Common/tests/System/IO/DelegateDelegatingStream.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ namespace System.IO
99
/// <summary>Provides a stream whose implementation is supplied by delegates or by an inner stream.</summary>
1010
internal sealed class DelegateDelegatingStream : DelegatingStream
1111
{
12+
public delegate int ReadSpanDelegate(Span<byte> buffer);
13+
1214
public static DelegateDelegatingStream NopDispose(Stream innerStream) =>
1315
new DelegateDelegatingStream(innerStream)
1416
{
@@ -27,6 +29,7 @@ public DelegateDelegatingStream(Stream innerStream) : base(innerStream) { }
2729
public Func<long> GetPositionFunc { get; set; }
2830
public Action<long> SetPositionFunc { get; set; }
2931
public Func<byte[], int, int, int> ReadFunc { get; set; }
32+
public ReadSpanDelegate ReadSpanFunc { get; set; }
3033
public Func<byte[], int, int, CancellationToken, Task<int>> ReadAsyncArrayFunc { get; set; }
3134
public Func<Memory<byte>, CancellationToken, ValueTask<int>> ReadAsyncMemoryFunc { get; set; }
3235
public Func<long, SeekOrigin, long> SeekFunc { get; set; }
@@ -48,6 +51,7 @@ public DelegateDelegatingStream(Stream innerStream) : base(innerStream) { }
4851
public override long Position => GetPositionFunc != null ? GetPositionFunc() : base.Position;
4952

5053
public override int Read(byte[] buffer, int offset, int count) => ReadFunc != null ? ReadFunc(buffer, offset, count) : base.Read(buffer, offset, count);
54+
public override int Read(Span<byte> buffer) => ReadSpanFunc != null ? ReadSpanFunc(buffer) : base.Read(buffer);
5155
public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => ReadAsyncArrayFunc != null ? ReadAsyncArrayFunc(buffer, offset, count, cancellationToken) : base.ReadAsync(buffer, offset, count, cancellationToken);
5256
public override ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default) => ReadAsyncMemoryFunc != null ? ReadAsyncMemoryFunc(buffer, cancellationToken) : base.ReadAsync(buffer, cancellationToken);
5357

src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ChunkedEncodingReadStream.cs

Lines changed: 40 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ public override int Read(Span<byte> buffer)
9494
}
9595

9696
// We're only here if we need more data to make forward progress.
97-
_connection.Fill();
97+
Fill();
9898

9999
// Now that we have more, see if we can get any response data, and if
100100
// we can we're done.
@@ -210,7 +210,7 @@ private async ValueTask<int> ReadAsyncCore(Memory<byte> buffer, CancellationToke
210210
}
211211

212212
// We're only here if we need more data to make forward progress.
213-
await _connection.FillAsync(async: true).ConfigureAwait(false);
213+
await FillAsync().ConfigureAwait(false);
214214

215215
// Now that we have more, see if we can get any response data, and if
216216
// we can we're done.
@@ -273,7 +273,7 @@ private async Task CopyToAsyncCore(Stream destination, CancellationToken cancell
273273
return;
274274
}
275275

276-
await _connection.FillAsync(async: true).ConfigureAwait(false);
276+
await FillAsync().ConfigureAwait(false);
277277
}
278278
}
279279
catch (Exception exc) when (CancellationHelper.ShouldWrapInOperationCanceledException(exc, cancellationToken))
@@ -323,7 +323,7 @@ private int ReadChunksFromConnectionBuffer(Span<byte> buffer, CancellationTokenR
323323
Debug.Assert(_chunkBytesRemaining == 0, $"Expected {nameof(_chunkBytesRemaining)} == 0, got {_chunkBytesRemaining}");
324324

325325
// Read the chunk header line.
326-
if (!_connection.TryReadNextChunkedLine(readingHeader: false, out currentLine))
326+
if (!_connection.TryReadNextChunkedLine(out currentLine))
327327
{
328328
// Could not get a whole line, so we can't parse the chunk header.
329329
return default;
@@ -379,7 +379,7 @@ private int ReadChunksFromConnectionBuffer(Span<byte> buffer, CancellationTokenR
379379
case ParsingState.ExpectChunkTerminator:
380380
Debug.Assert(_chunkBytesRemaining == 0, $"Expected {nameof(_chunkBytesRemaining)} == 0, got {_chunkBytesRemaining}");
381381

382-
if (!_connection.TryReadNextChunkedLine(readingHeader: false, out currentLine))
382+
if (!_connection.TryReadNextChunkedLine(out currentLine))
383383
{
384384
return default;
385385
}
@@ -395,38 +395,23 @@ private int ReadChunksFromConnectionBuffer(Span<byte> buffer, CancellationTokenR
395395
case ParsingState.ConsumeTrailers:
396396
Debug.Assert(_chunkBytesRemaining == 0, $"Expected {nameof(_chunkBytesRemaining)} == 0, got {_chunkBytesRemaining}");
397397

398-
while (true)
398+
// Consume the receive buffer. If the stream is disposed, pass a null response to avoid
399+
// processing headers for a connection returned to the pool.
400+
if (_connection.ParseHeaders(IsDisposed ? null : _response, isFromTrailer: true))
399401
{
400-
if (!_connection.TryReadNextChunkedLine(readingHeader: true, out currentLine))
401-
{
402-
break;
403-
}
404-
405-
if (currentLine.IsEmpty)
406-
{
407-
// Dispose of the registration and then check whether cancellation has been
408-
// requested. This is necessary to make deterministic a race condition between
409-
// cancellation being requested and unregistering from the token. Otherwise,
410-
// it's possible cancellation could be requested just before we unregister and
411-
// we then return a connection to the pool that has been or will be disposed
412-
// (e.g. if a timer is used and has already queued its callback but the
413-
// callback hasn't yet run).
414-
cancellationRegistration.Dispose();
415-
CancellationHelper.ThrowIfCancellationRequested(cancellationRegistration.Token);
416-
417-
_state = ParsingState.Done;
418-
_connection.CompleteResponse();
419-
_connection = null;
420-
421-
break;
422-
}
423-
// Parse the trailer.
424-
else if (!IsDisposed)
425-
{
426-
// Make sure that we don't inadvertently consume trailing headers
427-
// while draining a connection that's being returned back to the pool.
428-
HttpConnection.ParseHeaderNameValue(_connection, currentLine, _response, isFromTrailer: true);
429-
}
402+
// Dispose of the registration and then check whether cancellation has been
403+
// requested. This is necessary to make deterministic a race condition between
404+
// cancellation being requested and unregistering from the token. Otherwise,
405+
// it's possible cancellation could be requested just before we unregister and
406+
// we then return a connection to the pool that has been or will be disposed
407+
// (e.g. if a timer is used and has already queued its callback but the
408+
// callback hasn't yet run).
409+
cancellationRegistration.Dispose();
410+
CancellationHelper.ThrowIfCancellationRequested(cancellationRegistration.Token);
411+
412+
_state = ParsingState.Done;
413+
_connection.CompleteResponse();
414+
_connection = null;
430415
}
431416

432417
return default;
@@ -528,7 +513,7 @@ public override async ValueTask<bool> DrainAsync(int maxDrainBytes)
528513
}
529514
}
530515

531-
await _connection.FillAsync(async: true).ConfigureAwait(false);
516+
await FillAsync().ConfigureAwait(false);
532517
}
533518
}
534519
finally
@@ -537,6 +522,24 @@ public override async ValueTask<bool> DrainAsync(int maxDrainBytes)
537522
cts?.Dispose();
538523
}
539524
}
525+
526+
private void Fill()
527+
{
528+
Debug.Assert(_connection is not null);
529+
ValueTask fillTask = _state == ParsingState.ConsumeTrailers
530+
? _connection.FillForHeadersAsync(async: false)
531+
: _connection.FillAsync(async: false);
532+
Debug.Assert(fillTask.IsCompleted);
533+
fillTask.GetAwaiter().GetResult();
534+
}
535+
536+
private ValueTask FillAsync()
537+
{
538+
Debug.Assert(_connection is not null);
539+
return _state == ParsingState.ConsumeTrailers
540+
? _connection.FillForHeadersAsync(async: true)
541+
: _connection.FillAsync(async: true);
542+
}
540543
}
541544
}
542545
}

0 commit comments

Comments
 (0)