Skip to main content

s3s_multipart/
error.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: 2023-2026 The s3s Authors
3
4/// Errors produced while parsing a multipart body.
5///
6/// The enum is `#[non_exhaustive]`: more variants may be added in a minor
7/// release, so downstream `match` expressions need a wildcard arm. Existing
8/// variants stay constructible.
9#[derive(Debug, thiserror::Error)]
10#[non_exhaustive]
11pub enum Error {
12    /// The underlying stream returned an error.
13    #[error("stream read failed: {0}")]
14    StreamReadFailed(#[source] Box<dyn std::error::Error + Send + Sync>),
15    /// The supplied boundary is invalid.
16    #[error("invalid boundary")]
17    InvalidBoundary,
18    /// The body is not valid multipart data.
19    #[error("invalid multipart format")]
20    InvalidFormat,
21    /// The stream ended before the expected multipart structure.
22    #[error("incomplete multipart stream")]
23    IncompleteStream,
24    /// The accumulated part headers exceed the configured buffer limit.
25    #[error("part headers exceed the buffer limit of {limit} bytes")]
26    HeaderSizeExceeded {
27        /// The configured buffer limit that was exceeded.
28        limit: usize,
29    },
30    /// A [`FinalPartDataStream`](crate::FinalPartDataStream) found content
31    /// after the closing delimiter that is not the strict closing trailer
32    /// (an epilogue or another part).
33    #[error("content follows the closing delimiter of the taken part")]
34    StreamPartNotLast,
35    /// The multipart parser was already terminated by `take_data_stream`.
36    #[error("multipart stream has already been taken")]
37    StreamAlreadyTaken,
38    /// A taken part data stream ended before the closing delimiter.
39    #[error("incomplete streaming part")]
40    IncompleteStreamPart,
41}
42
43impl Error {
44    /// Wraps an error returned by the underlying stream.
45    ///
46    /// Equivalent to constructing [`Error::StreamReadFailed`] directly, with
47    /// the source boxed for you; convenient as a `map_err` function.
48    #[must_use]
49    pub fn stream_read_failed(source: impl Into<Box<dyn std::error::Error + Send + Sync>>) -> Self {
50        Self::StreamReadFailed(source.into())
51    }
52}
53
54#[cfg(test)]
55#[allow(clippy::expect_used, clippy::panic, clippy::unreachable, clippy::unwrap_used)]
56mod tests {
57    use std::error::Error as _;
58
59    use super::*;
60
61    #[test]
62    fn display_messages_are_stable() {
63        assert_eq!(Error::InvalidBoundary.to_string(), "invalid boundary");
64        assert_eq!(Error::InvalidFormat.to_string(), "invalid multipart format");
65        assert_eq!(Error::IncompleteStream.to_string(), "incomplete multipart stream");
66        assert_eq!(Error::IncompleteStreamPart.to_string(), "incomplete streaming part");
67        assert_eq!(
68            Error::StreamPartNotLast.to_string(),
69            "content follows the closing delimiter of the taken part"
70        );
71        assert_eq!(Error::StreamAlreadyTaken.to_string(), "multipart stream has already been taken");
72        assert_eq!(
73            Error::HeaderSizeExceeded { limit: 8192 }.to_string(),
74            "part headers exceed the buffer limit of 8192 bytes"
75        );
76        assert_eq!(
77            Error::StreamReadFailed(Box::new(std::io::Error::other("boom"))).to_string(),
78            "stream read failed: boom"
79        );
80    }
81
82    #[test]
83    fn only_stream_failures_expose_a_source() {
84        let err = Error::StreamReadFailed(Box::new(std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "eof")));
85        let kind = err
86            .source()
87            .and_then(|source| source.downcast_ref::<std::io::Error>())
88            .map(std::io::Error::kind);
89        assert_eq!(kind, Some(std::io::ErrorKind::UnexpectedEof));
90
91        for err in [
92            Error::InvalidBoundary,
93            Error::InvalidFormat,
94            Error::IncompleteStream,
95            Error::IncompleteStreamPart,
96            Error::StreamPartNotLast,
97            Error::StreamAlreadyTaken,
98            Error::HeaderSizeExceeded { limit: 1 },
99        ] {
100            assert!(err.source().is_none());
101        }
102    }
103
104    #[test]
105    fn error_is_send_sync_static() {
106        fn assert_bounds<T: std::error::Error + Send + Sync + 'static>() {}
107        assert_bounds::<Error>();
108    }
109}