toolkit_contract/runtime/multipart.rs
1//! `multipart/mixed` reader used by streaming clients.
2//!
3//! Translates a byte stream into a stream of typed items, one JSON item per
4//! body part. This is streaming *response* framing only — `multipart/form-data`
5//! requests, mixed per-part content types and `Content-Disposition` are all out
6//! of scope, and every part is assumed to carry a JSON document decodable as
7//! `T`.
8//!
9//! ### Framing is length-driven, not parse-driven
10//!
11//! A part's extent is decided by its MIME headers, never by where its JSON
12//! payload happens to become syntactically complete. Two modes, in this
13//! precedence:
14//!
15//! 1. **`Content-Length: N` present** — consume exactly `N` body bytes, decode,
16//! then expect the delimiter. Exact, one part per part, no lookahead, so a
17//! part is yielded the instant its own bytes have arrived — the reader never
18//! runs a part behind a live stream.
19//! 2. **Absent** — strict RFC 2046 delimiter scan. Correct, but a part is only
20//! emitted once the *next* delimiter (or the close delimiter) arrives, so a
21//! live producer that writes part `N+1`'s delimiter only when item `N+1`
22//! exists leaves the reader one part behind.
23//!
24//! `Content-Length` on a body part is unusual but well-formed: RFC 2046 §5.1
25//! makes the *delimiter* the authoritative boundary mechanism, so a strict
26//! parser that ignores the header still reads the stream correctly. The header
27//! is a fast path, not a protocol fork.
28//!
29//! ### A missing close delimiter is a truncation
30//!
31//! A complete multipart body ends with the `--<boundary>--` close delimiter
32//! (RFC 2046). A graceful end of the byte stream that did **not** see it is a
33//! truncation — a proxy idle-timeout, an LB half-close, or a rolling deploy
34//! that closed the connection mid-body — and the streaming client surfaces it
35//! as a [`TransportError::Framing`] end-of-stream error, exactly as the SSE
36//! path errors on a close without a terminal `event: done`.
37//!
38//! This is reported by the transport rather than left to the consumer because
39//! on the public path the consumer *cannot* detect it: `send_streaming` /
40//! `open_streaming` return a boxed stream that erases the concrete
41//! [`MultipartStream`], so [`MultipartStream::saw_close_delimiter`] is
42//! unreachable there, and a generic item type carries no terminal marker of its
43//! own. (`saw_close_delimiter` remains available to a caller holding the
44//! concrete stream, as diagnostic detail about the framing.) A partially
45//! buffered part at end of stream is discarded rather than surfaced, mirroring
46//! how the SSE parser discards an unterminated trailing event.
47//!
48//! An *aborted* body — the peer's byte stream erroring, which is what a
49//! truncated chunked encoding looks like — is a different thing and is
50//! surfaced as [`TransportError::Network`]. That is reserved for a genuine
51//! transport fault (a server item that would not serialize at all).
52//!
53//! A post-open **domain** failure is not an abort: the framer sends it as a
54//! typed **error part** — one `application/problem+json` part whose body is an
55//! RFC 9457 [`Problem`] — followed by the close delimiter. This reader decodes
56//! such a part into [`TransportError::Problem`] (which the generated client
57//! recovers as a typed `CanonicalError`), so a mid-stream domain error arrives
58//! as a *typed* `Err` item, not a truncation. An error part is **terminal**: the
59//! reader stops after it and treats it as a clean end, so a non-conforming peer
60//! cannot smuggle further data items past a reported error.
61//!
62//! Accumulated buffers are bounded by [`MAX_ACCUMULATED_BYTES`] for the same
63//! reason the SSE parser bounds its own: otherwise a peer that streams an
64//! unterminated construct grows the buffer without limit for the lifetime of a
65//! self-healing, indefinitely-reconnecting client.
66
67use std::collections::VecDeque;
68use std::pin::Pin;
69use std::task::{Context, Poll};
70
71use bytes::{Buf, Bytes, BytesMut};
72use futures_core::Stream;
73use serde::de::DeserializeOwned;
74
75use toolkit_canonical_errors::Problem;
76
77use crate::ir::binding::StreamFraming;
78use crate::runtime::sse::StreamActivity;
79use crate::runtime::transport_error::TransportError;
80
81/// Maximum bytes the reader accumulates for a single not-yet-complete
82/// construct — the preamble, a part's header block, a length-less part body, or
83/// a delimiter — before treating the peer as protocol-violating and
84/// terminating the stream with [`TransportError::Framing`]. Same value and same
85/// rationale as the SSE parser's own guard.
86pub const MAX_ACCUMULATED_BYTES: usize = 16 * 1024 * 1024;
87
88const CRLF: &[u8] = b"\r\n";
89const CLOSE_MARKER: &[u8] = b"--";
90const CONTENT_LENGTH: &[u8] = b"content-length";
91const CONTENT_TYPE: &[u8] = b"content-type";
92/// Media type marking a part as a typed **error part**: its body is an RFC 9457
93/// [`Problem`], not a `T`. The server framer emits exactly this token (see
94/// `toolkit::http::multipart`); matched case-insensitively and ignoring any
95/// `;`-parameters.
96const PROBLEM_MEDIA_TYPE: &[u8] = b"application/problem+json";
97
98/// Adapter that lifts a `Display`-only error into an
99/// `Error + Send + Sync + 'static` so it can be boxed into
100/// [`TransportError::Network`] without losing the original message.
101///
102/// Twin of the same adapter in [`crate::runtime::sse`]; duplicated rather than
103/// shared because the SSE parser is deliberately not touched by this work.
104#[derive(Debug)]
105struct DisplayError(String);
106impl std::fmt::Display for DisplayError {
107 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
108 f.write_str(&self.0)
109 }
110}
111impl std::error::Error for DisplayError {}
112
113/// A framing error message that keeps its underlying cause reachable via
114/// [`Error::source`], so a caller can see the `Utf8Error`/`ParseIntError`
115/// behind a parse failure rather than a flattened string (#4740). `Display` is
116/// the human message; `source()` is the real error.
117#[derive(Debug)]
118struct SourcedError {
119 message: String,
120 source: Box<dyn std::error::Error + Send + Sync + 'static>,
121}
122impl std::fmt::Display for SourcedError {
123 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
124 f.write_str(&self.message)
125 }
126}
127impl std::error::Error for SourcedError {
128 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
129 Some(&*self.source)
130 }
131}
132
133/// Extract the `boundary` parameter from a `Content-Type` header value.
134///
135/// The media type is matched case-insensitively and the boundary may be quoted
136/// (`boundary="abc"`) or bare (`boundary=abc`). Parameters are split on `;`,
137/// which is safe because RFC 2046 excludes `;` from the legal boundary
138/// characters, so no legal boundary can contain one even when quoted.
139///
140/// # Errors
141/// [`TransportError::Framing`] when the media type is not `multipart/mixed`,
142/// when no `boundary` parameter is present, or when the parameter is empty or
143/// an unterminated quoted string.
144pub fn boundary_from_content_type(value: &str) -> Result<String, TransportError> {
145 let mut params = value.split(';');
146 let media_type = params.next().unwrap_or_default().trim();
147 if !media_type.eq_ignore_ascii_case(StreamFraming::MultipartMixed.media_type()) {
148 return Err(framing_error(format!(
149 "expected media type `{}`, got `{media_type}`",
150 StreamFraming::MultipartMixed.media_type()
151 )));
152 }
153
154 for param in params {
155 let Some((name, raw)) = param.split_once('=') else {
156 continue;
157 };
158 if !name.trim().eq_ignore_ascii_case("boundary") {
159 continue;
160 }
161 let raw = raw.trim();
162 let boundary = if let Some(inner) = raw.strip_prefix('"') {
163 inner.strip_suffix('"').ok_or_else(|| {
164 framing_error(format!(
165 "`boundary` parameter is not a closed quoted string: `{raw}`"
166 ))
167 })?
168 } else {
169 raw
170 };
171 if boundary.is_empty() {
172 return Err(framing_error("`boundary` parameter is empty"));
173 }
174 return Ok(boundary.to_owned());
175 }
176
177 Err(framing_error(format!(
178 "`{}` response carries no `boundary` parameter: `{value}`",
179 StreamFraming::MultipartMixed.media_type()
180 )))
181}
182
183fn framing_error(message: impl Into<String>) -> TransportError {
184 TransportError::framing(StreamFraming::MultipartMixed, DisplayError(message.into()))
185}
186
187/// A framing error whose `source()` is `source` and whose `Display` is
188/// `message`, so the underlying parse error stays on the chain (#4740).
189fn framing_error_sourced(
190 message: impl Into<String>,
191 source: impl std::error::Error + Send + Sync + 'static,
192) -> TransportError {
193 TransportError::framing(
194 StreamFraming::MultipartMixed,
195 SourcedError {
196 message: message.into(),
197 source: Box::new(source),
198 },
199 )
200}
201
202/// Render an untrusted header value for an error message: truncate to a short
203/// fixed length and escape control characters, so a hostile peer cannot inject
204/// bare CR/LF or up to `MAX_ACCUMULATED_BYTES` of arbitrary bytes into a string
205/// that is then propagated and logged (#4740).
206fn display_value(text: &str) -> String {
207 const MAX_CHARS: usize = 64;
208 let mut out = String::new();
209 let mut truncated = false;
210 for (i, ch) in text.chars().enumerate() {
211 if i >= MAX_CHARS {
212 truncated = true;
213 break;
214 }
215 if ch.is_control() {
216 out.extend(ch.escape_default());
217 } else {
218 out.push(ch);
219 }
220 }
221 if truncated {
222 out.push_str("...");
223 }
224 out
225}
226
227/// Parse a `multipart/mixed` byte stream into a stream of typed items.
228///
229/// `body` is typically the byte-stream view of
230/// `toolkit_http::HttpResponse::into_body()` (adapted via
231/// [`crate::runtime::http::body_to_byte_stream`]); `boundary` is the value
232/// returned by [`boundary_from_content_type`] — the bare token, with no
233/// leading dashes. Errors from the inner stream are surfaced as
234/// [`TransportError::Network`].
235pub fn parse_multipart_stream<T, S, E>(body: S, boundary: &str) -> MultipartStream<T, S>
236where
237 S: Stream<Item = Result<Bytes, E>> + Unpin + 'static,
238 E: std::fmt::Display,
239 T: DeserializeOwned + 'static,
240{
241 let mut dash_boundary = Vec::with_capacity(CLOSE_MARKER.len() + boundary.len());
242 dash_boundary.extend_from_slice(CLOSE_MARKER);
243 dash_boundary.extend_from_slice(boundary.as_bytes());
244
245 let mut crlf_dash_boundary = Vec::with_capacity(CRLF.len() + dash_boundary.len());
246 crlf_dash_boundary.extend_from_slice(CRLF);
247 crlf_dash_boundary.extend_from_slice(&dash_boundary);
248
249 MultipartStream {
250 inner: body,
251 buf: BytesMut::with_capacity(4 * 1024),
252 scan_from: 0,
253 state: State::Preamble,
254 pending: VecDeque::new(),
255 dash_boundary,
256 crlf_dash_boundary,
257 deferred_decode_error: None,
258 done: false,
259 saw_close: false,
260 activity: StreamActivity::new(),
261 _marker: std::marker::PhantomData,
262 }
263}
264
265/// What a part's `Content-Type` says its body is, and therefore how the reader
266/// decodes it: an ordinary data item (`T`) or a typed error part (a [`Problem`],
267/// `application/problem+json`). Selected in [`MultipartStream::step_headers`] and
268/// carried through the body states.
269#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
270enum PartKind {
271 /// A normal `application/json` data part; the body decodes as `T`.
272 #[default]
273 Data,
274 /// An `application/problem+json` error part; the body decodes as a
275 /// [`Problem`] and is surfaced as a *terminal* typed `Err` item.
276 Error,
277}
278
279/// Where the reader is in the `multipart/mixed` grammar.
280///
281/// The plan's single `Delimiter` state is realised as the pair
282/// `Delimiter` (expects the `CRLF--<boundary>` token that closes a part) and
283/// `BoundaryTail` (having consumed a boundary token, decides between `CRLF`
284/// for another part and `--` for the close delimiter). Splitting them lets
285/// `Preamble` reuse the tail logic, since the very first delimiter has no
286/// leading `CRLF`.
287#[derive(Debug, Clone, Copy)]
288enum State {
289 /// Discard bytes until the first `--<boundary>`.
290 Preamble,
291 /// A boundary token has just been consumed: `CRLF` starts another part,
292 /// `--` is the close delimiter.
293 BoundaryTail,
294 /// Accumulating a part's header block, up to its terminating empty line.
295 Headers {
296 /// `Content-Length` seen so far for this part, if any.
297 content_length: Option<usize>,
298 /// Header-block bytes consumed so far, for the accumulation guard —
299 /// header lines leave `buf` as they are parsed, so `buf.len()` alone
300 /// would not bound a peer streaming an endless run of tiny headers.
301 consumed: usize,
302 /// What this part's `Content-Type` has said so far.
303 kind: PartKind,
304 },
305 /// Reading a part body. `Some(n)` is the exact `Content-Length` byte count;
306 /// `None` means scan for the next delimiter.
307 Body {
308 /// Exact remaining body length, when the part declared one.
309 remaining: Option<usize>,
310 /// Carried from [`State::Headers`]: how to decode this body.
311 kind: PartKind,
312 },
313 /// Expecting the `CRLF--<boundary>` token that closes the part just read.
314 Delimiter,
315 /// Terminal.
316 Closed,
317}
318
319/// Outcome of one step of the state machine.
320#[derive(Debug, Clone, Copy)]
321enum Step {
322 /// Progress was made — step again.
323 Progress,
324 /// Not enough buffered bytes to make progress — poll the wire.
325 NeedMore,
326 /// Terminal state reached; stop stepping.
327 Stop,
328}
329
330/// Stream yielded by [`parse_multipart_stream`].
331pub struct MultipartStream<T, S> {
332 inner: S,
333 buf: BytesMut,
334 /// Prefix length of `buf`, in bytes, already confirmed to contain no match
335 /// for whatever the current state is scanning for. Lets [`find_from`]
336 /// resume instead of rescanning the whole buffer on every poll — without
337 /// this, a single long construct delivered over many small chunks costs
338 /// `O(total_length²)` instead of `O(total_length)`. Reset to 0 whenever
339 /// bytes are consumed or the state changes what it scans for.
340 scan_from: usize,
341 state: State,
342 pending: VecDeque<Result<T, TransportError>>,
343 /// `--<boundary>`, precomputed.
344 dash_boundary: Vec<u8>,
345 /// `CRLF--<boundary>`, precomputed.
346 crlf_dash_boundary: Vec<u8>,
347 /// A sized part's body that would not decode, held until the bytes that
348 /// follow reveal *why*. A well-framed part with a malformed payload is a
349 /// [`TransportError::Serialization`] fault; a part whose declared length
350 /// disagrees with its delimiter is a [`TransportError::Framing`] fault —
351 /// and from the body bytes alone the two are indistinguishable, because a
352 /// wrong length hands the decoder the wrong bytes. Deciding at the
353 /// delimiter keeps the *successful* decode on the fast path, which is what
354 /// the latency property depends on: a part that decodes is yielded from
355 /// its own bytes, and only a part that does not wait for its delimiter.
356 ///
357 /// Only ever set while in [`State::Delimiter`].
358 deferred_decode_error: Option<serde_json::Error>,
359 done: bool,
360 saw_close: bool,
361 activity: StreamActivity,
362 _marker: std::marker::PhantomData<fn() -> T>,
363}
364
365impl<T, S> MultipartStream<T, S> {
366 /// Returns a clone of the shared wire-activity counter, bumped on every
367 /// wire chunk — including chunks that dispatch no item, such as a part
368 /// header block arriving on its own. The streaming client snapshots it
369 /// around an idle-timeout wait so the idle deadline stays *idle* rather
370 /// than merely *quiet*. Mirrors
371 /// [`crate::runtime::sse::SseStream::activity_handle`].
372 #[must_use]
373 pub fn activity_handle(&self) -> StreamActivity {
374 self.activity.clone()
375 }
376
377 /// Whether the stream reached a graceful end — the peer sent the closing
378 /// `--<boundary>--` delimiter, **or** a terminal error part ended it (an
379 /// error part is a complete terminator, so it counts as a clean end).
380 ///
381 /// Analogous to [`crate::runtime::sse::SseStream::saw_done_event`]: the
382 /// streaming client treats a graceful end *without* either as a truncation
383 /// and surfaces a [`TransportError::Framing`] error (see the module docs).
384 /// This accessor exposes the same framing fact to a caller holding the
385 /// concrete stream as diagnostic detail. Only meaningful once the stream has
386 /// yielded `None`.
387 #[must_use]
388 pub fn saw_close_delimiter(&self) -> bool {
389 self.saw_close
390 }
391}
392
393// `inner` is bounded by `Unpin` at construction; the rest of the fields are
394// trivially `Unpin`. Implement `Unpin` unconditionally so callers can poll
395// `Pin<&mut MultipartStream<...>>` without pinning the type itself.
396impl<T, S: Unpin> Unpin for MultipartStream<T, S> {}
397
398impl<T, S> MultipartStream<T, S>
399where
400 T: DeserializeOwned + 'static,
401{
402 /// Run the state machine over the currently buffered bytes, stopping as
403 /// soon as an item is ready.
404 ///
405 /// Stopping at the first `pending` item bounds the work done in one
406 /// `poll_next`: one wire chunk carrying many parts no longer runs every
407 /// `serde_json::from_slice` before yielding, and no longer pushes every
408 /// decoded item into the unbounded `pending` queue at once. The remaining
409 /// buffered bytes are drained on the next poll (`poll_next` drains before it
410 /// polls the wire), so no chunk is needed to make progress.
411 fn drain_buffer(&mut self) {
412 while self.pending.is_empty() && matches!(self.step(), Step::Progress) {}
413 if matches!(self.state, State::Closed) {
414 self.done = true;
415 }
416 }
417
418 fn step(&mut self) -> Step {
419 match self.state {
420 State::Preamble => self.step_preamble(),
421 State::BoundaryTail => self.step_boundary_tail(),
422 State::Headers {
423 content_length,
424 consumed,
425 kind,
426 } => self.step_headers(content_length, consumed, kind),
427 State::Body { remaining, kind } => match remaining {
428 Some(n) => self.step_sized_body(n, kind),
429 None => self.step_scanned_body(kind),
430 },
431 State::Delimiter => self.step_delimiter(),
432 State::Closed => Step::Stop,
433 }
434 }
435
436 fn step_preamble(&mut self) -> Step {
437 let mut scan = self.scan_from;
438 let hit = find_from(&self.buf, &self.dash_boundary, &mut scan);
439 self.scan_from = scan;
440 match hit {
441 Some(at) => {
442 self.consume(at + self.dash_boundary.len());
443 self.state = State::BoundaryTail;
444 Step::Progress
445 }
446 None => self.need_more("multipart preamble"),
447 }
448 }
449
450 fn step_boundary_tail(&mut self) -> Step {
451 if self.buf.len() >= CLOSE_MARKER.len() {
452 if self.buf.starts_with(CLOSE_MARKER) {
453 self.consume(CLOSE_MARKER.len());
454 self.saw_close = true;
455 self.state = State::Closed;
456 return Step::Stop;
457 }
458 } else if CLOSE_MARKER.starts_with(&self.buf) {
459 // Could still turn into the close delimiter once more bytes land.
460 return self.need_more("multipart boundary delimiter");
461 }
462
463 let mut scan = self.scan_from;
464 let hit = find_from(&self.buf, CRLF, &mut scan);
465 self.scan_from = scan;
466 match hit {
467 Some(at) => {
468 // RFC 2046 permits transport padding (linear whitespace)
469 // between the boundary and its CRLF; anything else means the
470 // boundary token was a prefix of some longer token.
471 if self.buf[..at].iter().any(|b| !matches!(b, b' ' | b'\t')) {
472 return self.fail("boundary delimiter is followed by neither `CRLF` nor `--`");
473 }
474 self.consume(at + CRLF.len());
475 self.state = State::Headers {
476 content_length: None,
477 consumed: 0,
478 kind: PartKind::Data,
479 };
480 Step::Progress
481 }
482 None => self.need_more("multipart boundary delimiter"),
483 }
484 }
485
486 fn step_headers(
487 &mut self,
488 content_length: Option<usize>,
489 consumed: usize,
490 kind: PartKind,
491 ) -> Step {
492 let mut scan = self.scan_from;
493 let hit = find_from(&self.buf, CRLF, &mut scan);
494 self.scan_from = scan;
495 let Some(at) = hit else {
496 return self.need_more("multipart part headers");
497 };
498
499 let line = self.buf.split_to(at + CRLF.len());
500 self.scan_from = 0;
501 let consumed = consumed + line.len();
502
503 // An empty line terminates the header block. A part with no headers at
504 // all is legal MIME and lands here on the first iteration.
505 if at == 0 {
506 self.state = State::Body {
507 remaining: content_length,
508 kind,
509 };
510 return Step::Progress;
511 }
512
513 if consumed > MAX_ACCUMULATED_BYTES {
514 return self.fail(format!(
515 "multipart part headers exceed maximum accumulated size ({MAX_ACCUMULATED_BYTES} bytes); aborting stream"
516 ));
517 }
518
519 // `Content-Length` sizes the body; `Content-Type` is read only to spot a
520 // typed error part (`application/problem+json`). Every other part header
521 // is deliberately ignored.
522 let content_length = match header_value(&line[..at], CONTENT_LENGTH) {
523 None => content_length,
524 Some(raw) => match parse_content_length(raw) {
525 Ok(n) => Some(n),
526 Err(err) => return self.fail_with(err),
527 },
528 };
529 let kind = if is_problem_content_type(header_value(&line[..at], CONTENT_TYPE)) {
530 PartKind::Error
531 } else {
532 kind
533 };
534
535 self.state = State::Headers {
536 content_length,
537 consumed,
538 kind,
539 };
540 Step::Progress
541 }
542
543 fn step_sized_body(&mut self, len: usize, kind: PartKind) -> Step {
544 if self.buf.len() < len {
545 return self.need_more("multipart part body");
546 }
547 let body = self.buf.split_to(len);
548 self.scan_from = 0;
549 self.state = State::Delimiter;
550 // The delimiter has not been proven yet, so a decode failure here may
551 // be the length's fault rather than the payload's — defer it.
552 match Self::decode_body(&body, kind) {
553 Ok(item) => {
554 self.pending.push_back(item);
555 self.finish_if_error(kind);
556 }
557 Err(e) => self.deferred_decode_error = Some(e),
558 }
559 Step::Progress
560 }
561
562 fn step_scanned_body(&mut self, kind: PartKind) -> Step {
563 let mut scan = self.scan_from;
564 let hit = find_from(&self.buf, &self.crlf_dash_boundary, &mut scan);
565 self.scan_from = scan;
566 match hit {
567 Some(at) => {
568 let body = self.buf.split_to(at);
569 self.scan_from = 0;
570 // `buf` now starts with the delimiter the scan found, so the
571 // body's extent is already authoritative and a decode failure
572 // can only be the payload's fault.
573 self.state = State::Delimiter;
574 self.decode_now(&body, kind)
575 }
576 None => self.need_more("multipart part body"),
577 }
578 }
579
580 /// An error part is **terminal** (#4740 F5): once a typed error item is
581 /// queued, close the stream so a non-conforming peer's parts after it are
582 /// never surfaced as data. The framer emits the close delimiter after an
583 /// error part anyway, but the reader must not rely on the peer to stop.
584 ///
585 /// The error part is itself a graceful, complete terminator, so it counts as
586 /// a clean end (`saw_close`) — otherwise the streaming client would treat the
587 /// unread close delimiter as a truncation and append a spurious framing error
588 /// after the typed one.
589 fn finish_if_error(&mut self, kind: PartKind) {
590 if kind == PartKind::Error {
591 self.state = State::Closed;
592 self.saw_close = true;
593 }
594 }
595
596 fn step_delimiter(&mut self) -> Step {
597 // RFC 2046 makes the CRLF part of the delimiter, so it is normally
598 // present; tolerate a producer that omits it before the close
599 // delimiter, since the boundary token alone is unambiguous.
600 let skip = if self.buf.starts_with(CRLF) {
601 CRLF.len()
602 } else if self.buf.len() < CRLF.len() && CRLF.starts_with(&self.buf) {
603 return self.need_more("multipart part delimiter");
604 } else {
605 0
606 };
607
608 let outcome = {
609 let avail = &self.buf[skip..];
610 if avail.len() < self.dash_boundary.len() {
611 if self.dash_boundary.starts_with(avail) {
612 DelimiterCheck::NeedMore
613 } else {
614 DelimiterCheck::Overrun
615 }
616 } else if avail.starts_with(&self.dash_boundary) {
617 DelimiterCheck::Matched
618 } else {
619 DelimiterCheck::Overrun
620 }
621 };
622
623 match outcome {
624 DelimiterCheck::NeedMore => self.need_more("multipart part delimiter"),
625 // The bytes right after the part body are not a delimiter, so the
626 // part's declared length did not describe the part: either it ran
627 // short (leaving payload bytes here) or it ran long (having eaten
628 // into the delimiter). Either way the framing is unusable.
629 DelimiterCheck::Overrun => {
630 // The length is the root cause and subsumes any deferred
631 // decode failure it caused, so drop that and report the
632 // framing fault.
633 self.deferred_decode_error = None;
634 self.fail(
635 "part `Content-Length` does not agree with the multipart delimiter that follows it",
636 )
637 }
638 DelimiterCheck::Matched => {
639 // The part was framed correctly after all, so a held decode
640 // failure really was the payload's fault.
641 if let Some(e) = self.deferred_decode_error.take() {
642 return self.fail_serialization(e);
643 }
644 self.consume(skip + self.dash_boundary.len());
645 self.state = State::BoundaryTail;
646 Step::Progress
647 }
648 }
649 }
650
651 /// Decode a part body whose extent is already proven by its delimiter.
652 ///
653 /// A malformed part terminates the stream: unlike a named SSE event, a
654 /// part is unambiguously the typed data channel, so a body that will not
655 /// decode is real data loss and must not be silently dropped.
656 fn decode_now(&mut self, body: &[u8], kind: PartKind) -> Step {
657 match Self::decode_body(body, kind) {
658 Ok(item) => {
659 self.pending.push_back(item);
660 self.finish_if_error(kind);
661 Step::Progress
662 }
663 Err(e) => self.fail_serialization(e),
664 }
665 }
666
667 /// Decode one part body into a pending item. A [`PartKind::Data`] part
668 /// decodes as `T` and yields `Ok(item)`; a [`PartKind::Error`] part decodes
669 /// as an RFC 9457 [`Problem`] and yields `Err(TransportError::Problem { .. })`.
670 ///
671 /// The `Err` variant is a *decoded item*, not a stream fault; the caller
672 /// pushes it and then closes the stream (`finish_if_error`), because an error
673 /// part is terminal. Only a body that will not decode at all is a
674 /// `serde_json::Error` here — a genuine serialization fault the caller
675 /// surfaces via `fail_serialization` (directly, or deferred to the delimiter
676 /// for a sized body).
677 fn decode_body(
678 body: &[u8],
679 kind: PartKind,
680 ) -> Result<Result<T, TransportError>, serde_json::Error> {
681 match kind {
682 PartKind::Error => {
683 let problem = serde_json::from_slice::<Problem>(body)?;
684 Ok(Err(TransportError::problem(problem)))
685 }
686 PartKind::Data => Ok(Ok(serde_json::from_slice::<T>(body)?)),
687 }
688 }
689
690 fn fail_serialization(&mut self, e: serde_json::Error) -> Step {
691 self.state = State::Closed;
692 self.pending
693 .push_back(Err(TransportError::serialization(e)));
694 Step::Stop
695 }
696
697 fn consume(&mut self, n: usize) {
698 self.buf.advance(n);
699 self.scan_from = 0;
700 }
701
702 /// Not enough bytes to complete the current construct. Trips the
703 /// accumulation guard first, so a peer that streams unbounded bytes
704 /// without ever completing one terminates the stream instead of growing
705 /// `buf` without limit.
706 fn need_more(&mut self, what: &str) -> Step {
707 if self.buf.len() > MAX_ACCUMULATED_BYTES {
708 return self.fail(format!(
709 "{what} exceeds maximum accumulated size ({MAX_ACCUMULATED_BYTES} bytes); aborting stream"
710 ));
711 }
712 Step::NeedMore
713 }
714
715 fn fail(&mut self, message: impl Into<String>) -> Step {
716 self.fail_with(framing_error(message))
717 }
718
719 /// Like [`fail`](Self::fail) but for an already-built error, so a caller
720 /// that constructed a sourced framing error keeps its `source()` chain.
721 fn fail_with(&mut self, err: TransportError) -> Step {
722 self.state = State::Closed;
723 self.pending.push_back(Err(err));
724 Step::Stop
725 }
726}
727
728/// Result of matching the buffered bytes against a part delimiter.
729#[derive(Debug, Clone, Copy)]
730enum DelimiterCheck {
731 Matched,
732 NeedMore,
733 Overrun,
734}
735
736impl<T, S, E> Stream for MultipartStream<T, S>
737where
738 T: DeserializeOwned + 'static,
739 S: Stream<Item = Result<Bytes, E>> + Unpin + 'static,
740 E: std::fmt::Display,
741{
742 type Item = Result<T, TransportError>;
743
744 fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
745 let this = self.get_mut();
746
747 loop {
748 if let Some(item) = this.pending.pop_front() {
749 return Poll::Ready(Some(item));
750 }
751 if this.done {
752 return Poll::Ready(None);
753 }
754
755 // Drain what is already buffered before touching the wire: a
756 // previous poll may have left decoded-but-unyielded parts in the
757 // buffer (drain_buffer stops at the first item). Only poll the wire
758 // when the buffer can produce nothing more on its own.
759 this.drain_buffer();
760 if !this.pending.is_empty() || this.done {
761 continue;
762 }
763
764 match Pin::new(&mut this.inner).poll_next(cx) {
765 Poll::Pending => return Poll::Pending,
766 Poll::Ready(None) => {
767 // A graceful end of the byte stream is a clean end of this
768 // stream, with or without the closing `--<boundary>--`.
769 // Whatever partial construct is buffered is discarded
770 // rather than surfaced as a truncation error — see the
771 // module docs on why completion detection is the
772 // consumer's, and why an *aborted* body (the `Err` arm
773 // below) is loud instead.
774 this.done = true;
775 this.state = State::Closed;
776 this.buf.clear();
777 // A held decode failure is the one thing a graceful EOF
778 // does not excuse: the part's `Content-Length` said its
779 // bytes were all there, and they were, and they did not
780 // decode. That is data loss rather than truncation, so it
781 // is surfaced even though the stream is ending cleanly.
782 if let Some(e) = this.deferred_decode_error.take() {
783 this.pending
784 .push_back(Err(TransportError::serialization(e)));
785 }
786 }
787 Poll::Ready(Some(Err(e))) => {
788 this.done = true;
789 this.state = State::Closed;
790 // `E: Display` only — wrap in a small Display->Error
791 // adapter so the source chain stays intact through
792 // `TransportError::network`.
793 return Poll::Ready(Some(Err(TransportError::network(DisplayError(
794 e.to_string(),
795 )))));
796 }
797 Poll::Ready(Some(Ok(chunk))) => {
798 // Any wire chunk counts as activity — even one carrying
799 // only a part header block — so the idle timeout in the
800 // streaming driver can tell "quiet but alive" from "truly
801 // idle". The new bytes are drained at the top of the next
802 // loop iteration, before the wire is polled again.
803 this.activity.bump();
804 this.buf.extend_from_slice(&chunk);
805 }
806 }
807 }
808 }
809}
810
811/// Find `needle` in `haystack`, resuming from `*scan_from` — the length of a
812/// prefix already confirmed to start no match.
813///
814/// On a miss, `*scan_from` advances to `haystack.len()`, so the next call
815/// rescans only the unavoidable `needle.len() - 1` bytes of overlap plus
816/// whatever was appended since. On a hit it is left at the match offset; the
817/// caller consumes bytes and resets it.
818fn find_from(haystack: &[u8], needle: &[u8], scan_from: &mut usize) -> Option<usize> {
819 let start = scan_from.saturating_sub(needle.len().saturating_sub(1));
820 if needle.is_empty() || haystack.len() < needle.len() {
821 *scan_from = haystack.len();
822 return None;
823 }
824 for at in start..=(haystack.len() - needle.len()) {
825 if haystack[at..].starts_with(needle) {
826 *scan_from = at;
827 return Some(at);
828 }
829 }
830 *scan_from = haystack.len();
831 None
832}
833
834/// Value of the header `name` (given lowercase) on `line`, or `None` when the
835/// line is a different header. `line` excludes its terminating `CRLF`.
836fn header_value<'l>(line: &'l [u8], name: &[u8]) -> Option<&'l [u8]> {
837 let at = line.iter().position(|b| *b == b':')?;
838 let (candidate, rest) = line.split_at(at);
839 if candidate.len() != name.len()
840 || !candidate
841 .iter()
842 .zip(name)
843 .all(|(a, b)| a.to_ascii_lowercase() == *b)
844 {
845 return None;
846 }
847 Some(rest[1..].trim_ascii())
848}
849
850/// Whether a part's `Content-Type` header value marks it a typed error part,
851/// i.e. its media type is `application/problem+json`. `None` (no `Content-Type`
852/// on this line) is not a match. Parameters after `;` are ignored and the media
853/// type is compared case-insensitively, per RFC 7231.
854fn is_problem_content_type(value: Option<&[u8]>) -> bool {
855 let Some(value) = value else { return false };
856 let media_type = match value.iter().position(|b| *b == b';') {
857 Some(at) => &value[..at],
858 None => value,
859 }
860 .trim_ascii();
861 media_type.eq_ignore_ascii_case(PROBLEM_MEDIA_TYPE)
862}
863
864/// Parse a part's `Content-Length`, rejecting a value the accumulation guard
865/// would not let us buffer anyway. `Err` is the framing error, carrying the
866/// underlying `Utf8Error`/`ParseIntError` as its `source()` and echoing the
867/// offending value only after truncating and escaping it.
868fn parse_content_length(raw: &[u8]) -> Result<usize, TransportError> {
869 let text = std::str::from_utf8(raw)
870 .map_err(|e| framing_error_sourced("part `Content-Length` is not valid UTF-8", e))?;
871 let len: usize = text.parse().map_err(|e| {
872 framing_error_sourced(
873 format!(
874 "part `Content-Length` is not a byte count: `{}`",
875 display_value(text)
876 ),
877 e,
878 )
879 })?;
880 if len > MAX_ACCUMULATED_BYTES {
881 return Err(framing_error(format!(
882 "part `Content-Length` of {len} exceeds maximum accumulated size ({MAX_ACCUMULATED_BYTES} bytes)"
883 )));
884 }
885 Ok(len)
886}
887
888#[cfg(test)]
889#[cfg_attr(coverage_nightly, coverage(off))]
890#[allow(clippy::unwrap_used)]
891#[path = "multipart_tests.rs"]
892mod tests;