-
Notifications
You must be signed in to change notification settings - Fork 10.6k
Add non-throwing parser path for bad request handling #65256
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
a099102
9b33013
4aa24e4
f76d662
e081175
42185f8
f988567
ededf0b
7adb422
d82f6f0
447e840
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,6 +10,7 @@ | |
| using Microsoft.AspNetCore.Http.Features; | ||
| using Microsoft.AspNetCore.Internal; | ||
| using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; | ||
| using Microsoft.Extensions.Logging; | ||
|
|
||
| namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; | ||
|
|
||
|
|
@@ -287,6 +288,128 @@ bool TrimAndTakeMessageHeaders(ref SequenceReader<byte> reader, bool trailers) | |
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Non-throwing version of ParseRequest. Returns HttpParseResult instead of throwing. | ||
| /// </summary> | ||
| private HttpParseResult TryParseRequestNoThrow(ref SequenceReader<byte> reader) | ||
| { | ||
| switch (_requestProcessingStatus) | ||
| { | ||
| case RequestProcessingStatus.RequestPending: | ||
| if (reader.TryPeek(out byte b) && (b == ByteCR || b == ByteLF)) | ||
| { | ||
| reader.AdvancePastAny(ByteCR, ByteLF); | ||
| } | ||
|
|
||
| if (reader.End) | ||
| { | ||
| return HttpParseResult.Incomplete; | ||
| } | ||
|
|
||
| TimeoutControl.ResetTimeout(ServerOptions.Limits.RequestHeadersTimeout, TimeoutReason.RequestHeaders); | ||
| _requestProcessingStatus = RequestProcessingStatus.ParsingRequestLine; | ||
| goto case RequestProcessingStatus.ParsingRequestLine; | ||
|
|
||
| case RequestProcessingStatus.ParsingRequestLine: | ||
| var startLineResult = TryTakeStartLineNoThrow(ref reader); | ||
| if (startLineResult.HasError) | ||
| { | ||
| return startLineResult; | ||
| } | ||
| if (startLineResult.IsComplete) | ||
| { | ||
| _requestProcessingStatus = RequestProcessingStatus.ParsingHeaders; | ||
| goto case RequestProcessingStatus.ParsingHeaders; | ||
| } | ||
| return HttpParseResult.Incomplete; | ||
|
|
||
| case RequestProcessingStatus.ParsingHeaders: | ||
| var headersResult = TryTakeMessageHeadersNoThrow(ref reader, trailers: false); | ||
| if (headersResult.HasError) | ||
| { | ||
| return headersResult; | ||
| } | ||
| if (headersResult.IsComplete) | ||
| { | ||
| _requestProcessingStatus = RequestProcessingStatus.AppStarted; | ||
| return HttpParseResult.Complete; | ||
| } | ||
| return HttpParseResult.Incomplete; | ||
| } | ||
|
|
||
| return HttpParseResult.Incomplete; | ||
| } | ||
|
|
||
| private HttpParseResult TryTakeStartLineNoThrow(ref SequenceReader<byte> reader) | ||
| { | ||
| if (reader.Remaining >= ServerOptions.Limits.MaxRequestLineSize) | ||
| { | ||
| return TryTrimAndTakeStartLineNoThrow(ref reader); | ||
| } | ||
|
|
||
| return ((HttpParser<Http1ParsingHandler>)_parser).TryParseRequestLine(new Http1ParsingHandler(this), ref reader); | ||
|
|
||
| HttpParseResult TryTrimAndTakeStartLineNoThrow(ref SequenceReader<byte> reader) | ||
| { | ||
| var trimmedBuffer = reader.Sequence.Slice(reader.Position, ServerOptions.Limits.MaxRequestLineSize); | ||
| var trimmedReader = new SequenceReader<byte>(trimmedBuffer); | ||
|
|
||
| var result = ((HttpParser<Http1ParsingHandler>)_parser).TryParseRequestLine(new Http1ParsingHandler(this), ref trimmedReader); | ||
adityamandaleeka marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| if (result.HasError) | ||
| { | ||
| return result; | ||
| } | ||
| if (!result.IsComplete) | ||
| { | ||
| return HttpParseResult.Error(RequestRejectionReason.RequestLineTooLong); | ||
| } | ||
|
|
||
| reader.Advance(trimmedReader.Consumed); | ||
| return HttpParseResult.Complete; | ||
| } | ||
| } | ||
|
|
||
| private HttpParseResult TryTakeMessageHeadersNoThrow(ref SequenceReader<byte> reader, bool trailers) | ||
| { | ||
| if (reader.Remaining > _remainingRequestHeadersBytesAllowed) | ||
| { | ||
| return TryTrimAndTakeMessageHeadersNoThrow(ref reader, trailers); | ||
| } | ||
|
|
||
| var alreadyConsumed = reader.Consumed; | ||
| var result = ((HttpParser<Http1ParsingHandler>)_parser).TryParseHeaders(new Http1ParsingHandler(this, trailers), ref reader); | ||
| _remainingRequestHeadersBytesAllowed -= reader.Consumed - alreadyConsumed; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Previous code put this in a finally for some reason. I think all cases where
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yup. When an error occurs, |
||
|
|
||
| if (result.IsComplete) | ||
| { | ||
| TimeoutControl.CancelTimeout(); | ||
| } | ||
|
|
||
| return result; | ||
|
|
||
| HttpParseResult TryTrimAndTakeMessageHeadersNoThrow(ref SequenceReader<byte> reader, bool trailers) | ||
| { | ||
| var trimmedBuffer = reader.Sequence.Slice(reader.Position, _remainingRequestHeadersBytesAllowed); | ||
| var trimmedReader = new SequenceReader<byte>(trimmedBuffer); | ||
|
|
||
| var result = ((HttpParser<Http1ParsingHandler>)_parser).TryParseHeaders(new Http1ParsingHandler(this, trailers), ref trimmedReader); | ||
| _remainingRequestHeadersBytesAllowed -= trimmedReader.Consumed; | ||
|
|
||
| if (result.HasError) | ||
| { | ||
| return result; | ||
| } | ||
| if (!result.IsComplete) | ||
| { | ||
| return HttpParseResult.Error(RequestRejectionReason.HeadersExceedMaxTotalSize); | ||
| } | ||
|
|
||
| TimeoutControl.CancelTimeout(); | ||
| reader.Advance(trimmedReader.Consumed); | ||
| return HttpParseResult.Complete; | ||
| } | ||
| } | ||
|
|
||
| public void OnStartLine(HttpVersionAndMethod versionAndMethod, TargetOffsetPathLength targetPath, Span<byte> startLine) | ||
| { | ||
| // Null characters are not allowed and should have been checked by HttpParser before calling this method | ||
|
|
@@ -721,37 +844,26 @@ protected override bool BeginRead(out ValueTask<ReadResult> awaitable) | |
| protected override bool TryParseRequest(ReadResult result, out bool endConnection) | ||
| { | ||
| var reader = new SequenceReader<byte>(result.Buffer); | ||
| var isConsumed = false; | ||
| try | ||
| { | ||
| isConsumed = ParseRequest(ref reader); | ||
| } | ||
| catch (InvalidOperationException) when (_requestProcessingStatus == RequestProcessingStatus.ParsingHeaders) | ||
| { | ||
| KestrelBadHttpRequestException.Throw(RequestRejectionReason.MalformedRequestInvalidHeaders); | ||
| throw; | ||
| } | ||
| #pragma warning disable CS0618 // Type or member is obsolete | ||
| catch (BadHttpRequestException ex) | ||
|
|
||
| // Use non-throwing parser path for performance | ||
| var parseResult = TryParseRequestNoThrow(ref reader); | ||
| var isConsumed = parseResult.IsComplete; | ||
|
|
||
| // Handle parse errors without exceptions | ||
| if (parseResult.HasError) | ||
| { | ||
| // Create exception and call OnBadRequest BEFORE AdvanceTo, as that may invalidate the buffer | ||
| var ex = CreateBadRequestException(parseResult, result.Buffer); | ||
| OnBadRequest(result.Buffer, ex); | ||
|
|
||
| // Avoid re-throwing - handle the bad request state directly here | ||
| // to eliminate exception propagation overhead through async state machines | ||
| Input.AdvanceTo(reader.Position, result.Buffer.End); | ||
|
|
||
| SetBadRequestState(ex); | ||
| endConnection = true; | ||
| return true; | ||
| } | ||
| #pragma warning restore CS0618 // Type or member is obsolete | ||
| catch (Exception) | ||
adityamandaleeka marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| { | ||
| KestrelMetrics.AddConnectionEndReason(MetricsContext, ConnectionEndReason.OtherError); | ||
| throw; | ||
| } | ||
| finally | ||
| { | ||
| Input.AdvanceTo(reader.Position, isConsumed ? reader.Position : result.Buffer.End); | ||
| } | ||
|
|
||
| Input.AdvanceTo(reader.Position, isConsumed ? reader.Position : result.Buffer.End); | ||
|
|
||
| if (result.IsCompleted) | ||
| { | ||
|
|
@@ -761,25 +873,35 @@ protected override bool TryParseRequest(ReadResult result, out bool endConnectio | |
| endConnection = true; | ||
| return true; | ||
| case RequestProcessingStatus.ParsingRequestLine: | ||
| KestrelBadHttpRequestException.Throw(RequestRejectionReason.InvalidRequestLine); | ||
| break; | ||
| { | ||
| var ex = KestrelBadHttpRequestException.GetException(RequestRejectionReason.InvalidRequestLine); | ||
adityamandaleeka marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| OnBadRequest(result.Buffer, ex); | ||
| SetBadRequestState(ex); | ||
| endConnection = true; | ||
| return true; | ||
| } | ||
| case RequestProcessingStatus.ParsingHeaders: | ||
| KestrelBadHttpRequestException.Throw(RequestRejectionReason.MalformedRequestInvalidHeaders); | ||
| break; | ||
| { | ||
| var ex = KestrelBadHttpRequestException.GetException(RequestRejectionReason.MalformedRequestInvalidHeaders); | ||
| OnBadRequest(result.Buffer, ex); | ||
| SetBadRequestState(ex); | ||
| endConnection = true; | ||
| return true; | ||
| } | ||
| } | ||
| } | ||
| else if (!_keepAlive && _requestProcessingStatus == RequestProcessingStatus.RequestPending) | ||
| { | ||
| // Stop the request processing loop if the server is shutting down or there was a keep-alive timeout | ||
| // and there is no ongoing request. | ||
adityamandaleeka marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| endConnection = true; | ||
| return true; | ||
| } | ||
| else if (RequestTimedOut) | ||
| { | ||
| // In this case, there is an ongoing request but the start line/header parsing has timed out, so send | ||
adityamandaleeka marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| // a 408 response. | ||
| KestrelBadHttpRequestException.Throw(RequestRejectionReason.RequestHeadersTimeout); | ||
| var ex = KestrelBadHttpRequestException.GetException(RequestRejectionReason.RequestHeadersTimeout); | ||
adityamandaleeka marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| OnBadRequest(result.Buffer, ex); | ||
| SetBadRequestState(ex); | ||
| endConnection = true; | ||
| return true; | ||
| } | ||
|
|
||
| endConnection = false; | ||
|
|
@@ -836,6 +958,34 @@ internal static ConnectionEndReason GetConnectionEndReason(Microsoft.AspNetCore. | |
| } | ||
| } | ||
|
|
||
| #pragma warning disable CS0618 // Type or member is obsolete | ||
adityamandaleeka marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| /// <summary> | ||
| /// Creates a BadHttpRequestException with error detail extracted from the buffer. | ||
| /// </summary> | ||
| private BadHttpRequestException CreateBadRequestException(HttpParseResult parseResult, ReadOnlySequence<byte> buffer) | ||
| { | ||
| // Some error reasons don't use detail | ||
| if (parseResult.ErrorReason == RequestRejectionReason.InvalidRequestHeadersNoCRLF) | ||
| { | ||
| return KestrelBadHttpRequestException.GetException(parseResult.ErrorReason); | ||
adityamandaleeka marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| // Extract error detail from buffer if available, but only when logging is enabled | ||
| // to avoid leaking internal details in production. | ||
| if (Log.IsEnabled(LogLevel.Information) && | ||
|
||
| parseResult.ErrorLength > 0 && | ||
| parseResult.ErrorOffset + parseResult.ErrorLength <= buffer.Length) | ||
| { | ||
| var errorSlice = buffer.Slice(parseResult.ErrorOffset, parseResult.ErrorLength); | ||
| var errorBytes = errorSlice.IsSingleSegment ? errorSlice.FirstSpan : errorSlice.ToArray(); | ||
| var detail = errorBytes.GetAsciiStringEscaped(Constants.MaxExceptionDetailSize); | ||
| return KestrelBadHttpRequestException.GetException(parseResult.ErrorReason, detail); | ||
| } | ||
|
|
||
| return KestrelBadHttpRequestException.GetException(parseResult.ErrorReason); | ||
| } | ||
| #pragma warning restore CS0618 // Type or member is obsolete | ||
|
|
||
| #pragma warning disable CS0618 // Type or member is obsolete | ||
| private void OnBadRequest(ReadOnlySequence<byte> requestData, BadHttpRequestException ex) | ||
adityamandaleeka marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| #pragma warning restore CS0618 // Type or member is obsolete | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.