base_web_content/content_error.rs
1// Released under the MIT License.
2// Copyright, 2026, by Samuel Williams.
3
4use base_web_multipart::{MultipartError, MultipartErrorKind};
5use base_web_url::{FormError, FormErrorKind};
6use std::fmt;
7
8/// Why content could not be parsed at all, with the offset in the body where the parser found it.
9///
10/// A fatal error stops parsing, and the parser reports the same error for every later call. It never includes submitted content, field values, filenames, media types, or names the declaration does not accept: only its kind and offset. Each kind maps to one HTTP status, from [`Self::status`].
11#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
12pub struct ContentError {
13 kind: ContentErrorKind,
14 offset: u64,
15}
16
17/// The kind of [`ContentError`].
18#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
19#[non_exhaustive]
20pub enum ContentErrorKind {
21 /// The request has no media type. 415 Unsupported Media Type.
22 MissingMediaType,
23 /// The request's media type is not a format this crate parses. 415 Unsupported Media Type.
24 UnsupportedMediaType,
25 /// The declaration does not accept the request's format. 415 Unsupported Media Type.
26 UnacceptedFormat,
27 /// An `application/x-www-form-urlencoded` media type declares a `charset` other than `UTF-8` or `US-ASCII`. 415 Unsupported Media Type.
28 UnsupportedCharset,
29 /// The body is longer than the largest the declaration could need, [`Content::body_size_limit`](crate::Content::body_size_limit). 413 Content Too Large.
30 BodySizeLimit,
31 /// The bytes before a `multipart/form-data` body's first boundary exceed the preamble limit. 413 Content Too Large.
32 PreambleSizeLimit,
33 /// The bytes after a `multipart/form-data` body's closing boundary line exceed the epilogue limit. 413 Content Too Large.
34 EpilogueSizeLimit,
35 /// A part's header section, with any padding after its boundary, exceeds the header section limit. 413 Content Too Large.
36 HeaderSizeLimit,
37 /// A part's header section has more fields than the header count limit. 413 Content Too Large.
38 HeaderCountLimit,
39 /// The body has more parts than the part count limit. 413 Content Too Large.
40 PartCountLimit,
41 /// An `application/x-www-form-urlencoded` body has more pairs than the pair count limit. 413 Content Too Large.
42 PairCountLimit,
43 /// A name has more components than the depth limit. 413 Content Too Large.
44 DepthLimit,
45 /// An array has more elements than its count limit. 413 Content Too Large.
46 CountLimit,
47 /// A field or upload has more content than its size limit. 413 Content Too Large.
48 FieldSizeLimit,
49 /// The fields and uploads together have more content than the total content limit. 413 Content Too Large.
50 ContentSizeLimit,
51 /// A name is not one the declaration accepts, including a name that is not `base`, `base[key]`, or `base[]`, repeated. 400 Bad Request.
52 UnknownName,
53 /// A `multipart/form-data` media type has no `boundary` parameter, or one RFC 2046 does not allow. 400 Bad Request.
54 InvalidBoundary,
55 /// A part's `Content-Disposition` is missing, is not `form-data`, or has no `name`. 400 Bad Request.
56 MissingName,
57 /// A part's header fields cannot be read: a header line is malformed or folded, a `Content-Disposition` or `Content-Type` is invalid or repeated, its parameters break the [`ParameterRules`](crate::ParameterRules), or its `Content-Transfer-Encoding` is not an identity encoding. 400 Bad Request.
58 InvalidPartHeader,
59 /// A part for a declared upload has an empty `filename`, which browsers send for a file input with no file chosen, yet has content. 400 Bad Request.
60 EmptyFilenameContent,
61 /// The content ended before it was complete: a multipart body before its closing boundary, or URL-encoded content whose end was not given to [`ContentParser::parse_end`](crate::ContentParser::parse_end). 400 Bad Request.
62 UnexpectedEnd,
63 /// The multipart framing is malformed, as `base-web-multipart` reports it, such as a boundary line that does not end with a line break, or a boundary after a bare CR or LF. 400 Bad Request.
64 MalformedMultipart,
65 /// URL-encoded content is malformed, as `base-web-url` reports it: a `%` not followed by two hexadecimal digits, or decoded text that is not UTF-8. 400 Bad Request.
66 MalformedUrlEncoded,
67}
68
69impl ContentErrorKind {
70 /// The HTTP status a server should respond with: 415, 413, or 400.
71 ///
72 /// ```
73 /// use base_web_content::ContentErrorKind;
74 ///
75 /// assert_eq!(ContentErrorKind::FieldSizeLimit.status(), 413);
76 /// ```
77 pub fn status(self) -> u16 {
78 match self {
79 Self::MissingMediaType
80 | Self::UnsupportedMediaType
81 | Self::UnacceptedFormat
82 | Self::UnsupportedCharset => 415,
83 Self::BodySizeLimit
84 | Self::PreambleSizeLimit
85 | Self::EpilogueSizeLimit
86 | Self::HeaderSizeLimit
87 | Self::HeaderCountLimit
88 | Self::PartCountLimit
89 | Self::PairCountLimit
90 | Self::DepthLimit
91 | Self::CountLimit
92 | Self::FieldSizeLimit
93 | Self::ContentSizeLimit => 413,
94 Self::UnknownName
95 | Self::InvalidBoundary
96 | Self::MissingName
97 | Self::InvalidPartHeader
98 | Self::EmptyFilenameContent
99 | Self::UnexpectedEnd
100 | Self::MalformedMultipart
101 | Self::MalformedUrlEncoded => 400,
102 }
103 }
104
105 fn description(self) -> &'static str {
106 match self {
107 Self::MissingMediaType => "content has no media type",
108 Self::UnsupportedMediaType => "content has an unsupported media type",
109 Self::UnacceptedFormat => "content is in a format the declaration does not accept",
110 Self::UnsupportedCharset => "URL-encoded content has an unsupported character set",
111 Self::PairCountLimit => "URL-encoded content exceeds its pair count limit",
112 Self::UnexpectedEnd => "content ends before it is complete",
113 Self::BodySizeLimit => "content exceeds the largest body its declaration accepts",
114 Self::PreambleSizeLimit => "multipart preamble exceeds its size limit",
115 Self::EpilogueSizeLimit => "multipart epilogue exceeds its size limit",
116 Self::HeaderSizeLimit => "multipart part header section exceeds its size limit",
117 Self::HeaderCountLimit => "multipart part header section exceeds its field count limit",
118 Self::PartCountLimit => "multipart body exceeds its part count limit",
119 Self::DepthLimit => "a name is deeper than its depth limit",
120 Self::CountLimit => "an array has more elements than its count limit",
121 Self::FieldSizeLimit => "a field exceeds its size limit",
122 Self::ContentSizeLimit => "content exceeds its total size limit",
123 Self::UnknownName => "a name is not declared",
124 Self::InvalidBoundary => "multipart media type has no valid boundary",
125 Self::MissingName => "multipart part has no form-data name",
126 Self::InvalidPartHeader => "multipart part has invalid header fields",
127 Self::EmptyFilenameContent => "multipart part with an empty filename has content",
128 Self::MalformedMultipart => "multipart body is malformed",
129 Self::MalformedUrlEncoded => "URL-encoded content is malformed",
130 }
131 }
132}
133
134impl ContentError {
135 pub(crate) fn new(kind: ContentErrorKind, offset: u64) -> Self {
136 Self { kind, offset }
137 }
138
139 /// What went wrong.
140 pub fn kind(&self) -> ContentErrorKind {
141 self.kind
142 }
143
144 /// The offset in the body, in bytes, where the parser found the problem: 0 for a problem with the media type; the start of a part's header section for a problem with the section, its size, its fields, or the part's name, which are decided when the section is complete; the start of the spaces and tabs after a boundary when they alone exceed the header section limit; the start of a URL-encoded name for a problem with the name, or the first byte beyond the longest it could be; and otherwise the first byte beyond a limit, or the first byte that is malformed.
145 pub fn offset(&self) -> u64 {
146 self.offset
147 }
148
149 /// The HTTP status a server should respond with, from the kind.
150 pub fn status(&self) -> u16 {
151 self.kind.status()
152 }
153}
154
155/// A `base-web-url` error, with the same offset: its pair count limit is this crate's, and a name longer than the longest the declaration accepts could make it is a name the declaration does not accept. Its other limits are wider than this crate's, which are reached first, and the rest are malformed content.
156pub(crate) fn from_form_error(error: FormError) -> ContentError {
157 let kind = match error.kind() {
158 FormErrorKind::PairCountLimit => ContentErrorKind::PairCountLimit,
159 FormErrorKind::NameLengthLimit => ContentErrorKind::UnknownName,
160 // The size and value length limits are this crate's body and field limits, which are reached first, and paths are read by the form, so only malformed content remains.
161 _ => ContentErrorKind::MalformedUrlEncoded,
162 };
163 ContentError::new(kind, error.offset())
164}
165
166/// A `base-web-multipart` error, with the same offset: its limits are this crate's, a malformed header line is a part header that cannot be read, and anything else is malformed framing.
167pub(crate) fn from_multipart_error(error: MultipartError) -> ContentError {
168 let kind = match error.kind() {
169 MultipartErrorKind::PreambleSizeLimit => ContentErrorKind::PreambleSizeLimit,
170 MultipartErrorKind::HeaderSizeLimit => ContentErrorKind::HeaderSizeLimit,
171 MultipartErrorKind::HeaderCountLimit => ContentErrorKind::HeaderCountLimit,
172 MultipartErrorKind::PartCountLimit => ContentErrorKind::PartCountLimit,
173 MultipartErrorKind::InvalidField | MultipartErrorKind::FoldedField => {
174 ContentErrorKind::InvalidPartHeader
175 }
176 MultipartErrorKind::UnexpectedEnd => ContentErrorKind::UnexpectedEnd,
177 _ => ContentErrorKind::MalformedMultipart,
178 };
179 ContentError::new(kind, error.offset())
180}
181
182impl fmt::Display for ContentError {
183 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
184 write!(
185 formatter,
186 "{} at byte {}",
187 self.kind.description(),
188 self.offset
189 )
190 }
191}
192
193impl std::error::Error for ContentError {}