axum_codec/
rejection.rs

1use axum::{extract::rejection::BytesRejection, http::StatusCode, response::Response};
2
3use crate::{ContentType, IntoCodecResponse};
4
5/// Rejection used for [`Codec`](crate::Codec).
6///
7/// Contains one variant for each way the [`Codec`](crate::Codec) extractor
8/// can fail.
9#[derive(Debug, thiserror::Error)]
10#[non_exhaustive]
11pub enum CodecRejection {
12	#[error(transparent)]
13	Bytes(#[from] BytesRejection),
14	#[cfg(feature = "json")]
15	#[error(transparent)]
16	Json(#[from] serde_json::Error),
17	#[cfg(feature = "form")]
18	#[error(transparent)]
19	Form(#[from] serde_urlencoded::de::Error),
20	#[cfg(feature = "msgpack")]
21	#[error(transparent)]
22	MsgPack(#[from] rmp_serde::decode::Error),
23	#[cfg(feature = "cbor")]
24	#[error(transparent)]
25	Cbor(#[from] ciborium::de::Error<std::io::Error>),
26	#[cfg(feature = "bincode")]
27	#[error(transparent)]
28	Bincode(#[from] bincode::error::DecodeError),
29	#[cfg(feature = "bitcode")]
30	#[error(transparent)]
31	Bitcode(#[from] bitcode::Error),
32	#[cfg(feature = "yaml")]
33	#[error(transparent)]
34	Yaml(#[from] serde_yaml::Error),
35	#[cfg(feature = "toml")]
36	#[error(transparent)]
37	Toml(#[from] toml::de::Error),
38	#[cfg(any(feature = "toml", feature = "yaml"))]
39	#[error(transparent)]
40	Utf8Error(#[from] core::str::Utf8Error),
41	#[cfg(feature = "validator")]
42	#[error("validator error")]
43	Validator(#[from] validator::ValidationErrors),
44}
45
46#[cfg(not(feature = "pretty-errors"))]
47impl IntoCodecResponse for CodecRejection {
48	fn into_codec_response(self, _content_type: ContentType) -> Response {
49		use axum::response::IntoResponse;
50
51		let mut response = self.to_string().into_response();
52
53		*response.status_mut() = self.status_code();
54		response
55	}
56}
57
58#[cfg(all(feature = "aide", feature = "pretty-errors"))]
59impl aide::OperationOutput for CodecRejection {
60	type Inner = Message;
61
62	fn operation_response(
63		ctx: &mut aide::generate::GenContext,
64		operation: &mut aide::openapi::Operation,
65	) -> Option<aide::openapi::Response> {
66		axum::Json::<Message>::operation_response(ctx, operation)
67	}
68
69	fn inferred_responses(
70		ctx: &mut aide::generate::GenContext,
71		operation: &mut aide::openapi::Operation,
72	) -> Vec<(Option<u16>, aide::openapi::Response)> {
73		axum::Json::<Message>::inferred_responses(ctx, operation)
74	}
75}
76
77#[cfg(all(feature = "aide", not(feature = "pretty-errors")))]
78impl aide::OperationOutput for CodecRejection {
79	type Inner = String;
80
81	fn operation_response(
82		ctx: &mut aide::generate::GenContext,
83		operation: &mut aide::openapi::Operation,
84	) -> Option<aide::openapi::Response> {
85		axum::Json::<String>::operation_response(ctx, operation)
86	}
87
88	fn inferred_responses(
89		ctx: &mut aide::generate::GenContext,
90		operation: &mut aide::openapi::Operation,
91	) -> Vec<(Option<u16>, aide::openapi::Response)> {
92		axum::Json::<String>::inferred_responses(ctx, operation)
93	}
94}
95
96#[cfg(feature = "pretty-errors")]
97impl IntoCodecResponse for CodecRejection {
98	fn into_codec_response(self, content_type: ContentType) -> Response {
99		let mut response = crate::Codec(self.message()).into_codec_response(content_type);
100
101		*response.status_mut() = self.status_code();
102		response
103	}
104}
105
106#[cfg(feature = "pretty-errors")]
107#[crate::apply(encode, crate = "crate")]
108pub struct Message {
109	/// A unique error code, useful for localization.
110	pub code: &'static str,
111	/// A human-readable error message in English.
112	// TODO: use Cow<'static, str> when bitcode supports it
113	pub content: String,
114}
115
116#[cfg(all(feature = "aide", feature = "pretty-errors"))]
117impl aide::OperationOutput for Message {
118	type Inner = Self;
119
120	fn operation_response(
121		ctx: &mut aide::generate::GenContext,
122		operation: &mut aide::openapi::Operation,
123	) -> Option<aide::openapi::Response> {
124		axum::Json::<Self>::operation_response(ctx, operation)
125	}
126
127	fn inferred_responses(
128		ctx: &mut aide::generate::GenContext,
129		operation: &mut aide::openapi::Operation,
130	) -> Vec<(Option<u16>, aide::openapi::Response)> {
131		axum::Json::<Self>::inferred_responses(ctx, operation)
132	}
133}
134
135impl CodecRejection {
136	/// Returns the HTTP status code for the rejection.
137	#[must_use]
138	pub fn status_code(&self) -> StatusCode {
139		if matches!(self, Self::Bytes(..)) {
140			StatusCode::PAYLOAD_TOO_LARGE
141		} else {
142			StatusCode::BAD_REQUEST
143		}
144	}
145
146	/// Consumes the rejection and returns a pretty [`Message`] representing the
147	/// error.
148	///
149	/// Useful for sending a detailed error message to the client, but not so much
150	/// for local debugging.
151	#[cfg(feature = "pretty-errors")]
152	#[must_use]
153	pub fn message(&self) -> Message {
154		let code = match self {
155			Self::Bytes(..) => {
156				return Message {
157					code: "payload_too_large",
158					content: "The request payload is too large.".into(),
159				}
160			}
161			#[cfg(feature = "json")]
162			Self::Json(..) => "decode",
163			#[cfg(feature = "form")]
164			Self::Form(..) => "decode",
165			#[cfg(feature = "msgpack")]
166			Self::MsgPack(..) => "decode",
167			#[cfg(feature = "cbor")]
168			Self::Cbor(..) => "decode",
169			#[cfg(feature = "bincode")]
170			Self::Bincode(..) => "decode",
171			#[cfg(feature = "bitcode")]
172			Self::Bitcode(..) => "decode",
173			#[cfg(feature = "yaml")]
174			Self::Yaml(..) => "decode",
175			#[cfg(feature = "toml")]
176			Self::Toml(..) => "decode",
177			#[cfg(any(feature = "toml", feature = "yaml"))]
178			Self::Utf8Error(..) => {
179				return Message {
180					code: "malformed_utf8",
181					content: "The request payload is not valid UTF-8 when it should be.".into(),
182				}
183			}
184			#[cfg(feature = "validator")]
185			Self::Validator(err) => {
186				return Message {
187					code: "invalid_input",
188					content: format_validator(err),
189				}
190			}
191		};
192
193		Message {
194			code,
195			content: self.to_string(),
196		}
197	}
198}
199
200#[cfg(all(feature = "pretty-errors", feature = "validator"))]
201fn format_validator(err: &validator::ValidationErrors) -> String {
202	let mut buf = String::new();
203
204	for (field, error) in err.errors() {
205		append_validator_errors(field, error, &mut buf);
206	}
207
208	buf
209}
210
211#[cfg(all(feature = "pretty-errors", feature = "validator"))]
212fn append_validator_errors(field: &str, err: &validator::ValidationErrorsKind, buf: &mut String) {
213	match err {
214		validator::ValidationErrorsKind::Field(errors) => {
215			for error in errors {
216				if !buf.is_empty() {
217					buf.push_str(", ");
218				}
219
220				buf.push_str(field);
221				buf.push_str(": ");
222
223				if let Some(message) = &error.message {
224					buf.push_str(message);
225				} else {
226					buf.push_str(&error.code);
227				}
228
229				if !error.params.is_empty() {
230					buf.push_str(" (");
231
232					let mut params = error.params.iter();
233
234					if let Some((key, value)) = params.next() {
235						buf.push_str(key);
236						buf.push_str(": ");
237						buf.push_str(&value.to_string());
238					}
239
240					for (key, value) in params {
241						buf.push_str(", ");
242						buf.push_str(key);
243						buf.push_str(": ");
244						buf.push_str(&value.to_string());
245					}
246
247					buf.push(')');
248				}
249			}
250		}
251		validator::ValidationErrorsKind::List(message) => {
252			for error in message.values() {
253				for (field, errors) in error.errors() {
254					append_validator_errors(field, errors, buf);
255				}
256			}
257		}
258		validator::ValidationErrorsKind::Struct(struct_) => {
259			for (field, errors) in struct_.errors() {
260				append_validator_errors(field, errors, buf);
261			}
262		}
263	}
264}