@@ -31,10 +31,31 @@ use worker::*;
3131
3232use crate :: frontend_worker:: { ApiResult , AppError } ;
3333
34+ /// An error surfaced by a [`BodyStream`], distinguishing a client-side
35+ /// decode fault from a transport failure so the `add-entries` handler can
36+ /// map each to the right HTTP status (see `From<BodyError> for AppError`).
37+ #[ derive( Debug ) ]
38+ pub ( crate ) enum BodyError {
39+ /// Malformed or truncated gzip body: a client fault, mapped to 400.
40+ Decode ( String ) ,
41+ /// Transport failure reading the underlying request body: mapped to
42+ /// 500.
43+ Transport ( Error ) ,
44+ }
45+
46+ impl std:: fmt:: Display for BodyError {
47+ fn fmt ( & self , f : & mut std:: fmt:: Formatter < ' _ > ) -> std:: fmt:: Result {
48+ match self {
49+ BodyError :: Decode ( e) => write ! ( f, "{e}" ) ,
50+ BodyError :: Transport ( e) => write ! ( f, "{e}" ) ,
51+ }
52+ }
53+ }
54+
3455/// A boxed, `Unpin` body stream, the common type the `add-entries`
3556/// handler feeds to [`crate::stream_buffer::StreamBuffer`] regardless of
3657/// whether the request body was identity- or gzip-encoded.
37- pub ( crate ) type BodyStream = Pin < Box < dyn Stream < Item = Result < Vec < u8 > > > > > ;
58+ pub ( crate ) type BodyStream = Pin < Box < dyn Stream < Item = std :: result :: Result < Vec < u8 > , BodyError > > > > ;
3859
3960/// Open the request body as a decoded chunk stream, honoring
4061/// `Content-Encoding`.
@@ -62,7 +83,7 @@ pub(crate) fn decoded_stream(
6283 // `Result<Vec<u8>>` chunk contract the buffer/gunzip pipeline expects.
6384 let raw = body. into_data_stream ( ) . map ( |r| {
6485 r. map ( |b| b. to_vec ( ) )
65- . map_err ( |e| Error :: from ( e. to_string ( ) ) )
86+ . map_err ( |e| BodyError :: Transport ( Error :: from ( e. to_string ( ) ) ) )
6687 } ) ;
6788 let stream: BodyStream = match encoding. as_str ( ) {
6889 "" | "identity" => Box :: pin ( raw) ,
@@ -125,7 +146,7 @@ impl GzipInflater {
125146/// terminal `Err` item, after which the stream ends.
126147pub ( crate ) fn gunzip < S > ( inner : S ) -> BodyStream
127148where
128- S : Stream < Item = Result < Vec < u8 > > > + Unpin + ' static ,
149+ S : Stream < Item = std :: result :: Result < Vec < u8 > , BodyError > > + Unpin + ' static ,
129150{
130151 struct DecodeState < S > {
131152 inner : S ,
@@ -151,7 +172,7 @@ where
151172 Ok ( out) => out,
152173 Err ( e) => {
153174 st. done = true ;
154- return Some ( ( Err ( e ) , st) ) ;
175+ return Some ( ( Err ( BodyError :: Decode ( e . to_string ( ) ) ) , st) ) ;
155176 }
156177 } ;
157178 // A chunk may not yet yield any plaintext (partial
@@ -172,7 +193,7 @@ where
172193 return match inflater. finish ( ) {
173194 Ok ( tail) if !tail. is_empty ( ) => Some ( ( Ok ( tail) , st) ) ,
174195 Ok ( _) => None ,
175- Err ( e) => Some ( ( Err ( e ) , st) ) ,
196+ Err ( e) => Some ( ( Err ( BodyError :: Decode ( e . to_string ( ) ) ) , st) ) ,
176197 } ;
177198 }
178199 }
@@ -199,13 +220,15 @@ mod tests {
199220 fn chunked_stream (
200221 bytes : & [ u8 ] ,
201222 size : usize ,
202- ) -> impl Stream < Item = Result < Vec < u8 > > > + Unpin + ' static {
203- let chunks: Vec < Result < Vec < u8 > > > =
223+ ) -> impl Stream < Item = std :: result :: Result < Vec < u8 > , BodyError > > + Unpin + ' static {
224+ let chunks: Vec < std :: result :: Result < Vec < u8 > , BodyError > > =
204225 bytes. chunks ( size. max ( 1 ) ) . map ( |c| Ok ( c. to_vec ( ) ) ) . collect ( ) ;
205226 stream:: iter ( chunks)
206227 }
207228
208- async fn collect ( mut s : impl Stream < Item = Result < Vec < u8 > > > + Unpin ) -> Result < Vec < u8 > > {
229+ async fn collect (
230+ mut s : impl Stream < Item = std:: result:: Result < Vec < u8 > , BodyError > > + Unpin ,
231+ ) -> std:: result:: Result < Vec < u8 > , BodyError > {
209232 let mut out = Vec :: new ( ) ;
210233 while let Some ( item) = s. next ( ) . await {
211234 out. extend_from_slice ( & item?) ;
@@ -249,7 +272,10 @@ mod tests {
249272 // ends mid-member; finish() must report the truncation.
250273 compressed. truncate ( compressed. len ( ) - 6 ) ;
251274 let err = collect ( gunzip ( chunked_stream ( & compressed, 4 ) ) ) . await ;
252- assert ! ( err. is_err( ) , "truncated gzip must surface an error" ) ;
275+ assert ! (
276+ matches!( err, Err ( BodyError :: Decode ( _) ) ) ,
277+ "truncated gzip must surface a client decode error"
278+ ) ;
253279 }
254280
255281 #[ tokio:: test]
@@ -259,14 +285,18 @@ mod tests {
259285 let mid = compressed. len ( ) / 2 ;
260286 compressed[ mid] ^= 0xff ;
261287 let err = collect ( gunzip ( chunked_stream ( & compressed, 5 ) ) ) . await ;
262- assert ! ( err. is_err( ) , "corrupt gzip must surface an error" ) ;
288+ assert ! (
289+ matches!( err, Err ( BodyError :: Decode ( _) ) ) ,
290+ "corrupt gzip must surface a client decode error"
291+ ) ;
263292 }
264293
265294 #[ tokio:: test]
266295 async fn upstream_error_propagates ( ) {
267296 let compressed = gzip ( b"partial" ) ;
268- let mut chunks: Vec < Result < Vec < u8 > > > = vec ! [ Ok ( compressed[ ..4 ] . to_vec( ) ) ] ;
269- chunks. push ( Err ( Error :: from ( "boom" ) ) ) ;
297+ let mut chunks: Vec < std:: result:: Result < Vec < u8 > , BodyError > > =
298+ vec ! [ Ok ( compressed[ ..4 ] . to_vec( ) ) ] ;
299+ chunks. push ( Err ( BodyError :: Transport ( Error :: from ( "boom" ) ) ) ) ;
270300 let err = collect ( gunzip ( stream:: iter ( chunks) ) ) . await ;
271301 assert ! ( err. is_err( ) , "upstream stream error must propagate" ) ;
272302 }
0 commit comments