@@ -165,20 +165,48 @@ impl TypedMultipartError {
165165 }
166166 }
167167
168- /// Public-facing message for the JSON error envelope.
168+ /// Serialize the canonical `4xx`/`422` JSON error envelope
169+ /// (`{"errors":[{"message":...,"path":...}]}`) for this error — byte-
170+ /// identical to `Validated<T>`'s envelope so JNI hoisting and clients
171+ /// treat both uniformly.
169172 ///
170- /// `Other` wraps internal I/O / blocking-task failures whose source
171- /// string can leak implementation details (temp-file paths, OS error
172- /// text); it is the only `500` variant, so it returns a stable, generic
173- /// message. Every other variant returns its `Display` (already safe —
174- /// it describes a client-supplied field problem). The full `Display`
175- /// (including `Other`'s `source`) stays available for server-side
176- /// logging via the `std::error::Error` impl.
177- fn response_message ( & self ) -> Cow < ' _ , str > {
178- if matches ! ( self , Self :: Other { .. } ) {
179- Cow :: Borrowed ( "internal error while processing multipart request" )
173+ /// The message streams through [`MultipartMessage`]: `Other` (the only
174+ /// `500`, whose source can leak temp-file paths / OS text) yields a stable
175+ /// generic string; every other (client-caused) variant streams its own
176+ /// `Display` with NO intermediate `String`. `path` is the offending field
177+ /// name when known, else empty. Infallible in practice; the fallback keeps
178+ /// this request-time path panic-free instead of unwinding in a handler.
179+ fn error_body ( & self ) -> Vec < u8 > {
180+ serde_json:: to_vec ( & MultipartErrorEnvelope {
181+ errors : [ MultipartOneError {
182+ message : MultipartMessage ( self ) ,
183+ path : self . field_name ( ) . unwrap_or ( "" ) ,
184+ } ] ,
185+ } )
186+ . unwrap_or_else ( |_| br#"{"errors":[{"message":"serialization error","path":""}]}"# . to_vec ( ) )
187+ }
188+ }
189+
190+ /// Stable, source-free public message for the only `500` variant (`Other`),
191+ /// whose wrapped `source` can leak temp-file paths / OS error text. Every
192+ /// other variant is client-caused and safe to expose verbatim.
193+ const MULTIPART_INTERNAL_ERROR_MSG : & str = "internal error while processing multipart request" ;
194+
195+ /// Streams a multipart error's public message straight into the serializer
196+ /// with NO intermediate `String`: `Other` becomes [`MULTIPART_INTERNAL_ERROR_MSG`];
197+ /// every other (client-caused) variant streams its own `Display` via
198+ /// `collect_str`. Byte-identical to the previous `to_string()`-then-serialize
199+ /// path (serde escapes a `collect_str` stream exactly like an equal `&str`)
200+ /// but allocation-free on the common client-error path — mirroring
201+ /// `Validated<T>`'s 422 serializer.
202+ struct MultipartMessage < ' a > ( & ' a TypedMultipartError ) ;
203+
204+ impl serde:: Serialize for MultipartMessage < ' _ > {
205+ fn serialize < S : serde:: Serializer > ( & self , serializer : S ) -> Result < S :: Ok , S :: Error > {
206+ if matches ! ( self . 0 , TypedMultipartError :: Other { .. } ) {
207+ serializer. serialize_str ( MULTIPART_INTERNAL_ERROR_MSG )
180208 } else {
181- Cow :: Owned ( self . to_string ( ) )
209+ serializer . collect_str ( self . 0 )
182210 }
183211 }
184212}
@@ -191,7 +219,7 @@ impl TypedMultipartError {
191219/// map/array/object intermediate).
192220#[ derive( serde:: Serialize ) ]
193221struct MultipartOneError < ' a > {
194- message : & ' a str ,
222+ message : MultipartMessage < ' a > ,
195223 path : & ' a str ,
196224}
197225
@@ -221,24 +249,9 @@ impl IntoResponse for TypedMultipartError {
221249 Self :: FieldTooLarge { .. } => StatusCode :: PAYLOAD_TOO_LARGE ,
222250 Self :: Other { .. } => StatusCode :: INTERNAL_SERVER_ERROR ,
223251 } ;
224- // Serialize the canonical 422 envelope (see module-scope
225- // `MultipartErrorEnvelope` / `MultipartOneError`); `path` is the
226- // offending field name when known, else empty.
227- let path = self . field_name ( ) . unwrap_or ( "" ) ;
228- let message = self . response_message ( ) ;
229- let body = serde_json:: to_vec ( & MultipartErrorEnvelope {
230- errors : [ MultipartOneError {
231- message : & message,
232- path,
233- } ] ,
234- } )
235- // Serializing a struct of two `&str` is infallible in practice; the
236- // fallback keeps this request-time error path panic-free (matching
237- // `Validated<T>`'s 422 envelope) by emitting a minimal valid envelope
238- // instead of unwinding inside a handler.
239- . unwrap_or_else ( |_| {
240- br#"{"errors":[{"message":"serialization error","path":""}]}"# . to_vec ( )
241- } ) ;
252+ // Serialize the canonical 422 envelope (see `error_body` /
253+ // module-scope `MultipartErrorEnvelope`).
254+ let body = self . error_body ( ) ;
242255 (
243256 status,
244257 [ (
0 commit comments