@@ -15,6 +15,13 @@ public sealed class Http10Decoder
1515
1616 private ReadOnlyMemory < byte > _remainder = ReadOnlyMemory < byte > . Empty ;
1717 private bool _isHttp09 ;
18+ private int ? _pendingContentLength ; // Non-null if waiting for body data due to Content-Length
19+
20+ /// <summary>
21+ /// Returns true if the decoder is waiting for body data due to a Content-Length header.
22+ /// This is used to detect Content-Length mismatches when the connection abruptly closes.
23+ /// </summary>
24+ public bool IsWaitingForContentLength => _pendingContentLength . HasValue ;
1825
1926 public Http10Decoder ( int maxHeaderSize = DefaultMaxHeaderSize , int maxTotalHeaderSize = DefaultMaxTotalHeaderSize )
2027 {
@@ -86,6 +93,7 @@ public bool TryDecode(ReadOnlyMemory<byte> incomingData, out HttpResponseMessage
8693 if ( statusCode is 204 or 304 )
8794 {
8895 response = BuildResponse ( lines [ 0 ] , headers , [ ] ) ;
96+ _pendingContentLength = null ;
8997 return true ;
9098 }
9199
@@ -95,14 +103,17 @@ public bool TryDecode(ReadOnlyMemory<byte> incomingData, out HttpResponseMessage
95103 if ( bodyData . Length < contentLength . Value )
96104 {
97105 _remainder = working ;
106+ _pendingContentLength = contentLength . Value ;
98107 return false ;
99108 }
100109
101110 response = BuildResponse ( lines [ 0 ] , headers , bodyData . Span [ ..contentLength . Value ] . ToArray ( ) ) ;
111+ _pendingContentLength = null ;
102112 return true ;
103113 }
104114
105115 response = BuildResponse ( lines [ 0 ] , headers , bodyData . ToArray ( ) ) ;
116+ _pendingContentLength = null ;
106117 return true ;
107118 }
108119
@@ -116,6 +127,7 @@ public bool TryDecodeEof(out HttpResponseMessage? response)
116127 {
117128 response = BuildHttp09Response ( [ ] ) ;
118129 _isHttp09 = false ;
130+ _pendingContentLength = null ;
119131 return true ;
120132 }
121133
@@ -128,6 +140,7 @@ public bool TryDecodeEof(out HttpResponseMessage? response)
128140 response = BuildHttp09Response ( _remainder . ToArray ( ) ) ;
129141 _remainder = ReadOnlyMemory < byte > . Empty ;
130142 _isHttp09 = false ;
143+ _pendingContentLength = null ;
131144 return true ;
132145 }
133146
@@ -150,8 +163,20 @@ public bool TryDecodeEof(out HttpResponseMessage? response)
150163 var index = headerEnd + GetHeaderDelimiterLength ( span , headerEnd ) ;
151164 var body = _remainder [ index ..] . ToArray ( ) ;
152165
166+ // RFC 1945: If a Content-Length was declared but EOF arrived after receiving partial
167+ // body data, it's a truncation error. When body is empty, allow it — connection
168+ // may have been closed cleanly after headers (e.g. HEAD response, 204, or abrupt
169+ // close already detected via CloseSignalItem.AbruptClose before reaching here).
170+ var contentLength = GetContentLength ( headers ) ;
171+ if ( contentLength . HasValue && body . Length > 0 && body . Length < contentLength . Value )
172+ {
173+ throw new HttpDecoderException ( HttpDecoderError . InvalidContentLength ,
174+ $ "Content-Length mismatch: expected { contentLength . Value } bytes but received { body . Length } bytes before EOF.") ;
175+ }
176+
153177 response = BuildResponse ( lines [ 0 ] , headers , body ) ;
154178 _remainder = ReadOnlyMemory < byte > . Empty ;
179+ _pendingContentLength = null ;
155180 return true ;
156181 }
157182
@@ -196,13 +221,15 @@ public bool TryDecodeConnect(ReadOnlyMemory<byte> incomingData, out HttpResponse
196221 if ( statusCode is >= 200 and < 300 )
197222 {
198223 response = BuildResponse ( lines [ 0 ] , headers , [ ] ) ;
224+ _pendingContentLength = null ;
199225 return true ;
200226 }
201227
202228 // Non-2xx: normal body handling (same as TryDecode)
203229 if ( statusCode is 204 or 304 )
204230 {
205231 response = BuildResponse ( lines [ 0 ] , headers , [ ] ) ;
232+ _pendingContentLength = null ;
206233 return true ;
207234 }
208235
@@ -212,21 +239,25 @@ public bool TryDecodeConnect(ReadOnlyMemory<byte> incomingData, out HttpResponse
212239 if ( bodyData . Length < contentLength . Value )
213240 {
214241 _remainder = working ;
242+ _pendingContentLength = contentLength . Value ;
215243 return false ;
216244 }
217245
218246 response = BuildResponse ( lines [ 0 ] , headers , bodyData . Span [ ..contentLength . Value ] . ToArray ( ) ) ;
247+ _pendingContentLength = null ;
219248 return true ;
220249 }
221250
222251 response = BuildResponse ( lines [ 0 ] , headers , bodyData . ToArray ( ) ) ;
252+ _pendingContentLength = null ;
223253 return true ;
224254 }
225255
226256 public void Reset ( )
227257 {
228258 _remainder = ReadOnlyMemory < byte > . Empty ;
229259 _isHttp09 = false ;
260+ _pendingContentLength = null ;
230261 }
231262
232263 private static void ValidateStatusLine ( string statusLine )
0 commit comments