pavex/request/path/
errors.rs

1//! Errors that can happen when extracting path parameters.
2use std::str::Utf8Error;
3
4use pavex_macros::methods;
5
6use crate::Response;
7
8/// The error returned by [`PathParams::extract`] when the extraction fails.
9///
10/// See [`PathParams::extract`] and the documentation of each error variant for more details.
11///
12/// Pavex provides [`ExtractPathParamsError::into_response`] as the default error handler for
13/// this failure.
14///
15/// [`PathParams::extract`]: crate::request::path::PathParams::extract
16#[derive(Debug, thiserror::Error)]
17#[non_exhaustive]
18pub enum ExtractPathParamsError {
19    #[error(transparent)]
20    /// See [`InvalidUtf8InPathParam`] for details.
21    InvalidUtf8InPathParameter(InvalidUtf8InPathParam),
22    #[error(transparent)]
23    /// See [`PathDeserializationError`] for details.
24    PathDeserializationError(PathDeserializationError),
25}
26
27#[derive(Debug, thiserror::Error)]
28#[non_exhaustive]
29/// One of the percent-decoded path parameters is not a valid UTF8 string.
30///
31/// URL parameters must be percent-encoded whenever they contain characters that are not
32/// URL safe—e.g. whitespaces.
33///
34/// Pavex automatically percent-decodes URL parameters before trying to deserialize them
35/// in [`PathParams<T>`].
36/// This error is returned whenever the percent-decoding step fails—i.e. the decoded data is not a
37/// valid UTF8 string.
38///
39/// # Example
40///
41/// One of our routes is `/address/{address_id}`.
42/// We receive a request with `/address/the%20street` as path—`address_id` is set to
43/// `the%20street` and Pavex automatically decodes it into `the street`.
44///
45/// We could also receive a request using `/address/dirty%DE~%C7%1FY` as path—`address_id`, when
46/// decoded, is a sequence of bytes that cannot be interpreted as a well-formed UTF8 string.
47/// This error is then returned.
48///
49/// [`PathParams<T>`]: struct@crate::request::path::PathParams
50#[error(
51    "`{invalid_raw_segment}` cannot be used as `{invalid_key}` \
52since it is not a well-formed UTF8 string when percent-decoded"
53)]
54pub struct InvalidUtf8InPathParam {
55    pub(super) invalid_key: String,
56    pub(super) invalid_raw_segment: String,
57    #[source]
58    pub(super) source: Utf8Error,
59}
60
61/// The error returned by [`EncodedParamValue::decode`] when the percent-decoded path parameter
62/// is not a valid UTF8 string.
63///
64/// Path parameters must be percent-encoded whenever they contain characters that are not
65/// URL safe—e.g. whitespaces.
66/// This error is returned whenever the percent-decoding step fails—i.e. the decoded data is not a
67/// valid UTF8 string.
68///
69/// # Example
70///
71/// You might try to percent-decode `dirty%DE~%C7%1FY`.
72/// When decoded, it is a sequence of bytes that cannot be interpreted as a well-formed UTF8 string.
73/// This error is then returned.
74///
75/// [`EncodedParamValue::decode`]: super::EncodedParamValue::decode
76#[derive(Debug, thiserror::Error)]
77#[error("`{invalid_raw_segment}` is not a well-formed UTF8 string when percent-decoded")]
78pub struct DecodeError {
79    pub(super) invalid_raw_segment: String,
80    #[source]
81    pub(super) source: Utf8Error,
82}
83
84#[methods]
85impl ExtractPathParamsError {
86    /// Convert an [`ExtractPathParamsError`] into an HTTP response.
87    ///
88    /// It returns a `500 Internal Server Error` to the caller if the failure was caused by a
89    /// programmer error (e.g. `T` in [`PathParams<T>`] is an unsupported type).
90    /// It returns a `400 Bad Request` for all other cases.
91    ///
92    /// [`PathParams<T>`]: struct@crate::request::path::PathParams
93    #[error_handler(pavex = crate)]
94    pub fn into_response(&self) -> Response {
95        match self {
96            ExtractPathParamsError::InvalidUtf8InPathParameter(e) => {
97                Response::bad_request().set_typed_body(format!("Invalid URL.\n{e}"))
98            }
99            ExtractPathParamsError::PathDeserializationError(e) => match e.kind {
100                ErrorKind::ParseErrorAtKey { .. } | ErrorKind::ParseError { .. } => {
101                    Response::bad_request().set_typed_body(format!("Invalid URL.\n{}", e.kind))
102                }
103                // We put the "custom" message variant here as well because it's not clear
104                // whether it's a programmer error or not. We err on the side of safety and
105                // prefer to return a 500 with an opaque error message.
106                ErrorKind::Message(_) | ErrorKind::UnsupportedType { .. } => {
107                    Response::internal_server_error()
108                        .set_typed_body("Something went wrong when trying to process the request")
109                }
110            },
111        }
112    }
113}
114
115#[derive(Debug)]
116/// Something went wrong when trying to deserialize the percent-decoded URL parameters into
117/// the target type you specified—`T` in [`PathParams<T>`].
118///
119/// You can use [`PathDeserializationError::kind`] to get more details about the error.
120///
121/// [`PathParams<T>`]: struct@crate::request::path::PathParams
122pub struct PathDeserializationError {
123    pub(super) kind: ErrorKind,
124}
125
126impl PathDeserializationError {
127    pub(super) fn new(kind: ErrorKind) -> Self {
128        Self { kind }
129    }
130
131    /// Retrieve the details of the error that occurred while trying to deserialize the URL
132    /// parameters into the target type.
133    pub fn kind(&self) -> &ErrorKind {
134        &self.kind
135    }
136
137    #[track_caller]
138    pub(super) fn unsupported_type(name: &'static str) -> Self {
139        Self::new(ErrorKind::UnsupportedType { name })
140    }
141}
142
143impl serde::de::Error for PathDeserializationError {
144    #[inline]
145    fn custom<T>(msg: T) -> Self
146    where
147        T: std::fmt::Display,
148    {
149        Self {
150            kind: ErrorKind::Message(msg.to_string()),
151        }
152    }
153}
154
155impl std::fmt::Display for PathDeserializationError {
156    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
157        std::fmt::Display::fmt(&self.kind, f)
158    }
159}
160
161impl std::error::Error for PathDeserializationError {}
162
163/// The kinds of errors that can happen when deserializing into a [`PathParams`].
164///
165/// This type is obtained through [`PathDeserializationError::kind`] and is useful for building
166/// more precise error messages (e.g. implementing your own custom conversion from
167/// [`PathDeserializationError`] into an HTTP response).
168///
169/// [`PathParams`]: struct@crate::request::path::PathParams
170#[derive(Debug, PartialEq, Eq)]
171#[non_exhaustive]
172pub enum ErrorKind {
173    /// Failed to parse the value at a specific key into the expected type.
174    ///
175    /// This variant is used when deserializing into types that have named fields, such as structs.
176    ParseErrorAtKey {
177        /// The key at which the value was located.
178        key: String,
179        /// The value from the URI.
180        value: String,
181        /// The expected type of the value.
182        expected_type: &'static str,
183    },
184
185    /// Failed to parse a value into the expected type.
186    ///
187    /// This variant is used when deserializing into a primitive type (such as `String` and `u32`).
188    ParseError {
189        /// The value from the URI.
190        value: String,
191        /// The expected type of the value.
192        expected_type: &'static str,
193    },
194
195    /// Tried to serialize into an unsupported type such as collections, tuples or nested maps.
196    ///
197    /// This error kind is caused by programmer errors and thus gets converted into a `500 Internal
198    /// Server Error` response.
199    UnsupportedType {
200        /// The name of the unsupported type.
201        name: &'static str,
202    },
203
204    /// Catch-all variant for errors that don't fit any other variant.
205    Message(String),
206}
207
208impl std::fmt::Display for ErrorKind {
209    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
210        match self {
211            ErrorKind::Message(error) => std::fmt::Display::fmt(error, f),
212            ErrorKind::UnsupportedType { name } => {
213                write!(
214                    f,
215                    "`{name}` is not a supported type for the `PathParams` extractor. \
216                    The type `T` in `Path<T>` must be a struct (with one public field for each \
217                    templated path segment) or a map (e.g. `HashMap<&'a str, Cow<'a, str>>`)."
218                )
219            }
220            ErrorKind::ParseErrorAtKey {
221                key,
222                value,
223                expected_type,
224            } => write!(
225                f,
226                "`{key}` is set to `{value}`, which we can't parse as a `{expected_type}`"
227            ),
228            ErrorKind::ParseError {
229                value,
230                expected_type,
231            } => write!(f, "We can't parse `{value}` as a `{expected_type}`"),
232        }
233    }
234}