diff --git a/src/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ChunkedEncodingReadStream.cs b/src/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ChunkedEncodingReadStream.cs index e9c0380603c2..28e287007378 100644 --- a/src/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ChunkedEncodingReadStream.cs +++ b/src/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ChunkedEncodingReadStream.cs @@ -22,8 +22,6 @@ private sealed class ChunkedEncodingReadStream : HttpContentReadStream /// infinite chunk length is sent. This value is arbitrary and can be changed as needed. /// private const int MaxChunkBytesAllowed = 16*1024; - /// How long a trailing header can be. This value is arbitrary and can be changed as needed. - private const int MaxTrailingHeaderLength = 16*1024; /// The number of bytes remaining in the chunk. private ulong _chunkBytesRemaining; /// The current state of the parsing state machine for the chunked response. @@ -298,6 +296,9 @@ private ReadOnlyMemory ReadChunkFromConnectionBuffer(int maxBytesToRead, C else { _state = ParsingState.ConsumeTrailers; + // Apply the MaxResponseHeadersLength limit to all trailing headers. + // The limit is applied to regular response headers and trailing headers separately. + _connection._allowedReadLineBytes = _connection.MaxResponseHeadersLength; goto case ParsingState.ConsumeTrailers; } @@ -345,7 +346,6 @@ private ReadOnlyMemory ReadChunkFromConnectionBuffer(int maxBytesToRead, C while (true) { // TODO: Consider adding folded trailing header support #35769. - _connection._allowedReadLineBytes = MaxTrailingHeaderLength; if (!_connection.TryReadNextLine(out currentLine)) { break; diff --git a/src/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnection.cs b/src/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnection.cs index fe87ca09567e..b329fa9f72cc 100644 --- a/src/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnection.cs +++ b/src/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnection.cs @@ -205,6 +205,8 @@ public bool EnsureReadAheadAndPollRead() public HttpConnectionKind Kind => _pool.Kind; + private int MaxResponseHeadersLength => (int)Math.Min(int.MaxValue, _pool.Settings._maxResponseHeadersLength * 1024L); + private int ReadBufferSize => _readBuffer.Length; private ReadOnlyMemory RemainingBuffer => new ReadOnlyMemory(_readBuffer, _readOffset, _readLength - _readOffset); @@ -480,7 +482,7 @@ public async Task SendAsyncCore(HttpRequestMessage request, } // Start to read response. - _allowedReadLineBytes = (int)Math.Min(int.MaxValue, _pool.Settings._maxResponseHeadersLength * 1024L); + _allowedReadLineBytes = MaxResponseHeadersLength; // We should not have any buffered data here; if there was, it should have been treated as an error // by the previous request handling. (Note we do not support HTTP pipelining.) @@ -1243,7 +1245,7 @@ private bool TryReadNextLine(out ReadOnlySpan line) { if (_allowedReadLineBytes < buffer.Length) { - throw new HttpRequestException(SR.Format(SR.net_http_response_headers_exceeded_length, _pool.Settings._maxResponseHeadersLength * 1024L)); + throw new HttpRequestException(SR.Format(SR.net_http_response_headers_exceeded_length, MaxResponseHeadersLength)); } line = default; @@ -1354,7 +1356,7 @@ private void ThrowIfExceededAllowedReadLineBytes() { if (_allowedReadLineBytes < 0) { - throw new HttpRequestException(SR.Format(SR.net_http_response_headers_exceeded_length, _pool.Settings._maxResponseHeadersLength * 1024L)); + throw new HttpRequestException(SR.Format(SR.net_http_response_headers_exceeded_length, MaxResponseHeadersLength)); } } diff --git a/src/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.cs b/src/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.cs index c8e50ca46af1..1bf1c13f6c95 100644 --- a/src/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.cs +++ b/src/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.cs @@ -767,6 +767,60 @@ await TestHelper.WhenAllCompletedOrAnyFailed( } }); } + + [Theory] + [InlineData(1024, 64, false)] + [InlineData(1024, 1024 - 2, false)] // we need at least 2 spare bytes for the next CRLF + [InlineData(1024, 1024 - 1, true)] + [InlineData(1024, 1024, true)] + [InlineData(1024, 1024 + 1, true)] + [InlineData(1024 * 1024, 1024 * 1024 - 2, false)] + [InlineData(1024 * 1024, 1024 * 1024 - 1, true)] + [InlineData(1024 * 1024, 1024 * 1024, true)] + public async Task GetAsync_MaxResponseHeadersLength_EnforcedOnTrailingHeaders(int maxResponseHeadersLength, int trailersLength, bool shouldThrow) + { + await LoopbackServer.CreateClientAndServerAsync( + async uri => + { + using HttpClientHandler handler = CreateHttpClientHandler(); + using HttpClient client = CreateHttpClient(handler); + + handler.MaxResponseHeadersLength = maxResponseHeadersLength / 1024; + + if (shouldThrow) + { + await Assert.ThrowsAsync(() => client.GetAsync(uri)); + } + else + { + (await client.GetAsync(uri)).Dispose(); + } + }, + async server => + { + try + { + const string TrailerName1 = "My-Trailer-1"; + const string TrailerName2 = "My-Trailer-2"; + + int trailerOneLength = trailersLength / 2; + int trailerTwoLength = trailersLength - trailerOneLength; + + await server.AcceptConnectionSendCustomResponseAndCloseAsync( + "HTTP/1.1 200 OK\r\n" + + "Connection: close\r\n" + + "Transfer-Encoding: chunked\r\n" + + "\r\n" + + "4\r\n" + + "data\r\n" + + "0\r\n" + + $"{TrailerName1}: {new string('a', trailerOneLength - TrailerName1.Length - 4)}\r\n" + + $"{TrailerName2}: {new string('b', trailerTwoLength - TrailerName2.Length - 4)}\r\n" + + "\r\n"); + } + catch { } + }); + } } public sealed class SocketsHttpHandler_Http2_TrailingHeaders_Test : SocketsHttpHandler_TrailingHeaders_Test