@@ -67,12 +67,6 @@ async fn max_middleware_body_bytes() -> usize {
6767 chain[ 0 ] . max_payload_bytes ( )
6868}
6969const RELAY_BUF_SIZE : usize = 8192 ;
70- /// Maximum aggregate chunk framing relayed for one body. Framing includes
71- /// chunk-size lines, the CRLF after each chunk payload, and trailers. Unknown
72- /// length framing lines must be read to their boundary one byte at a time so a
73- /// pipelined request is not consumed; bounding their aggregate size also bounds
74- /// the resulting read/write amplification for tiny chunks.
75- const MAX_CHUNKED_FRAMING_BYTES : usize = 32 * 1024 ;
7670const RESPONSE_UNIT_COALESCE_TIMEOUT : std:: time:: Duration = std:: time:: Duration :: from_millis ( 2 ) ;
7771const HTTP_METHOD_PREFIXES : & [ & [ u8 ] ] = & [
7872 b"GET " ,
@@ -3134,10 +3128,10 @@ where
31343128 let started_at = std:: time:: Instant :: now ( ) ;
31353129 let mut read_buf = [ 0u8 ; RELAY_BUF_SIZE ] ;
31363130 let mut parse_buf = Vec :: from ( already_forwarded) ;
3131+ let mut forwarded_len = already_forwarded. len ( ) ;
31373132 let mut pos = 0usize ;
31383133 let mut chunk_count = 0usize ;
31393134 let mut chunk_payload_bytes = 0usize ;
3140- let mut framing_bytes = 0usize ;
31413135
31423136 // Parse chunk-size lines + chunk payloads until final 0-size chunk, then
31433137 // parse trailers until the terminating empty trailer line.
@@ -3147,17 +3141,15 @@ where
31473141 if let Some ( end) = find_crlf ( & parse_buf, pos) {
31483142 break end;
31493143 }
3150- ensure_chunked_framing_capacity (
3151- framing_bytes,
3152- parse_buf. len ( ) . saturating_sub ( pos) . saturating_add ( 1 ) ,
3153- ) ?;
3144+ if parse_buf. len ( ) . saturating_sub ( pos) >= MAX_HEADER_BYTES {
3145+ return Err ( miette ! ( "Chunk-size line exceeds limit" ) ) ;
3146+ }
31543147 let target_len = parse_buf
31553148 . len ( )
31563149 . checked_add ( 1 )
31573150 . ok_or_else ( || miette ! ( "Chunked body size overflow" ) ) ?;
31583151 relay_chunked_until_len (
31593152 reader,
3160- writer,
31613153 & mut read_buf,
31623154 & mut parse_buf,
31633155 target_len,
@@ -3166,7 +3158,9 @@ where
31663158 )
31673159 . await ?;
31683160 } ;
3169- add_chunked_framing_bytes ( & mut framing_bytes, size_line_end + 2 - pos) ?;
3161+ if size_line_end. saturating_add ( 2 ) . saturating_sub ( pos) > MAX_HEADER_BYTES {
3162+ return Err ( miette ! ( "Chunk-size line exceeds limit" ) ) ;
3163+ }
31703164
31713165 let size_line = std:: str:: from_utf8 ( & parse_buf[ pos..size_line_end] )
31723166 . into_diagnostic ( )
@@ -3180,6 +3174,15 @@ where
31803174 . into_diagnostic ( )
31813175 . map_err ( |_| miette ! ( "Invalid chunk size token: {size_token:?}" ) ) ?;
31823176 pos = size_line_end + 2 ;
3177+ flush_validated_chunked_bytes (
3178+ writer,
3179+ & parse_buf,
3180+ & mut forwarded_len,
3181+ pos,
3182+ false ,
3183+ generation_guard,
3184+ )
3185+ . await ?;
31833186
31843187 if chunk_size == 0 {
31853188 // Parse trailers (if any). Terminates on empty trailer line.
@@ -3189,17 +3192,15 @@ where
31893192 if let Some ( end) = find_crlf ( & parse_buf, pos) {
31903193 break end;
31913194 }
3192- ensure_chunked_framing_capacity (
3193- framing_bytes,
3194- parse_buf. len ( ) . saturating_sub ( pos) . saturating_add ( 1 ) ,
3195- ) ?;
3195+ if parse_buf. len ( ) . saturating_sub ( pos) >= MAX_HEADER_BYTES {
3196+ return Err ( miette ! ( "Chunk trailer line exceeds limit" ) ) ;
3197+ }
31963198 let target_len = parse_buf
31973199 . len ( )
31983200 . checked_add ( 1 )
31993201 . ok_or_else ( || miette ! ( "Chunked trailer size overflow" ) ) ?;
32003202 relay_chunked_until_len (
32013203 reader,
3202- writer,
32033204 & mut read_buf,
32043205 & mut parse_buf,
32053206 target_len,
@@ -3208,11 +3209,23 @@ where
32083209 )
32093210 . await ?;
32103211 } ;
3212+ if trailer_end. saturating_add ( 2 ) . saturating_sub ( pos) > MAX_HEADER_BYTES {
3213+ return Err ( miette ! ( "Chunk trailer line exceeds limit" ) ) ;
3214+ }
32113215
32123216 let trailer_line = & parse_buf[ pos..trailer_end] ;
3213- add_chunked_framing_bytes ( & mut framing_bytes , trailer_end + 2 - pos ) ? ;
3217+ let trailer_is_empty = trailer_line . is_empty ( ) ;
32143218 pos = trailer_end + 2 ;
3215- if trailer_line. is_empty ( ) {
3219+ flush_validated_chunked_bytes (
3220+ writer,
3221+ & parse_buf,
3222+ & mut forwarded_len,
3223+ pos,
3224+ trailer_is_empty,
3225+ generation_guard,
3226+ )
3227+ . await ?;
3228+ if trailer_is_empty {
32163229 debug ! (
32173230 chunk_count,
32183231 chunk_payload_bytes,
@@ -3223,6 +3236,11 @@ where
32233236 return Ok ( ( ) ) ;
32243237 }
32253238 trailer_count += 1 ;
3239+ if pos > RELAY_BUF_SIZE * 4 && forwarded_len >= pos {
3240+ parse_buf. drain ( ..pos) ;
3241+ forwarded_len -= pos;
3242+ pos = 0 ;
3243+ }
32263244 }
32273245 }
32283246
@@ -3234,10 +3252,8 @@ where
32343252 . checked_add ( 2 )
32353253 . ok_or_else ( || miette ! ( "Chunk size overflow" ) ) ?;
32363254
3237- ensure_chunked_framing_capacity ( framing_bytes, 2 ) ?;
32383255 relay_chunked_until_len (
32393256 reader,
3240- writer,
32413257 & mut read_buf,
32423258 & mut parse_buf,
32433259 chunk_with_crlf_end,
@@ -3248,42 +3264,35 @@ where
32483264 if & parse_buf[ chunk_end..chunk_with_crlf_end] != b"\r \n " {
32493265 return Err ( miette ! ( "Chunk missing terminating CRLF" ) ) ;
32503266 }
3251- add_chunked_framing_bytes ( & mut framing_bytes, 2 ) ?;
32523267 pos = chunk_with_crlf_end;
3268+ flush_validated_chunked_bytes (
3269+ writer,
3270+ & parse_buf,
3271+ & mut forwarded_len,
3272+ pos,
3273+ false ,
3274+ generation_guard,
3275+ )
3276+ . await ?;
32533277 chunk_count += 1 ;
32543278 chunk_payload_bytes = chunk_payload_bytes. saturating_add ( chunk_size) ;
32553279
32563280 // Keep parser memory bounded for long streams.
3257- if pos > RELAY_BUF_SIZE * 4 {
3281+ if pos > RELAY_BUF_SIZE * 4 && forwarded_len >= pos {
32583282 parse_buf. drain ( ..pos) ;
3283+ forwarded_len -= pos;
32593284 pos = 0 ;
32603285 }
32613286 }
32623287}
32633288
3264- fn ensure_chunked_framing_capacity ( current : usize , additional : usize ) -> Result < ( ) > {
3265- if additional > MAX_CHUNKED_FRAMING_BYTES . saturating_sub ( current) {
3266- return Err ( miette ! (
3267- "Chunked body framing exceeds {MAX_CHUNKED_FRAMING_BYTES} bytes"
3268- ) ) ;
3269- }
3270- Ok ( ( ) )
3271- }
3272-
3273- fn add_chunked_framing_bytes ( current : & mut usize , additional : usize ) -> Result < ( ) > {
3274- ensure_chunked_framing_capacity ( * current, additional) ?;
3275- * current += additional;
3276- Ok ( ( ) )
3277- }
3278-
3279- /// Read and forward only the bytes needed to reach `target_len`.
3289+ /// Read only the bytes needed to reach `target_len`.
32803290///
3281- /// Limiting each socket read to the current chunked framing boundary prevents
3282- /// a pipelined request from being consumed and written upstream as part of the
3283- /// authorized request body .
3284- async fn relay_chunked_until_len < R , W > (
3291+ /// The connection-scoped buffered reader may fetch more data from the socket,
3292+ /// but this parser consumes only the current body. Any read-ahead remains in
3293+ /// that reader for the next request's policy decision .
3294+ async fn relay_chunked_until_len < R > (
32853295 reader : & mut R ,
3286- writer : & mut W ,
32873296 read_buf : & mut [ u8 ; RELAY_BUF_SIZE ] ,
32883297 parse_buf : & mut Vec < u8 > ,
32893298 target_len : usize ,
@@ -3292,7 +3301,6 @@ async fn relay_chunked_until_len<R, W>(
32923301) -> Result < ( ) >
32933302where
32943303 R : AsyncRead + Unpin ,
3295- W : AsyncWrite + Unpin ,
32963304{
32973305 while parse_buf. len ( ) < target_len {
32983306 let remaining = target_len - parse_buf. len ( ) ;
@@ -3307,12 +3315,42 @@ where
33073315 if let Some ( guard) = generation_guard {
33083316 guard. ensure_current ( ) ?;
33093317 }
3310- writer. write_all ( & read_buf[ ..n] ) . await . into_diagnostic ( ) ?;
33113318 parse_buf. extend_from_slice ( & read_buf[ ..n] ) ;
33123319 }
33133320 Ok ( ( ) )
33143321}
33153322
3323+ /// Forward complete, validated chunk framing in relay-sized batches.
3324+ ///
3325+ /// Tiny chunks no longer cause one upstream write per framing byte, while the
3326+ /// final forced flush guarantees the complete body reaches upstream before the
3327+ /// response relay starts.
3328+ async fn flush_validated_chunked_bytes < W > (
3329+ writer : & mut W ,
3330+ parse_buf : & [ u8 ] ,
3331+ forwarded_len : & mut usize ,
3332+ validated_len : usize ,
3333+ force : bool ,
3334+ generation_guard : Option < & PolicyGenerationGuard > ,
3335+ ) -> Result < ( ) >
3336+ where
3337+ W : AsyncWrite + Unpin ,
3338+ {
3339+ let pending = validated_len. saturating_sub ( * forwarded_len) ;
3340+ if pending == 0 || ( !force && pending < RELAY_BUF_SIZE ) {
3341+ return Ok ( ( ) ) ;
3342+ }
3343+ if let Some ( guard) = generation_guard {
3344+ guard. ensure_current ( ) ?;
3345+ }
3346+ writer
3347+ . write_all ( & parse_buf[ * forwarded_len..validated_len] )
3348+ . await
3349+ . into_diagnostic ( ) ?;
3350+ * forwarded_len = validated_len;
3351+ Ok ( ( ) )
3352+ }
3353+
33163354fn find_crlf ( buf : & [ u8 ] , start : usize ) -> Option < usize > {
33173355 buf. get ( start..) ?
33183356 . windows ( 2 )
@@ -3940,6 +3978,7 @@ mod tests {
39403978 #[ derive( Default ) ]
39413979 struct CountingWriter {
39423980 writes : usize ,
3981+ bytes : Vec < u8 > ,
39433982 }
39443983
39453984 impl AsyncWrite for CountingWriter {
@@ -3949,6 +3988,7 @@ mod tests {
39493988 buffer : & [ u8 ] ,
39503989 ) -> Poll < std:: io:: Result < usize > > {
39513990 self . writes += 1 ;
3991+ self . bytes . extend_from_slice ( buffer) ;
39523992 Poll :: Ready ( Ok ( buffer. len ( ) ) )
39533993 }
39543994
@@ -4677,8 +4717,9 @@ mod tests {
46774717 let mut wire = chunked_body. to_vec ( ) ;
46784718 wire. extend_from_slice ( pipelined_request) ;
46794719
4680- let ( mut relay_reader, mut client_writer) = tokio:: io:: duplex ( 4096 ) ;
4720+ let ( relay_reader, mut client_writer) = tokio:: io:: duplex ( 4096 ) ;
46814721 client_writer. write_all ( & wire) . await . unwrap ( ) ;
4722+ let mut relay_reader = tokio:: io:: BufReader :: with_capacity ( 4096 , relay_reader) ;
46824723 let ( mut relay_writer, mut upstream_reader) = tokio:: io:: duplex ( 4096 ) ;
46834724
46844725 relay_chunked ( & mut relay_reader, & mut relay_writer, & [ ] , None )
@@ -4704,8 +4745,9 @@ mod tests {
47044745 let mut later_read = body_remainder. to_vec ( ) ;
47054746 later_read. extend_from_slice ( pipelined_request) ;
47064747
4707- let ( mut relay_reader, mut client_writer) = tokio:: io:: duplex ( 4096 ) ;
4748+ let ( relay_reader, mut client_writer) = tokio:: io:: duplex ( 4096 ) ;
47084749 client_writer. write_all ( & later_read) . await . unwrap ( ) ;
4750+ let mut relay_reader = tokio:: io:: BufReader :: with_capacity ( 4096 , relay_reader) ;
47094751 let ( mut relay_writer, mut upstream_reader) = tokio:: io:: duplex ( 4096 ) ;
47104752 relay_writer. write_all ( already_forwarded) . await . unwrap ( ) ;
47114753
@@ -4731,32 +4773,35 @@ mod tests {
47314773 }
47324774
47334775 #[ tokio:: test]
4734- async fn relay_chunked_bounds_tiny_chunk_io_amplification ( ) {
4735- let mut wire = Vec :: new ( ) ;
4776+ async fn relay_chunked_buffers_tiny_chunks_without_an_aggregate_framing_limit ( ) {
4777+ let mut chunked_body = Vec :: new ( ) ;
47364778 for _ in 0 ..10_000 {
4737- wire . extend_from_slice ( b"1\r \n a\r \n " ) ;
4779+ chunked_body . extend_from_slice ( b"1\r \n a\r \n " ) ;
47384780 }
4739- wire. extend_from_slice ( b"0\r \n \r \n " ) ;
4781+ chunked_body. extend_from_slice ( b"0\r \n \r \n " ) ;
4782+ let pipelined_request = b"DELETE /blocked HTTP/1.1\r \n Host: example.com\r \n \r \n " ;
4783+ let mut wire = chunked_body. clone ( ) ;
4784+ wire. extend_from_slice ( pipelined_request) ;
47404785
4741- let mut reader = CountingReader :: new ( wire) ;
4786+ let mut reader =
4787+ tokio:: io:: BufReader :: with_capacity ( RELAY_BUF_SIZE , CountingReader :: new ( wire) ) ;
47424788 let mut writer = CountingWriter :: default ( ) ;
4743- let error = relay_chunked ( & mut reader, & mut writer, & [ ] , None )
4789+ relay_chunked ( & mut reader, & mut writer, & [ ] , None )
47444790 . await
4745- . expect_err ( "excessive aggregate chunk framing must be rejected") ;
4791+ . expect ( "valid tiny chunks must not be rejected by an aggregate framing limit ") ;
47464792
4793+ let reads_after_body = reader. get_ref ( ) . reads ;
4794+ let mut remaining = vec ! [ 0 ; pipelined_request. len( ) ] ;
4795+ reader. read_exact ( & mut remaining) . await . unwrap ( ) ;
4796+ assert_eq ! ( remaining, pipelined_request) ;
4797+ assert_eq ! ( writer. bytes, chunked_body) ;
47474798 assert ! (
4748- error
4749- . to_string( )
4750- . contains( "Chunked body framing exceeds 32768 bytes" )
4751- ) ;
4752- assert ! (
4753- reader. reads < 40_000 ,
4754- "framing limit allowed {} socket reads" ,
4755- reader. reads
4799+ reads_after_body < 100 ,
4800+ "connection buffering required {reads_after_body} underlying reads"
47564801 ) ;
47574802 assert ! (
4758- writer. writes < 40_000 ,
4759- "framing limit allowed {} upstream writes" ,
4803+ writer. writes < 100 ,
4804+ "framing coalescing required {} upstream writes" ,
47604805 writer. writes
47614806 ) ;
47624807 }
0 commit comments