Skip to main content

form_net/
error.rs

1//! The error type: one `Copy` enum for the whole crate, its reason enums,
2//! and where an error was found.
3//!
4//! No error ever holds input bytes, a field value, a file name, or a path, so
5//! every error is safe to log.
6
7use core::fmt;
8
9use crate::hook::Refusal;
10
11/// Everything that can go wrong in form-net.
12///
13/// Input errors (`Limit`, `Syntax`, `Encoding`, `Policy`) carry a
14/// [`Location`]; caller mistakes (`InvalidLimits`, `BufferTooSmall`,
15/// `Poisoned`) and environment failures (`Io`, `Random`) do not.
16/// [`status_hint`](Self::status_hint) maps every error to an HTTP status and
17/// [`code`](Self::code) to a stable name.
18///
19/// # Examples
20///
21/// ```
22/// use form_net::{FormError, Limit};
23///
24/// let err = form_net::parse_urlencoded(&vec![b'a'; 11 << 20]).unwrap_err();
25/// assert!(matches!(err, FormError::Limit { limit: Limit::Body, .. }));
26/// assert_eq!((err.status_hint(), err.code()), (413, "limit.body"));
27/// ```
28#[non_exhaustive]
29#[derive(Clone, Copy, Debug, PartialEq, Eq)]
30pub enum FormError {
31    /// A budget of [`Limits`](crate::Limits) was exceeded.
32    Limit {
33        /// Which limit.
34        limit: Limit,
35        /// Where.
36        at: Location,
37    },
38    /// The input breaks the grammar.
39    Syntax {
40        /// What was wrong.
41        reason: SyntaxError,
42        /// Where.
43        at: Location,
44    },
45    /// Bytes that do not decode: percent escapes, UTF-8, charsets.
46    Encoding {
47        /// What was wrong.
48        reason: EncodingError,
49        /// Where.
50        at: Location,
51    },
52    /// The input is well formed but a [`Policy`](crate::Policy) refuses it.
53    Policy {
54        /// Which rule.
55        reason: PolicyRefusal,
56        /// Where.
57        at: Location,
58    },
59    /// An inspector refused the input.
60    Refused {
61        /// The inspector's status and code.
62        refusal: Refusal,
63        /// Where.
64        at: Location,
65    },
66    /// Typed extraction failed.
67    Extract {
68        /// What was wrong.
69        reason: ExtractError,
70        /// The Rust-side field name, when there is one.
71        field: Option<&'static str>,
72    },
73    /// The content type is not one form-net parses.
74    UnsupportedMediaType,
75    /// [`Limits::validate`](crate::Limits::validate) refused the
76    /// configuration.
77    InvalidLimits {
78        /// The field it refused.
79        limit: Limit,
80    },
81    /// A buffer the caller provided is too short.
82    BufferTooSmall {
83        /// The length it must have.
84        needed: usize,
85    },
86    /// The reader, the writer, or the file system failed.
87    #[cfg(feature = "std")]
88    #[cfg_attr(docsrs, doc(cfg(feature = "std")))]
89    Io {
90        /// The kind of failure; never a path or an OS message.
91        kind: std::io::ErrorKind,
92    },
93    /// The operating system's random source failed.
94    #[cfg(feature = "std")]
95    #[cfg_attr(docsrs, doc(cfg(feature = "std")))]
96    Random,
97    /// A parser was called again after an error, without `reset`.
98    Poisoned,
99}
100
101impl FormError {
102    pub(crate) const fn limit(limit: Limit, at: Location) -> Self {
103        Self::Limit { limit, at }
104    }
105
106    pub(crate) const fn syntax(reason: SyntaxError, at: Location) -> Self {
107        Self::Syntax { reason, at }
108    }
109
110    pub(crate) const fn encoding(reason: EncodingError, at: Location) -> Self {
111        Self::Encoding { reason, at }
112    }
113
114    pub(crate) const fn policy(reason: PolicyRefusal, at: Location) -> Self {
115        Self::Policy { reason, at }
116    }
117
118    /// The same error with its location moved `by` bytes later, for errors
119    /// found inside a component that starts at `by`.
120    #[must_use]
121    pub(crate) const fn shifted(self, by: u64) -> Self {
122        match self {
123            Self::Limit { limit, at } => Self::Limit {
124                limit,
125                at: at.shifted(by),
126            },
127            Self::Syntax { reason, at } => Self::Syntax {
128                reason,
129                at: at.shifted(by),
130            },
131            Self::Encoding { reason, at } => Self::Encoding {
132                reason,
133                at: at.shifted(by),
134            },
135            Self::Policy { reason, at } => Self::Policy {
136                reason,
137                at: at.shifted(by),
138            },
139            Self::Refused { refusal, at } => Self::Refused {
140                refusal,
141                at: at.shifted(by),
142            },
143            other => other,
144        }
145    }
146
147    /// The HTTP status a server should answer with.
148    ///
149    /// 413 for every limit, 400 for syntax and most encoding and policy
150    /// errors, 415 for unsupported media types, charsets, transfer encodings,
151    /// and nested multipart, 422 for extraction errors, 503 when the shared
152    /// spill budget is full, 500 for caller mistakes and environment
153    /// failures, and the inspector's own status for a refusal (400 if that
154    /// status is outside 400 to 599).
155    ///
156    /// # Examples
157    ///
158    /// ```
159    /// use form_net::FormError;
160    ///
161    /// assert_eq!(FormError::UnsupportedMediaType.status_hint(), 415);
162    /// assert_eq!(FormError::Poisoned.status_hint(), 500);
163    /// ```
164    #[must_use]
165    pub const fn status_hint(&self) -> u16 {
166        match self {
167            Self::Limit { .. } => 413,
168            Self::Syntax { .. } => 400,
169            Self::Encoding { reason, .. } => match reason {
170                EncodingError::UnsupportedCharset => 415,
171                _ => 400,
172            },
173            Self::Policy { reason, .. } => match reason {
174                PolicyRefusal::TransferEncoding | PolicyRefusal::NestedMultipart => 415,
175                _ => 400,
176            },
177            Self::Refused { refusal, .. } => {
178                if refusal.status >= 400 && refusal.status <= 599 {
179                    refusal.status
180                } else {
181                    400
182                }
183            }
184            Self::Extract { .. } => 422,
185            Self::UnsupportedMediaType => 415,
186            Self::InvalidLimits { .. } | Self::BufferTooSmall { .. } | Self::Poisoned => 500,
187            #[cfg(feature = "std")]
188            Self::Io { .. } | Self::Random => 500,
189        }
190    }
191
192    /// A stable, machine-readable name for the error: the variant and the
193    /// reason in snake case, joined with a dot (`limit.parts`,
194    /// `syntax.boundary_in_content`). A refusal returns the inspector's own
195    /// [`Refusal::code`]. Codes never change meaning after 1.0.
196    ///
197    /// # Examples
198    ///
199    /// ```
200    /// use form_net::FormError;
201    ///
202    /// assert_eq!(FormError::UnsupportedMediaType.code(), "unsupported_media_type");
203    /// let err = form_net::parse_urlencoded(b"a=%zz").unwrap_err();
204    /// assert_eq!(err.code(), "encoding.invalid_percent");
205    /// ```
206    #[must_use]
207    pub const fn code(&self) -> &'static str {
208        match self {
209            Self::Limit { limit, .. } => limit.code(),
210            Self::Syntax { reason, .. } => reason.code(),
211            Self::Encoding { reason, .. } => reason.code(),
212            Self::Policy { reason, .. } => reason.code(),
213            Self::Refused { refusal, .. } => refusal.code,
214            Self::Extract { reason, .. } => reason.code(),
215            Self::UnsupportedMediaType => "unsupported_media_type",
216            Self::InvalidLimits { .. } => "invalid_limits",
217            Self::BufferTooSmall { .. } => "buffer_too_small",
218            #[cfg(feature = "std")]
219            Self::Io { .. } => "io",
220            #[cfg(feature = "std")]
221            Self::Random => "random",
222            Self::Poisoned => "poisoned",
223        }
224    }
225
226    /// Where the error was found, for input errors and refusals.
227    ///
228    /// # Examples
229    ///
230    /// ```
231    /// let err = form_net::parse_urlencoded(b"ok=1&bad=%zz").unwrap_err();
232    /// assert_eq!(err.location().map(|l| l.offset), Some(9));
233    /// assert_eq!(form_net::FormError::Poisoned.location(), None);
234    /// ```
235    #[must_use]
236    pub const fn location(&self) -> Option<Location> {
237        match self {
238            Self::Limit { at, .. }
239            | Self::Syntax { at, .. }
240            | Self::Encoding { at, .. }
241            | Self::Policy { at, .. }
242            | Self::Refused { at, .. } => Some(*at),
243            _ => None,
244        }
245    }
246}
247
248impl fmt::Display for FormError {
249    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
250        match self {
251            Self::Limit { limit, at } => {
252                write!(f, "{} ({at}): {}", limit.describe(), limit.action())
253            }
254            Self::Syntax { reason, at } => write!(f, "{} ({at})", reason.describe()),
255            Self::Encoding { reason, at } => write!(f, "{} ({at})", reason.describe()),
256            Self::Policy { reason, at } => write!(f, "{} ({at})", reason.describe()),
257            Self::Refused { refusal, at } => {
258                write!(f, "refused by the inspector: {} ({at})", refusal.code)
259            }
260            Self::Extract { reason, field } => match field {
261                Some(name) => write!(f, "{} (field `{name}`)", reason.describe()),
262                None => f.write_str(reason.describe()),
263            },
264            Self::UnsupportedMediaType => f.write_str(
265                "unsupported media type: only application/x-www-form-urlencoded and multipart/form-data are parsed",
266            ),
267            Self::InvalidLimits { limit } => write!(
268                f,
269                "invalid limits: {} cannot be satisfied by any body; fix the configuration",
270                limit.field_name()
271            ),
272            Self::BufferTooSmall { needed } => write!(
273                f,
274                "buffer too small: {needed} bytes needed; size it with the Limits buffer functions"
275            ),
276            #[cfg(feature = "std")]
277            Self::Io { kind } => write!(f, "I/O failure: {kind}"),
278            #[cfg(feature = "std")]
279            Self::Random => f.write_str(
280                "the operating system's random source failed; no predictable fallback is used",
281            ),
282            Self::Poisoned => {
283                f.write_str("parser used after an error; call reset before parsing again")
284            }
285        }
286    }
287}
288
289impl core::error::Error for FormError {}
290
291/// Where an error was found.
292///
293/// # Examples
294///
295/// ```
296/// let err = form_net::parse_urlencoded(b"a=1&b=%").unwrap_err();
297/// let at = err.location().unwrap();
298/// assert_eq!((at.part, at.offset), (None, 6));
299/// ```
300#[non_exhaustive]
301#[derive(Clone, Copy, Debug, PartialEq, Eq)]
302pub struct Location {
303    /// The multipart part index (from 0), when the error is inside a part.
304    pub part: Option<u32>,
305    /// The byte offset in the body (or in the component, for `decode_into`).
306    pub offset: u64,
307}
308
309impl Location {
310    /// No location: configuration errors.
311    pub const NONE: Location = Location {
312        part: None,
313        offset: 0,
314    };
315
316    pub(crate) const fn at(offset: u64) -> Self {
317        Self { part: None, offset }
318    }
319
320    pub(crate) const fn in_part(part: Option<u32>, offset: u64) -> Self {
321        Self { part, offset }
322    }
323
324    const fn shifted(self, by: u64) -> Self {
325        Self {
326            part: self.part,
327            offset: self.offset.saturating_add(by),
328        }
329    }
330}
331
332impl fmt::Display for Location {
333    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
334        match self.part {
335            Some(part) => write!(f, "part {part}, byte {}", self.offset),
336            None => write!(f, "byte {}", self.offset),
337        }
338    }
339}
340
341/// Which budget of [`Limits`](crate::Limits) was exceeded.
342#[non_exhaustive]
343#[derive(Clone, Copy, Debug, PartialEq, Eq)]
344pub enum Limit {
345    /// `max_body_bytes`.
346    Body,
347    /// `max_fields`.
348    Fields,
349    /// `max_name_bytes`.
350    NameLength,
351    /// `max_value_bytes`.
352    ValueLength,
353    /// `max_text_bytes`.
354    TextTotal,
355    /// `max_parts`.
356    Parts,
357    /// `max_files`.
358    Files,
359    /// `max_file_bytes`.
360    FileSize,
361    /// `max_part_header_bytes`.
362    PartHeaderBytes,
363    /// `max_part_headers`.
364    PartHeaderCount,
365    /// `max_filename_bytes`.
366    FilenameLength,
367    /// `max_preamble_bytes`.
368    Preamble,
369    /// `max_epilogue_bytes`.
370    Epilogue,
371    /// `max_padding_bytes`.
372    Padding,
373    /// `max_token_bytes`.
374    TokenLength,
375    /// The per-request disk limit of a store.
376    Disk,
377}
378
379impl Limit {
380    const fn code(self) -> &'static str {
381        match self {
382            Self::Body => "limit.body",
383            Self::Fields => "limit.fields",
384            Self::NameLength => "limit.name_length",
385            Self::ValueLength => "limit.value_length",
386            Self::TextTotal => "limit.text_total",
387            Self::Parts => "limit.parts",
388            Self::Files => "limit.files",
389            Self::FileSize => "limit.file_size",
390            Self::PartHeaderBytes => "limit.part_header_bytes",
391            Self::PartHeaderCount => "limit.part_header_count",
392            Self::FilenameLength => "limit.filename_length",
393            Self::Preamble => "limit.preamble",
394            Self::Epilogue => "limit.epilogue",
395            Self::Padding => "limit.padding",
396            Self::TokenLength => "limit.token_length",
397            Self::Disk => "limit.disk",
398        }
399    }
400
401    const fn field_name(self) -> &'static str {
402        match self {
403            Self::Body => "max_body_bytes",
404            Self::Fields => "max_fields",
405            Self::NameLength => "max_name_bytes",
406            Self::ValueLength => "max_value_bytes",
407            Self::TextTotal => "max_text_bytes",
408            Self::Parts => "max_parts",
409            Self::Files => "max_files",
410            Self::FileSize => "max_file_bytes",
411            Self::PartHeaderBytes => "max_part_header_bytes",
412            Self::PartHeaderCount => "max_part_headers",
413            Self::FilenameLength => "max_filename_bytes",
414            Self::Preamble => "max_preamble_bytes",
415            Self::Epilogue => "max_epilogue_bytes",
416            Self::Padding => "max_padding_bytes",
417            Self::TokenLength => "max_token_bytes",
418            Self::Disk => "max_disk_bytes",
419        }
420    }
421
422    const fn describe(self) -> &'static str {
423        match self {
424            Self::Body => "body too large",
425            Self::Fields => "too many fields",
426            Self::NameLength => "field name too long",
427            Self::ValueLength => "field value too long",
428            Self::TextTotal => "text fields too large in total",
429            Self::Parts => "too many parts",
430            Self::Files => "too many files",
431            Self::FileSize => "file too large",
432            Self::PartHeaderBytes => "part headers too large",
433            Self::PartHeaderCount => "too many part header lines",
434            Self::FilenameLength => "file name too long",
435            Self::Preamble => "too many bytes before the first boundary",
436            Self::Epilogue => "too many bytes after the last boundary",
437            Self::Padding => "too much padding after a boundary",
438            Self::TokenLength => "anti-forgery token too long",
439            Self::Disk => "too many bytes stored on disk for one request",
440        }
441    }
442
443    const fn action(self) -> &'static str {
444        match self {
445            Self::Body => "raise max_body_bytes if this route accepts larger bodies",
446            Self::Fields => "raise max_fields if this route accepts more fields",
447            Self::NameLength => "raise max_name_bytes if this route uses longer names",
448            Self::ValueLength => "raise max_value_bytes, or use a file input for large text",
449            Self::TextTotal => "raise max_text_bytes if this route accepts more text",
450            Self::Parts => "raise max_parts if this route accepts more parts",
451            Self::Files => "raise max_files if this route accepts more files",
452            Self::FileSize => "raise max_file_bytes if this route accepts larger files",
453            Self::PartHeaderBytes => "raise max_part_header_bytes if real clients need it",
454            Self::PartHeaderCount => "raise max_part_headers if real clients need it",
455            Self::FilenameLength => "raise max_filename_bytes if real clients need it",
456            Self::Preamble | Self::Epilogue => "the boundary is probably wrong",
457            Self::Padding => "the body is not what browsers send",
458            Self::TokenLength => "treat the request as forged",
459            Self::Disk => "raise the store's max_disk_bytes if this route accepts more",
460        }
461    }
462}
463
464/// What part of the grammar the input broke.
465#[non_exhaustive]
466#[derive(Clone, Copy, Debug, PartialEq, Eq)]
467pub enum SyntaxError {
468    /// A `Content-Type` value (of the request or of a part) is malformed.
469    InvalidContentType,
470    /// `multipart/form-data` without a `boundary` parameter.
471    MissingBoundary,
472    /// Two `boundary` parameters.
473    DuplicateBoundary,
474    /// A boundary that is empty, longer than 70 bytes, has a byte outside
475    /// RFC 2046 `bchars`, or ends with a space.
476    InvalidBoundary,
477    /// The body ended without any delimiter.
478    NoInitialDelimiter,
479    /// A full delimiter followed by something other than `--`, padding, or
480    /// CRLF; or writer data that contains the delimiter.
481    BoundaryInContent,
482    /// LF without CR where CRLF is required.
483    BareLineFeed,
484    /// A part header line without a colon.
485    MalformedHeaderLine,
486    /// A folded part header line.
487    HeaderFolding,
488    /// A part header name that is not a token, or has whitespace before the
489    /// colon.
490    InvalidHeaderName,
491    /// A control byte other than HTAB in a part header value.
492    ControlInHeader,
493    /// A part without `Content-Disposition`.
494    MissingContentDisposition,
495    /// A known part header given twice.
496    DuplicateHeader,
497    /// A `Content-Disposition` type other than `form-data`.
498    NotFormData,
499    /// A `Content-Disposition` without `name`.
500    MissingName,
501    /// An empty `name`.
502    EmptyName,
503    /// A parameter given twice.
504    DuplicateParameter,
505    /// Malformed parameters (or more than 16 on one header).
506    MalformedParameter,
507    /// A quoted parameter value that contains `\"`.
508    AmbiguousQuote,
509    /// The body ended before the close delimiter.
510    Truncated,
511}
512
513impl SyntaxError {
514    const fn code(self) -> &'static str {
515        match self {
516            Self::InvalidContentType => "syntax.invalid_content_type",
517            Self::MissingBoundary => "syntax.missing_boundary",
518            Self::DuplicateBoundary => "syntax.duplicate_boundary",
519            Self::InvalidBoundary => "syntax.invalid_boundary",
520            Self::NoInitialDelimiter => "syntax.no_initial_delimiter",
521            Self::BoundaryInContent => "syntax.boundary_in_content",
522            Self::BareLineFeed => "syntax.bare_line_feed",
523            Self::MalformedHeaderLine => "syntax.malformed_header_line",
524            Self::HeaderFolding => "syntax.header_folding",
525            Self::InvalidHeaderName => "syntax.invalid_header_name",
526            Self::ControlInHeader => "syntax.control_in_header",
527            Self::MissingContentDisposition => "syntax.missing_content_disposition",
528            Self::DuplicateHeader => "syntax.duplicate_header",
529            Self::NotFormData => "syntax.not_form_data",
530            Self::MissingName => "syntax.missing_name",
531            Self::EmptyName => "syntax.empty_name",
532            Self::DuplicateParameter => "syntax.duplicate_parameter",
533            Self::MalformedParameter => "syntax.malformed_parameter",
534            Self::AmbiguousQuote => "syntax.ambiguous_quote",
535            Self::Truncated => "syntax.truncated",
536        }
537    }
538
539    const fn describe(self) -> &'static str {
540        match self {
541            Self::InvalidContentType => "malformed Content-Type",
542            Self::MissingBoundary => "multipart/form-data without a boundary",
543            Self::DuplicateBoundary => "two boundary parameters",
544            Self::InvalidBoundary => "invalid multipart boundary",
545            Self::NoInitialDelimiter => {
546                "no multipart delimiter found: the body is not multipart or the boundary is wrong"
547            }
548            Self::BoundaryInContent => "the boundary appears inside a part",
549            Self::BareLineFeed => "LF without CR where CRLF is required",
550            Self::MalformedHeaderLine => "malformed part header line",
551            Self::HeaderFolding => "folded part header line",
552            Self::InvalidHeaderName => "invalid part header name",
553            Self::ControlInHeader => "control byte in a part header",
554            Self::MissingContentDisposition => "part without Content-Disposition",
555            Self::DuplicateHeader => "part header given twice",
556            Self::NotFormData => "Content-Disposition type is not form-data",
557            Self::MissingName => "part without a name",
558            Self::EmptyName => "part with an empty name",
559            Self::DuplicateParameter => "parameter given twice",
560            Self::MalformedParameter => "malformed header parameter",
561            Self::AmbiguousQuote => "quoted value contains \\\"",
562            Self::Truncated => "body ended before the closing boundary",
563        }
564    }
565}
566
567/// Bytes that do not decode.
568#[non_exhaustive]
569#[derive(Clone, Copy, Debug, PartialEq, Eq)]
570pub enum EncodingError {
571    /// `%` not followed by two hex digits, under `PercentPolicy::Refuse`.
572    InvalidPercent,
573    /// Text that is not valid UTF-8.
574    InvalidUtf8,
575    /// A charset other than UTF-8 or US-ASCII.
576    UnsupportedCharset,
577    /// A malformed `filename*`.
578    InvalidExtendedValue,
579}
580
581impl EncodingError {
582    const fn code(self) -> &'static str {
583        match self {
584            Self::InvalidPercent => "encoding.invalid_percent",
585            Self::InvalidUtf8 => "encoding.invalid_utf8",
586            Self::UnsupportedCharset => "encoding.unsupported_charset",
587            Self::InvalidExtendedValue => "encoding.invalid_extended_value",
588        }
589    }
590
591    const fn describe(self) -> &'static str {
592        match self {
593            Self::InvalidPercent => "invalid percent escape",
594            Self::InvalidUtf8 => "text is not valid UTF-8",
595            Self::UnsupportedCharset => "unsupported charset: only UTF-8 and US-ASCII are accepted",
596            Self::InvalidExtendedValue => "malformed filename*",
597        }
598    }
599}
600
601/// Well-formed input that a [`Policy`](crate::Policy) refuses.
602#[non_exhaustive]
603#[derive(Clone, Copy, Debug, PartialEq, Eq)]
604pub enum PolicyRefusal {
605    /// A `Content-Transfer-Encoding` other than `7bit`, `8bit`, `binary`.
606    TransferEncoding,
607    /// A part declared `multipart/*`.
608    NestedMultipart,
609    /// An unknown part header under `HeaderPolicy::Refuse`.
610    UnknownHeader,
611    /// Both `filename` and `filename*` in one part.
612    BothFilenames,
613    /// `filename*` under `ExtendedFilenamePolicy::Refuse`.
614    ExtendedFilename,
615    /// A file part before the token field under
616    /// `TokenPlacement::BeforeFirstFile`.
617    TokenFieldLate,
618    /// The token field given twice.
619    DuplicateToken,
620}
621
622impl PolicyRefusal {
623    const fn code(self) -> &'static str {
624        match self {
625            Self::TransferEncoding => "policy.transfer_encoding",
626            Self::NestedMultipart => "policy.nested_multipart",
627            Self::UnknownHeader => "policy.unknown_header",
628            Self::BothFilenames => "policy.both_filenames",
629            Self::ExtendedFilename => "policy.extended_filename",
630            Self::TokenFieldLate => "policy.token_field_late",
631            Self::DuplicateToken => "policy.duplicate_token",
632        }
633    }
634
635    const fn describe(self) -> &'static str {
636        match self {
637            Self::TransferEncoding => "Content-Transfer-Encoding other than identity",
638            Self::NestedMultipart => "nested multipart part",
639            Self::UnknownHeader => "unknown part header",
640            Self::BothFilenames => "both filename and filename*",
641            Self::ExtendedFilename => "filename* is not accepted",
642            Self::TokenFieldLate => {
643                "file part before the anti-forgery token; treat as a possible forgery"
644            }
645            Self::DuplicateToken => "anti-forgery token given twice; treat as a possible forgery",
646        }
647    }
648}
649
650/// Why typed extraction failed.
651#[non_exhaustive]
652#[derive(Clone, Copy, Debug, PartialEq, Eq)]
653pub enum ExtractError {
654    /// A name the struct neither declares nor ignores.
655    Unknown,
656    /// A required field is absent.
657    Missing,
658    /// A single-valued field was given twice.
659    Duplicate,
660    /// A value that does not convert; the text says why.
661    Invalid(&'static str),
662    /// A file where text was expected, or the reverse.
663    WrongKind,
664}
665
666impl ExtractError {
667    const fn code(self) -> &'static str {
668        match self {
669            Self::Unknown => "extract.unknown",
670            Self::Missing => "extract.missing",
671            Self::Duplicate => "extract.duplicate",
672            Self::Invalid(_) => "extract.invalid",
673            Self::WrongKind => "extract.wrong_kind",
674        }
675    }
676
677    const fn describe(self) -> &'static str {
678        match self {
679            Self::Unknown => "unknown field",
680            Self::Missing => "missing field",
681            Self::Duplicate => "field given more than once",
682            Self::Invalid(why) => why,
683            Self::WrongKind => "file where text was expected, or text where a file was expected",
684        }
685    }
686}
687
688#[cfg(test)]
689mod tests {
690    use super::*;
691    use alloc::string::ToString;
692
693    #[test]
694    fn test_status_hint_every_family_maps() {
695        let at = Location::NONE;
696        assert_eq!(FormError::limit(Limit::Parts, at).status_hint(), 413);
697        assert_eq!(
698            FormError::syntax(SyntaxError::Truncated, at).status_hint(),
699            400
700        );
701        assert_eq!(
702            FormError::encoding(EncodingError::UnsupportedCharset, at).status_hint(),
703            415
704        );
705        assert_eq!(
706            FormError::encoding(EncodingError::InvalidUtf8, at).status_hint(),
707            400
708        );
709        assert_eq!(
710            FormError::policy(PolicyRefusal::NestedMultipart, at).status_hint(),
711            415
712        );
713        assert_eq!(
714            FormError::policy(PolicyRefusal::TokenFieldLate, at).status_hint(),
715            400
716        );
717        let extract = FormError::Extract {
718            reason: ExtractError::Missing,
719            field: Some("email"),
720        };
721        assert_eq!(extract.status_hint(), 422);
722        assert_eq!(
723            FormError::InvalidLimits { limit: Limit::Body }.status_hint(),
724            500
725        );
726        assert_eq!(FormError::BufferTooSmall { needed: 3 }.status_hint(), 500);
727    }
728
729    #[test]
730    fn test_status_hint_refusal_out_of_range_reports_400() {
731        let at = Location::NONE;
732        let ok = FormError::Refused {
733            refusal: Refusal::new(422, "x"),
734            at,
735        };
736        assert_eq!(ok.status_hint(), 422);
737        for status in [0, 200, 399, 600, 999] {
738            let e = FormError::Refused {
739                refusal: Refusal::new(status, "x"),
740                at,
741            };
742            assert_eq!(e.status_hint(), 400);
743        }
744    }
745
746    #[test]
747    fn test_code_refusal_returns_inspector_code() {
748        let e = FormError::Refused {
749            refusal: Refusal::new(422, "customs.unexpected_field"),
750            at: Location::NONE,
751        };
752        assert_eq!(e.code(), "customs.unexpected_field");
753    }
754
755    #[test]
756    fn test_display_never_contains_the_field_value() {
757        let e = FormError::Extract {
758            reason: ExtractError::Invalid("not a number"),
759            field: Some("age"),
760        };
761        assert_eq!(e.to_string(), "not a number (field `age`)");
762        let e = FormError::limit(Limit::Parts, Location::in_part(Some(3), 99));
763        assert_eq!(
764            e.to_string(),
765            "too many parts (part 3, byte 99): raise max_parts if this route accepts more parts"
766        );
767    }
768
769    #[test]
770    fn test_shifted_moves_input_errors_only() {
771        let e = FormError::encoding(EncodingError::InvalidPercent, Location::at(2)).shifted(10);
772        assert_eq!(e.location().map(|l| l.offset), Some(12));
773        assert_eq!(FormError::Poisoned.shifted(10), FormError::Poisoned);
774    }
775
776    #[test]
777    fn test_form_error_is_small_and_copy() {
778        fn is_copy<T: Copy>() {}
779        is_copy::<FormError>();
780        assert!(core::mem::size_of::<FormError>() <= 48);
781    }
782}