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}