1010using Microsoft . AspNetCore . Http . Features ;
1111using Microsoft . AspNetCore . Internal ;
1212using Microsoft . AspNetCore . Server . Kestrel . Core . Internal . Infrastructure ;
13+ using Microsoft . Extensions . Logging ;
1314
1415namespace 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