Skip to content

Commit 214b734

Browse files
authored
HTTP/3: Use max field section size setting (#33596)
1 parent a861e8d commit 214b734

File tree

20 files changed

+265
-60
lines changed

20 files changed

+265
-60
lines changed

src/Servers/Kestrel/Core/src/Http2Limits.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ public class Http2Limits
2525
/// <summary>
2626
/// Limits the number of concurrent request streams per HTTP/2 connection. Excess streams will be refused.
2727
/// <para>
28-
/// Value must be greater than 0, defaults to 100
28+
/// Value must be greater than 0, defaults to 100.
2929
/// </para>
3030
/// </summary>
3131
public int MaxStreamsPerConnection
@@ -45,7 +45,7 @@ public int MaxStreamsPerConnection
4545
/// <summary>
4646
/// Limits the size of the header compression tables, in octets, the HPACK encoder and decoder on the server can use.
4747
/// <para>
48-
/// Value must be greater than or equal to 0, defaults to 4096
48+
/// Value must be greater than or equal to 0, defaults to 4096.
4949
/// </para>
5050
/// </summary>
5151
public int HeaderTableSize
@@ -65,7 +65,7 @@ public int HeaderTableSize
6565
/// <summary>
6666
/// Indicates the size of the largest frame payload that is allowed to be received, in octets. The size must be between 2^14 and 2^24-1.
6767
/// <para>
68-
/// Value must be between 2^14 and 2^24, defaults to 2^14 (16,384)
68+
/// Value must be between 2^14 and 2^24, defaults to 2^14 (16,384).
6969
/// </para>
7070
/// </summary>
7171
public int MaxFrameSize
@@ -85,7 +85,7 @@ public int MaxFrameSize
8585
/// <summary>
8686
/// Indicates the size of the maximum allowed size of a request header field sequence. This limit applies to both name and value sequences in their compressed and uncompressed representations.
8787
/// <para>
88-
/// Value must be greater than 0, defaults to 2^14 (16,384)
88+
/// Value must be greater than 0, defaults to 2^14 (16,384).
8989
/// </para>
9090
/// </summary>
9191
public int MaxRequestHeaderFieldSize

src/Servers/Kestrel/Core/src/Http3Limits.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
1010
/// </summary>
1111
public class Http3Limits
1212
{
13+
internal const int DefaultMaxRequestHeaderFieldSize = 16 * 1024;
14+
1315
private int _headerTableSize;
14-
private int _maxRequestHeaderFieldSize = 8192;
16+
private int _maxRequestHeaderFieldSize = DefaultMaxRequestHeaderFieldSize;
1517

1618
/// <summary>
1719
/// Limits the size of the header compression table, in octets, the QPACK decoder on the server can use.
@@ -37,7 +39,7 @@ internal int HeaderTableSize
3739
/// <summary>
3840
/// Indicates the size of the maximum allowed size of a request header field sequence. This limit applies to both name and value sequences in their compressed and uncompressed representations.
3941
/// <para>
40-
/// Value must be greater than 0, defaults to 8192
42+
/// Value must be greater than 0, defaults to 2^14 (16,384).
4143
/// </para>
4244
/// </summary>
4345
public int MaxRequestHeaderFieldSize

src/Servers/Kestrel/Core/src/Internal/Http3/Http3Connection.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ internal class Http3Connection : IHttp3StreamLifetimeHandler, IRequestProcessor
3939
private int _activeRequestCount;
4040

4141
private readonly Http3PeerSettings _serverSettings = new Http3PeerSettings();
42+
private readonly Http3PeerSettings _clientSettings = new Http3PeerSettings();
4243
private readonly StreamCloseAwaitable _streamCompletionAwaitable = new StreamCloseAwaitable();
4344
private readonly IProtocolErrorCodeFeature _errorCodeFeature;
4445

@@ -53,7 +54,7 @@ public Http3Connection(HttpMultiplexedConnectionContext context)
5354
var httpLimits = context.ServiceContext.ServerOptions.Limits;
5455

5556
_serverSettings.HeaderTableSize = (uint)httpLimits.Http3.HeaderTableSize;
56-
_serverSettings.MaxRequestHeaderFieldSize = (uint)httpLimits.Http3.MaxRequestHeaderFieldSize;
57+
_serverSettings.MaxRequestHeaderFieldSectionSize = (uint)httpLimits.MaxRequestHeadersTotalSize;
5758
}
5859

5960
private void UpdateHighestStreamId(long streamId)
@@ -260,6 +261,7 @@ public async Task ProcessRequestsAsync<TContext>(IHttpApplication<TContext> appl
260261
streamContext.Transport,
261262
_streamLifetimeHandler,
262263
streamContext,
264+
_clientSettings,
263265
_serverSettings);
264266
httpConnectionContext.TimeoutControl = _context.TimeoutControl;
265267

@@ -423,6 +425,7 @@ private async ValueTask<Http3ControlStream> CreateNewUnidirectionalStreamAsync<T
423425
streamContext.Transport,
424426
_streamLifetimeHandler,
425427
streamContext,
428+
_clientSettings,
426429
_serverSettings);
427430
httpConnectionContext.TimeoutControl = _context.TimeoutControl;
428431

@@ -522,6 +525,7 @@ void IHttp3StreamLifetimeHandler.OnInboundControlStreamSetting(Http3SettingType
522525
case Http3SettingType.QPackMaxTableCapacity:
523526
break;
524527
case Http3SettingType.MaxFieldSectionSize:
528+
_clientSettings.MaxRequestHeaderFieldSectionSize = (uint)value;
525529
break;
526530
case Http3SettingType.QPackBlockedStreams:
527531
break;

src/Servers/Kestrel/Core/src/Internal/Http3/Http3ControlStream.cs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ public Http3ControlStream(Http3StreamContext context)
4242
{
4343
var httpLimits = context.ServiceContext.ServerOptions.Limits;
4444
_context = context;
45-
_serverPeerSettings = context.ServerSettings;
45+
_serverPeerSettings = context.ServerPeerSettings;
4646
_streamIdFeature = context.ConnectionFeatures.Get<IStreamIdFeature>()!;
4747
_errorCodeFeature = context.ConnectionFeatures.Get<IProtocolErrorCodeFeature>()!;
4848
_headerType = -1;
@@ -55,7 +55,9 @@ public Http3ControlStream(Http3StreamContext context)
5555
context.ConnectionId,
5656
context.MemoryPool,
5757
context.ServiceContext.Log,
58-
_streamIdFeature);
58+
_streamIdFeature,
59+
context.ClientPeerSettings,
60+
this);
5961
}
6062

6163
private void OnStreamClosed()
@@ -295,16 +297,16 @@ private ValueTask ProcessSettingsFrameAsync(ReadOnlySequence<byte> payload)
295297

296298
while (true)
297299
{
298-
var id = VariableLengthIntegerHelper.GetInteger(payload, out var consumed, out var examinded);
300+
var id = VariableLengthIntegerHelper.GetInteger(payload, out var consumed, out _);
299301
if (id == -1)
300302
{
301303
break;
302304
}
303305

304306
payload = payload.Slice(consumed);
305307

306-
var value = VariableLengthIntegerHelper.GetInteger(payload, out consumed, out examinded);
307-
if (id == -1)
308+
var value = VariableLengthIntegerHelper.GetInteger(payload, out consumed, out _);
309+
if (value == -1)
308310
{
309311
break;
310312
}

src/Servers/Kestrel/Core/src/Internal/Http3/Http3FrameWriter.cs

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ internal class Http3FrameWriter
2323
{
2424
private readonly object _writeLock = new object();
2525

26-
private IEnumerator<KeyValuePair<string, string>>? _headersEnumerator;
26+
private readonly int _maxTotalHeaderSize;
2727
private readonly PipeWriter _outputWriter;
2828
private readonly ConnectionContext _connectionContext;
2929
private readonly ITimeoutControl _timeoutControl;
@@ -32,20 +32,22 @@ internal class Http3FrameWriter
3232
private readonly MemoryPool<byte> _memoryPool;
3333
private readonly IKestrelTrace _log;
3434
private readonly IStreamIdFeature _streamIdFeature;
35+
private readonly IHttp3Stream _http3Stream;
3536
private readonly Http3RawFrame _outgoingFrame;
3637
private readonly TimingPipeFlusher _flusher;
3738

39+
private IEnumerator<KeyValuePair<string, string>>? _headersEnumerator;
40+
private int _headersTotalSize;
3841
// TODO update max frame size
3942
private uint _maxFrameSize = 10000; //Http3PeerSettings.MinAllowedMaxFrameSize;
4043
private byte[] _headerEncodingBuffer;
41-
4244
private long _unflushedBytes;
4345
private bool _completed;
4446
private bool _aborted;
4547

4648
//private int _unflushedBytes;
4749

48-
public Http3FrameWriter(PipeWriter output, ConnectionContext connectionContext, ITimeoutControl timeoutControl, MinDataRate? minResponseDataRate, string connectionId, MemoryPool<byte> memoryPool, IKestrelTrace log, IStreamIdFeature streamIdFeature)
50+
public Http3FrameWriter(PipeWriter output, ConnectionContext connectionContext, ITimeoutControl timeoutControl, MinDataRate? minResponseDataRate, string connectionId, MemoryPool<byte> memoryPool, IKestrelTrace log, IStreamIdFeature streamIdFeature, Http3PeerSettings clientPeerSettings, IHttp3Stream http3Stream)
4951
{
5052
_outputWriter = output;
5153
_connectionContext = connectionContext;
@@ -55,9 +57,18 @@ public Http3FrameWriter(PipeWriter output, ConnectionContext connectionContext,
5557
_memoryPool = memoryPool;
5658
_log = log;
5759
_streamIdFeature = streamIdFeature;
60+
_http3Stream = http3Stream;
5861
_outgoingFrame = new Http3RawFrame();
5962
_flusher = new TimingPipeFlusher(_outputWriter, timeoutControl, log);
6063
_headerEncodingBuffer = new byte[_maxFrameSize];
64+
65+
// Note that max total header size value doesn't react to settings change during a stream.
66+
// Unlikely to be a problem in practice:
67+
// - Settings rarely change after the start of a connection.
68+
// - Response header size limits are a best-effort requirement in the spec.
69+
_maxTotalHeaderSize = clientPeerSettings.MaxRequestHeaderFieldSectionSize > int.MaxValue
70+
? int.MaxValue
71+
: (int)clientPeerSettings.MaxRequestHeaderFieldSectionSize;
6172
}
6273

6374
public void UpdateMaxFrameSize(uint maxFrameSize)
@@ -257,7 +268,7 @@ internal static int WriteHeader(Http3RawFrame frame, PipeWriter output)
257268
return totalLength;
258269
}
259270

260-
public ValueTask<FlushResult> WriteResponseTrailers(HttpResponseTrailers headers)
271+
public ValueTask<FlushResult> WriteResponseTrailersAsync(long streamId, HttpResponseTrailers headers)
261272
{
262273
lock (_writeLock)
263274
{
@@ -269,16 +280,17 @@ public ValueTask<FlushResult> WriteResponseTrailers(HttpResponseTrailers headers
269280
try
270281
{
271282
_headersEnumerator = EnumerateHeaders(headers).GetEnumerator();
283+
_headersTotalSize = 0;
272284

273285
_outgoingFrame.PrepareHeaders();
274286
var buffer = _headerEncodingBuffer.AsSpan();
275-
var done = QPackHeaderWriter.BeginEncode(_headersEnumerator, buffer, out var payloadLength);
287+
var done = QPackHeaderWriter.BeginEncode(_headersEnumerator, buffer, ref _headersTotalSize, out var payloadLength);
276288
FinishWritingHeaders(payloadLength, done);
277289
}
278-
catch (QPackEncodingException)
290+
catch (QPackEncodingException ex)
279291
{
280-
//_log.HPackEncodingError(_connectionId, streamId, hex);
281-
//_http3Stream.Abort(new ConnectionAbortedException(hex.Message, hex));
292+
_log.QPackEncodingError(_connectionId, streamId, ex);
293+
_http3Stream.Abort(new ConnectionAbortedException(ex.Message, ex), Http3ErrorCode.InternalError);
282294
}
283295

284296
return TimeFlushUnsynchronizedAsync();
@@ -324,14 +336,13 @@ internal void WriteResponseHeaders(int statusCode, IHeaderDictionary headers)
324336

325337
_outgoingFrame.PrepareHeaders();
326338
var buffer = _headerEncodingBuffer.AsSpan();
327-
var done = QPackHeaderWriter.BeginEncode(statusCode, _headersEnumerator, buffer, out var payloadLength);
339+
var done = QPackHeaderWriter.BeginEncode(statusCode, _headersEnumerator, buffer, ref _headersTotalSize, out var payloadLength);
328340
FinishWritingHeaders(payloadLength, done);
329341
}
330-
catch (QPackEncodingException hex)
342+
catch (QPackEncodingException ex)
331343
{
332-
// TODO figure out how to abort the stream here.
333-
//_http3Stream.Abort(new ConnectionAbortedException(hex.Message, hex));
334-
throw new InvalidOperationException(hex.Message, hex); // Report the error to the user if this was the first write.
344+
_http3Stream.Abort(new ConnectionAbortedException(ex.Message, ex), Http3ErrorCode.InternalError);
345+
throw new InvalidOperationException(ex.Message, ex); // Report the error to the user if this was the first write.
335346
}
336347
}
337348
}
@@ -346,12 +357,18 @@ private void FinishWritingHeaders(int payloadLength, bool done)
346357

347358
while (!done)
348359
{
349-
done = QPackHeaderWriter.Encode(_headersEnumerator!, buffer, out payloadLength);
360+
done = QPackHeaderWriter.Encode(_headersEnumerator!, buffer, ref _headersTotalSize, out payloadLength);
350361
_outgoingFrame.Length = payloadLength;
351362

352363
WriteHeaderUnsynchronized();
353364
_outputWriter.Write(buffer.Slice(0, payloadLength));
354365
}
366+
367+
// https://quicwg.org/base-drafts/draft-ietf-quic-http.html#section-4.1.1.3
368+
if (_headersTotalSize > _maxTotalHeaderSize)
369+
{
370+
throw new QPackEncodingException($"The encoded HTTP headers length exceeds the limit specified by the peer of {_maxTotalHeaderSize} bytes.");
371+
}
355372
}
356373

357374
public ValueTask CompleteAsync()

src/Servers/Kestrel/Core/src/Internal/Http3/Http3OutputProducer.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -352,7 +352,7 @@ private async ValueTask<FlushResult> ProcessDataWrites()
352352
}
353353

354354
_stream.ResponseTrailers.SetReadOnly();
355-
flushResult = await _frameWriter.WriteResponseTrailers(_stream.ResponseTrailers);
355+
flushResult = await _frameWriter.WriteResponseTrailersAsync(_stream.StreamId, _stream.ResponseTrailers);
356356
}
357357
else if (readResult.IsCompleted)
358358
{

src/Servers/Kestrel/Core/src/Internal/Http3/Http3PeerSettings.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ internal class Http3PeerSettings
1212
public const uint DefaultMaxRequestHeaderFieldSize = uint.MaxValue;
1313

1414
public uint HeaderTableSize { get; internal set; } = DefaultHeaderTableSize;
15-
public uint MaxRequestHeaderFieldSize { get; internal set; } = DefaultMaxRequestHeaderFieldSize;
15+
public uint MaxRequestHeaderFieldSectionSize { get; internal set; } = DefaultMaxRequestHeaderFieldSize;
1616

1717
// Gets the settings that are different from the protocol defaults (as opposed to the server defaults).
1818
internal List<Http3PeerSetting> GetNonProtocolDefaults()
@@ -26,9 +26,9 @@ internal List<Http3PeerSetting> GetNonProtocolDefaults()
2626
list.Add(new Http3PeerSetting(Http3SettingType.QPackMaxTableCapacity, HeaderTableSize));
2727
}
2828

29-
if (MaxRequestHeaderFieldSize != DefaultMaxRequestHeaderFieldSize)
29+
if (MaxRequestHeaderFieldSectionSize != DefaultMaxRequestHeaderFieldSize)
3030
{
31-
list.Add(new Http3PeerSetting(Http3SettingType.MaxFieldSectionSize, MaxRequestHeaderFieldSize));
31+
list.Add(new Http3PeerSetting(Http3SettingType.MaxFieldSectionSize, MaxRequestHeaderFieldSectionSize));
3232
}
3333

3434
return list;

src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,9 @@ public Http3Stream(Http3StreamContext context)
7171
context.ConnectionId,
7272
context.MemoryPool,
7373
context.ServiceContext.Log,
74-
_streamIdFeature);
74+
_streamIdFeature,
75+
context.ClientPeerSettings,
76+
this);
7577

7678
// ResponseHeaders aren't set, kind of ugly that we need to reset.
7779
Reset();
@@ -485,8 +487,16 @@ private async Task ProcessHeadersFrameAsync<TContext>(IHttpApplication<TContext>
485487
_requestHeaderParsingState = RequestHeaderParsingState.Trailers;
486488
}
487489

488-
QPackDecoder.Decode(payload, handler: this);
489-
QPackDecoder.Reset();
490+
try
491+
{
492+
QPackDecoder.Decode(payload, handler: this);
493+
QPackDecoder.Reset();
494+
}
495+
catch (QPackDecodingException ex)
496+
{
497+
Log.QPackDecodingError(ConnectionId, StreamId, ex);
498+
throw new Http3StreamErrorException(ex.Message, Http3ErrorCode.InternalError);
499+
}
490500

491501
switch (_requestHeaderParsingState)
492502
{

0 commit comments

Comments
 (0)