Skip to content

Commit d9712bb

Browse files
Add non-throwing parser path for bad request handling (#65256)
1 parent 566c4f7 commit d9712bb

9 files changed

Lines changed: 826 additions & 406 deletions

File tree

src/Servers/Kestrel/Core/src/CoreStrings.resx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,9 @@
150150
<data name="BadRequest_InvalidRequestHeadersNoCRLF" xml:space="preserve">
151151
<value>Invalid request headers: missing final CRLF in header fields.</value>
152152
</data>
153+
<data name="BadRequest_InvalidRequestHeader" xml:space="preserve">
154+
<value>Invalid request header.</value>
155+
</data>
153156
<data name="BadRequest_InvalidRequestHeader_Detail" xml:space="preserve">
154157
<value>Invalid request header: '{detail}'</value>
155158
</data>
@@ -193,6 +196,9 @@
193196
<value>Unexpected end of request content.</value>
194197
</data>
195198
<data name="BadRequest_UnrecognizedHTTPVersion" xml:space="preserve">
199+
<value>Unrecognized HTTP version.</value>
200+
</data>
201+
<data name="BadRequest_UnrecognizedHTTPVersion_Detail" xml:space="preserve">
196202
<value>Unrecognized HTTP version: '{detail}'</value>
197203
</data>
198204
<data name="FallbackToIPv4Any" xml:space="preserve">

src/Servers/Kestrel/Core/src/Internal/Http/Http1Connection.cs

Lines changed: 206 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
using Microsoft.AspNetCore.Http.Features;
1111
using Microsoft.AspNetCore.Internal;
1212
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
13+
using Microsoft.Extensions.Logging;
1314

1415
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
1516

@@ -27,6 +28,7 @@ internal partial class Http1Connection : HttpProtocol, IRequestProcessor, IHttpO
2728
private readonly HttpConnectionContext _context;
2829
private readonly IHttpParser<Http1ParsingHandler> _parser;
2930
private readonly Http1OutputProducer _http1Output;
31+
private readonly bool _showErrorDetails;
3032

3133
private volatile bool _requestTimedOut;
3234
private uint _requestCount;
@@ -52,6 +54,7 @@ public Http1Connection(HttpConnectionContext context)
5254

5355
_context = context;
5456
_parser = ServiceContext.HttpParser;
57+
_showErrorDetails = Log.IsEnabled(LogLevel.Information);
5558

5659
_http1Output = new Http1OutputProducer(
5760
_context.Transport.Output,
@@ -164,51 +167,6 @@ public void HandleReadDataRateTimeout()
164167
SendTimeoutResponse();
165168
}
166169

167-
public bool ParseRequest(ref SequenceReader<byte> reader)
168-
{
169-
switch (_requestProcessingStatus)
170-
{
171-
case RequestProcessingStatus.RequestPending:
172-
// Skip any empty lines (\r or \n) between requests.
173-
// Peek first as a minor performance optimization; it's a quick inlined check.
174-
if (reader.TryPeek(out byte b) && (b == ByteCR || b == ByteLF))
175-
{
176-
reader.AdvancePastAny(ByteCR, ByteLF);
177-
}
178-
179-
if (reader.End)
180-
{
181-
break;
182-
}
183-
184-
TimeoutControl.ResetTimeout(ServerOptions.Limits.RequestHeadersTimeout, TimeoutReason.RequestHeaders);
185-
186-
_requestProcessingStatus = RequestProcessingStatus.ParsingRequestLine;
187-
goto case RequestProcessingStatus.ParsingRequestLine;
188-
case RequestProcessingStatus.ParsingRequestLine:
189-
if (TakeStartLine(ref reader))
190-
{
191-
_requestProcessingStatus = RequestProcessingStatus.ParsingHeaders;
192-
goto case RequestProcessingStatus.ParsingHeaders;
193-
}
194-
else
195-
{
196-
break;
197-
}
198-
case RequestProcessingStatus.ParsingHeaders:
199-
if (TakeMessageHeaders(ref reader, trailers: false))
200-
{
201-
_requestProcessingStatus = RequestProcessingStatus.AppStarted;
202-
// Consumed preamble
203-
return true;
204-
}
205-
break;
206-
}
207-
208-
// Haven't completed consuming preamble
209-
return false;
210-
}
211-
212170
public bool TakeStartLine(ref SequenceReader<byte> reader)
213171
{
214172
// Make sure the buffer is limited
@@ -287,6 +245,130 @@ bool TrimAndTakeMessageHeaders(ref SequenceReader<byte> reader, bool trailers)
287245
}
288246
}
289247

248+
/// <summary>
249+
/// Non-throwing version of ParseRequest. Returns HttpParseResult instead of throwing.
250+
/// </summary>
251+
private HttpParseResult TryParseRequestCore(ref SequenceReader<byte> reader)
252+
{
253+
switch (_requestProcessingStatus)
254+
{
255+
case RequestProcessingStatus.RequestPending:
256+
// Skip any empty lines (\r or \n) between requests.
257+
// Peek first as a minor performance optimization; it's a quick inlined check.
258+
if (reader.TryPeek(out byte b) && (b == ByteCR || b == ByteLF))
259+
{
260+
reader.AdvancePastAny(ByteCR, ByteLF);
261+
}
262+
263+
if (reader.End)
264+
{
265+
return HttpParseResult.Incomplete;
266+
}
267+
268+
TimeoutControl.ResetTimeout(ServerOptions.Limits.RequestHeadersTimeout, TimeoutReason.RequestHeaders);
269+
_requestProcessingStatus = RequestProcessingStatus.ParsingRequestLine;
270+
goto case RequestProcessingStatus.ParsingRequestLine;
271+
272+
case RequestProcessingStatus.ParsingRequestLine:
273+
var startLineResult = TryTakeStartLineCore(ref reader);
274+
if (startLineResult.HasError)
275+
{
276+
return startLineResult;
277+
}
278+
if (startLineResult.IsComplete)
279+
{
280+
_requestProcessingStatus = RequestProcessingStatus.ParsingHeaders;
281+
goto case RequestProcessingStatus.ParsingHeaders;
282+
}
283+
return HttpParseResult.Incomplete;
284+
285+
case RequestProcessingStatus.ParsingHeaders:
286+
var headersResult = TryTakeMessageHeadersCore(ref reader, trailers: false);
287+
if (headersResult.HasError)
288+
{
289+
return headersResult;
290+
}
291+
if (headersResult.IsComplete)
292+
{
293+
_requestProcessingStatus = RequestProcessingStatus.AppStarted;
294+
return HttpParseResult.Complete;
295+
}
296+
return HttpParseResult.Incomplete;
297+
}
298+
299+
return HttpParseResult.Incomplete;
300+
}
301+
302+
private HttpParseResult TryTakeStartLineCore(ref SequenceReader<byte> reader)
303+
{
304+
if (reader.Remaining >= ServerOptions.Limits.MaxRequestLineSize)
305+
{
306+
return TryTrimAndTakeStartLineCore(ref reader);
307+
}
308+
309+
return _parser.TryParseRequestLine(new Http1ParsingHandler(this), ref reader);
310+
311+
HttpParseResult TryTrimAndTakeStartLineCore(ref SequenceReader<byte> reader)
312+
{
313+
var trimmedBuffer = reader.Sequence.Slice(reader.Position, ServerOptions.Limits.MaxRequestLineSize);
314+
var trimmedReader = new SequenceReader<byte>(trimmedBuffer);
315+
316+
var result = _parser.TryParseRequestLine(new Http1ParsingHandler(this), ref trimmedReader);
317+
if (result.HasError)
318+
{
319+
return result;
320+
}
321+
if (!result.IsComplete)
322+
{
323+
return HttpParseResult.Error(RequestRejectionReason.RequestLineTooLong);
324+
}
325+
326+
reader.Advance(trimmedReader.Consumed);
327+
return HttpParseResult.Complete;
328+
}
329+
}
330+
331+
private HttpParseResult TryTakeMessageHeadersCore(ref SequenceReader<byte> reader, bool trailers)
332+
{
333+
if (reader.Remaining > _remainingRequestHeadersBytesAllowed)
334+
{
335+
return TryTrimAndTakeMessageHeadersCore(ref reader, trailers);
336+
}
337+
338+
var alreadyConsumed = reader.Consumed;
339+
var result = _parser.TryParseHeaders(new Http1ParsingHandler(this, trailers), ref reader);
340+
_remainingRequestHeadersBytesAllowed -= reader.Consumed - alreadyConsumed;
341+
342+
if (result.IsComplete)
343+
{
344+
TimeoutControl.CancelTimeout();
345+
}
346+
347+
return result;
348+
349+
HttpParseResult TryTrimAndTakeMessageHeadersCore(ref SequenceReader<byte> reader, bool trailers)
350+
{
351+
var trimmedBuffer = reader.Sequence.Slice(reader.Position, _remainingRequestHeadersBytesAllowed);
352+
var trimmedReader = new SequenceReader<byte>(trimmedBuffer);
353+
354+
var result = _parser.TryParseHeaders(new Http1ParsingHandler(this, trailers), ref trimmedReader);
355+
_remainingRequestHeadersBytesAllowed -= trimmedReader.Consumed;
356+
357+
if (result.HasError)
358+
{
359+
return result;
360+
}
361+
if (!result.IsComplete)
362+
{
363+
return HttpParseResult.Error(RequestRejectionReason.HeadersExceedMaxTotalSize);
364+
}
365+
366+
TimeoutControl.CancelTimeout();
367+
reader.Advance(trimmedReader.Consumed);
368+
return HttpParseResult.Complete;
369+
}
370+
}
371+
290372
public void OnStartLine(HttpVersionAndMethod versionAndMethod, TargetOffsetPathLength targetPath, Span<byte> startLine)
291373
{
292374
// Null characters are not allowed and should have been checked by HttpParser before calling this method
@@ -722,29 +804,40 @@ protected override bool TryParseRequest(ReadResult result, out bool endConnectio
722804
{
723805
var reader = new SequenceReader<byte>(result.Buffer);
724806
var isConsumed = false;
807+
808+
// The parser itself doesn't throw, but handler callbacks (OnStartLine, OnHeader)
809+
// can still throw BadHttpRequestException for semantic errors (e.g., invalid request target).
810+
// We catch those here to ensure metrics are recorded via OnBadRequest.
811+
HttpParseResult parseResult;
725812
try
726813
{
727-
isConsumed = ParseRequest(ref reader);
728-
}
729-
catch (InvalidOperationException) when (_requestProcessingStatus == RequestProcessingStatus.ParsingHeaders)
730-
{
731-
KestrelBadHttpRequestException.Throw(RequestRejectionReason.MalformedRequestInvalidHeaders);
732-
throw;
814+
parseResult = TryParseRequestCore(ref reader);
815+
816+
// Handle parse errors without exceptions
817+
if (parseResult.HasError)
818+
{
819+
// Create exception and call HandleBadRequest BEFORE finally runs AdvanceTo,
820+
// as that may invalidate the buffer
821+
var ex = CreateBadRequestException(parseResult, result.Buffer);
822+
HandleBadRequest(result.Buffer, ex);
823+
endConnection = true;
824+
return true;
825+
}
826+
827+
isConsumed = parseResult.IsComplete;
733828
}
734829
#pragma warning disable CS0618 // Type or member is obsolete
735830
catch (BadHttpRequestException ex)
736831
{
737-
OnBadRequest(result.Buffer, ex);
738-
739-
// Avoid re-throwing - handle the bad request state directly here
740-
// to eliminate exception propagation overhead through async state machines
741-
SetBadRequestState(ex);
832+
// HandleBadRequest must be called before finally runs AdvanceTo
833+
HandleBadRequest(result.Buffer, ex);
742834
endConnection = true;
743835
return true;
744836
}
745837
#pragma warning restore CS0618 // Type or member is obsolete
746838
catch (Exception)
747839
{
840+
// Record OtherError for unexpected exceptions that escape the parser
748841
KestrelMetrics.AddConnectionEndReason(MetricsContext, ConnectionEndReason.OtherError);
749842
throw;
750843
}
@@ -761,11 +854,13 @@ protected override bool TryParseRequest(ReadResult result, out bool endConnectio
761854
endConnection = true;
762855
return true;
763856
case RequestProcessingStatus.ParsingRequestLine:
764-
KestrelBadHttpRequestException.Throw(RequestRejectionReason.InvalidRequestLine);
765-
break;
857+
HandleBadRequest(result.Buffer, RequestRejectionReason.InvalidRequestLine);
858+
endConnection = true;
859+
return true;
766860
case RequestProcessingStatus.ParsingHeaders:
767-
KestrelBadHttpRequestException.Throw(RequestRejectionReason.MalformedRequestInvalidHeaders);
768-
break;
861+
HandleBadRequest(result.Buffer, RequestRejectionReason.MalformedRequestInvalidHeaders);
862+
endConnection = true;
863+
return true;
769864
}
770865
}
771866
else if (!_keepAlive && _requestProcessingStatus == RequestProcessingStatus.RequestPending)
@@ -779,7 +874,9 @@ protected override bool TryParseRequest(ReadResult result, out bool endConnectio
779874
{
780875
// In this case, there is an ongoing request but the start line/header parsing has timed out, so send
781876
// a 408 response.
782-
KestrelBadHttpRequestException.Throw(RequestRejectionReason.RequestHeadersTimeout);
877+
HandleBadRequest(result.Buffer, RequestRejectionReason.RequestHeadersTimeout);
878+
endConnection = true;
879+
return true;
783880
}
784881

785882
endConnection = false;
@@ -836,6 +933,54 @@ internal static ConnectionEndReason GetConnectionEndReason(Microsoft.AspNetCore.
836933
}
837934
}
838935

936+
#pragma warning disable CS0618 // Type or member is obsolete
937+
/// <summary>
938+
/// Helper method to handle bad request: records metrics, sets bad request state, and ends the connection.
939+
/// </summary>
940+
private void HandleBadRequest(ReadOnlySequence<byte> requestData, BadHttpRequestException ex)
941+
{
942+
OnBadRequest(requestData, ex);
943+
SetBadRequestState(ex);
944+
}
945+
946+
/// <summary>
947+
/// Helper method to handle bad request by rejection reason: creates exception, records metrics, sets bad request state.
948+
/// </summary>
949+
private void HandleBadRequest(ReadOnlySequence<byte> requestData, RequestRejectionReason reason)
950+
{
951+
var ex = KestrelBadHttpRequestException.GetException(reason);
952+
HandleBadRequest(requestData, ex);
953+
}
954+
#pragma warning restore CS0618 // Type or member is obsolete
955+
956+
#pragma warning disable CS0618 // Type or member is obsolete
957+
/// <summary>
958+
/// Creates a BadHttpRequestException with error detail extracted from the buffer.
959+
/// </summary>
960+
private BadHttpRequestException CreateBadRequestException(HttpParseResult parseResult, ReadOnlySequence<byte> buffer)
961+
{
962+
// Some error reasons don't use detail
963+
if (parseResult.ErrorReason == RequestRejectionReason.InvalidRequestHeadersNoCRLF)
964+
{
965+
return KestrelBadHttpRequestException.GetException(parseResult.ErrorReason);
966+
}
967+
968+
// Extract error detail from buffer if available, but only when _showErrorDetails is enabled
969+
// to avoid leaking internal details in production.
970+
if (_showErrorDetails &&
971+
parseResult.ErrorLength > 0 &&
972+
parseResult.ErrorOffset + parseResult.ErrorLength <= buffer.Length)
973+
{
974+
var errorSlice = buffer.Slice(parseResult.ErrorOffset, parseResult.ErrorLength);
975+
var errorBytes = errorSlice.IsSingleSegment ? errorSlice.FirstSpan : errorSlice.ToArray();
976+
var detail = errorBytes.GetAsciiStringEscaped(Constants.MaxExceptionDetailSize);
977+
return KestrelBadHttpRequestException.GetException(parseResult.ErrorReason, detail);
978+
}
979+
980+
return KestrelBadHttpRequestException.GetException(parseResult.ErrorReason);
981+
}
982+
#pragma warning restore CS0618 // Type or member is obsolete
983+
839984
#pragma warning disable CS0618 // Type or member is obsolete
840985
private void OnBadRequest(ReadOnlySequence<byte> requestData, BadHttpRequestException ex)
841986
#pragma warning restore CS0618 // Type or member is obsolete

0 commit comments

Comments
 (0)