1#![allow(clippy::new_ret_no_self)]
2
3use std::collections::BTreeMap;
4
5use http::StatusCode;
6
7#[derive(Debug)]
8pub struct HttpException {
9 pub status: StatusCode,
10 pub message: String,
11 pub error: &'static str,
12 pub errors: Option<BTreeMap<String, Vec<String>>>,
13 pub source: Option<anyhow::Error>,
14}
15
16impl HttpException {
17 pub fn new(status: StatusCode, error: &'static str, message: impl Into<String>) -> Self {
18 Self {
19 status,
20 error,
21 message: message.into(),
22 errors: None,
23 source: None,
24 }
25 }
26
27 pub fn with_source(mut self, err: impl Into<anyhow::Error>) -> Self {
28 self.source = Some(err.into());
29 self
30 }
31
32 pub fn with_errors(mut self, errors: BTreeMap<String, Vec<String>>) -> Self {
33 self.errors = Some(errors);
34 self
35 }
36}
37
38pub(crate) fn startup_error(message: impl Into<String>) -> HttpException {
39 HttpException::new(
40 StatusCode::INTERNAL_SERVER_ERROR,
41 "Internal Server Error",
42 message,
43 )
44}
45
46pub struct BadRequestException;
47impl BadRequestException {
48 pub fn new(message: impl Into<String>) -> HttpException {
49 HttpException::new(StatusCode::BAD_REQUEST, "Bad Request", message)
50 }
51}
52
53pub struct UnauthorizedException;
54impl UnauthorizedException {
55 pub fn new(message: impl Into<String>) -> HttpException {
56 HttpException::new(StatusCode::UNAUTHORIZED, "Unauthorized", message)
57 }
58}
59
60pub struct PaymentRequiredException;
61impl PaymentRequiredException {
62 pub fn new(message: impl Into<String>) -> HttpException {
63 HttpException::new(StatusCode::PAYMENT_REQUIRED, "Payment Required", message)
64 }
65}
66
67pub struct ForbiddenException;
68impl ForbiddenException {
69 pub fn new(message: impl Into<String>) -> HttpException {
70 HttpException::new(StatusCode::FORBIDDEN, "Forbidden", message)
71 }
72}
73
74pub struct NotFoundException;
75impl NotFoundException {
76 pub fn new(message: impl Into<String>) -> HttpException {
77 HttpException::new(StatusCode::NOT_FOUND, "Not Found", message)
78 }
79}
80
81pub struct MethodNotAllowedException;
82impl MethodNotAllowedException {
83 pub fn new(message: impl Into<String>) -> HttpException {
84 HttpException::new(
85 StatusCode::METHOD_NOT_ALLOWED,
86 "Method Not Allowed",
87 message,
88 )
89 }
90}
91
92pub struct NotAcceptableException;
93impl NotAcceptableException {
94 pub fn new(message: impl Into<String>) -> HttpException {
95 HttpException::new(StatusCode::NOT_ACCEPTABLE, "Not Acceptable", message)
96 }
97}
98
99pub struct ProxyAuthenticationRequiredException;
100impl ProxyAuthenticationRequiredException {
101 pub fn new(message: impl Into<String>) -> HttpException {
102 HttpException::new(
103 StatusCode::PROXY_AUTHENTICATION_REQUIRED,
104 "Proxy Authentication Required",
105 message,
106 )
107 }
108}
109
110pub struct RequestTimeoutException;
111impl RequestTimeoutException {
112 pub fn new(message: impl Into<String>) -> HttpException {
113 HttpException::new(StatusCode::REQUEST_TIMEOUT, "Request Timeout", message)
114 }
115}
116
117pub struct ConflictException;
118impl ConflictException {
119 pub fn new(message: impl Into<String>) -> HttpException {
120 HttpException::new(StatusCode::CONFLICT, "Conflict", message)
121 }
122}
123
124pub struct GoneException;
125impl GoneException {
126 pub fn new(message: impl Into<String>) -> HttpException {
127 HttpException::new(StatusCode::GONE, "Gone", message)
128 }
129}
130
131pub struct LengthRequiredException;
132impl LengthRequiredException {
133 pub fn new(message: impl Into<String>) -> HttpException {
134 HttpException::new(StatusCode::LENGTH_REQUIRED, "Length Required", message)
135 }
136}
137
138pub struct PreconditionFailedException;
139impl PreconditionFailedException {
140 pub fn new(message: impl Into<String>) -> HttpException {
141 HttpException::new(
142 StatusCode::PRECONDITION_FAILED,
143 "Precondition Failed",
144 message,
145 )
146 }
147}
148
149pub struct PayloadTooLargeException;
150impl PayloadTooLargeException {
151 pub fn new(message: impl Into<String>) -> HttpException {
152 HttpException::new(StatusCode::PAYLOAD_TOO_LARGE, "Payload Too Large", message)
153 }
154}
155
156pub struct UriTooLongException;
157impl UriTooLongException {
158 pub fn new(message: impl Into<String>) -> HttpException {
159 HttpException::new(StatusCode::URI_TOO_LONG, "URI Too Long", message)
160 }
161}
162
163pub struct UnsupportedMediaTypeException;
164impl UnsupportedMediaTypeException {
165 pub fn new(message: impl Into<String>) -> HttpException {
166 HttpException::new(
167 StatusCode::UNSUPPORTED_MEDIA_TYPE,
168 "Unsupported Media Type",
169 message,
170 )
171 }
172}
173
174pub struct RangeNotSatisfiableException;
175impl RangeNotSatisfiableException {
176 pub fn new(message: impl Into<String>) -> HttpException {
177 HttpException::new(
178 StatusCode::RANGE_NOT_SATISFIABLE,
179 "Range Not Satisfiable",
180 message,
181 )
182 }
183}
184
185pub struct ExpectationFailedException;
186impl ExpectationFailedException {
187 pub fn new(message: impl Into<String>) -> HttpException {
188 HttpException::new(
189 StatusCode::EXPECTATION_FAILED,
190 "Expectation Failed",
191 message,
192 )
193 }
194}
195
196pub struct ImATeapotException;
197impl ImATeapotException {
198 pub fn new(message: impl Into<String>) -> HttpException {
199 HttpException::new(StatusCode::IM_A_TEAPOT, "I'm a teapot", message)
200 }
201}
202
203pub struct MisdirectedRequestException;
204impl MisdirectedRequestException {
205 pub fn new(message: impl Into<String>) -> HttpException {
206 HttpException::new(
207 StatusCode::MISDIRECTED_REQUEST,
208 "Misdirected Request",
209 message,
210 )
211 }
212}
213
214pub struct UnprocessableEntityException;
215impl UnprocessableEntityException {
216 pub fn new(message: impl Into<String>) -> HttpException {
217 HttpException::new(
218 StatusCode::UNPROCESSABLE_ENTITY,
219 "Unprocessable Entity",
220 message,
221 )
222 }
223}
224
225pub struct LockedException;
226impl LockedException {
227 pub fn new(message: impl Into<String>) -> HttpException {
228 HttpException::new(StatusCode::LOCKED, "Locked", message)
229 }
230}
231
232pub struct FailedDependencyException;
233impl FailedDependencyException {
234 pub fn new(message: impl Into<String>) -> HttpException {
235 HttpException::new(StatusCode::FAILED_DEPENDENCY, "Failed Dependency", message)
236 }
237}
238
239pub struct TooEarlyException;
240impl TooEarlyException {
241 pub fn new(message: impl Into<String>) -> HttpException {
242 HttpException::new(StatusCode::TOO_EARLY, "Too Early", message)
243 }
244}
245
246pub struct UpgradeRequiredException;
247impl UpgradeRequiredException {
248 pub fn new(message: impl Into<String>) -> HttpException {
249 HttpException::new(StatusCode::UPGRADE_REQUIRED, "Upgrade Required", message)
250 }
251}
252
253pub struct PreconditionRequiredException;
254impl PreconditionRequiredException {
255 pub fn new(message: impl Into<String>) -> HttpException {
256 HttpException::new(
257 StatusCode::PRECONDITION_REQUIRED,
258 "Precondition Required",
259 message,
260 )
261 }
262}
263
264pub struct TooManyRequestsException;
265impl TooManyRequestsException {
266 pub fn new(message: impl Into<String>) -> HttpException {
267 HttpException::new(StatusCode::TOO_MANY_REQUESTS, "Too Many Requests", message)
268 }
269}
270
271pub struct RequestHeaderFieldsTooLargeException;
272impl RequestHeaderFieldsTooLargeException {
273 pub fn new(message: impl Into<String>) -> HttpException {
274 HttpException::new(
275 StatusCode::REQUEST_HEADER_FIELDS_TOO_LARGE,
276 "Request Header Fields Too Large",
277 message,
278 )
279 }
280}
281
282pub struct UnavailableForLegalReasonsException;
283impl UnavailableForLegalReasonsException {
284 pub fn new(message: impl Into<String>) -> HttpException {
285 HttpException::new(
286 StatusCode::UNAVAILABLE_FOR_LEGAL_REASONS,
287 "Unavailable For Legal Reasons",
288 message,
289 )
290 }
291}
292
293pub struct InternalServerErrorException;
294impl InternalServerErrorException {
295 pub fn new(err: impl Into<anyhow::Error>) -> HttpException {
296 HttpException::new(
297 StatusCode::INTERNAL_SERVER_ERROR,
298 "Internal Server Error",
299 "Internal Server Error",
300 )
301 .with_source(err)
302 }
303}
304
305#[cfg(feature = "sqlx")]
306impl From<sqlx::Error> for HttpException {
307 fn from(err: sqlx::Error) -> Self {
308 InternalServerErrorException::new(err)
309 }
310}
311
312#[cfg(feature = "validator")]
313impl From<validator::ValidationErrors> for HttpException {
314 fn from(err: validator::ValidationErrors) -> Self {
315 BadRequestException::new("Validation failed").with_errors(format_validation_errors(&err))
316 }
317}
318
319#[cfg(feature = "validator")]
320fn format_validation_errors(err: &validator::ValidationErrors) -> BTreeMap<String, Vec<String>> {
321 let mut errors = BTreeMap::new();
322 collect_validation_errors("", err, &mut errors);
323 errors
324}
325
326#[cfg(feature = "validator")]
327fn collect_validation_errors(
328 prefix: &str,
329 err: &validator::ValidationErrors,
330 field_errors: &mut BTreeMap<String, Vec<String>>,
331) {
332 let mut fields = err.errors().iter().collect::<Vec<_>>();
333 fields.sort_by(|(left, _), (right, _)| left.as_ref().cmp(right.as_ref()));
334
335 for (field, kind) in fields {
336 let path = if prefix.is_empty() {
337 (*field).to_string()
338 } else {
339 format!("{prefix}.{field}")
340 };
341
342 collect_validation_error_kind(&path, kind, field_errors);
343 }
344}
345
346#[cfg(feature = "validator")]
347fn collect_validation_error_kind(
348 path: &str,
349 kind: &validator::ValidationErrorsKind,
350 field_errors: &mut BTreeMap<String, Vec<String>>,
351) {
352 match kind {
353 validator::ValidationErrorsKind::Field(errors) => {
354 for error in errors {
355 field_errors
356 .entry(path.to_string())
357 .or_default()
358 .push(format_validation_error(error));
359 }
360 }
361 validator::ValidationErrorsKind::Struct(errors) => {
362 collect_validation_errors(path, errors, field_errors);
363 }
364 validator::ValidationErrorsKind::List(errors) => {
365 for (index, errors) in errors {
366 collect_validation_errors(&format!("{path}[{index}]"), errors, field_errors);
367 }
368 }
369 }
370}
371
372#[cfg(feature = "validator")]
373fn format_validation_error(error: &validator::ValidationError) -> String {
374 if let Some(message) = &error.message {
375 return message.to_string();
376 }
377
378 match error.code.as_ref() {
379 "email" => "must be a valid email".to_string(),
380 "length" => format_length_validation_error(error),
381 "required" => "is required".to_string(),
382 "url" => "must be a valid URL".to_string(),
383 "regex" => "has an invalid format".to_string(),
384 "contains" => match validation_param(error, "needle") {
385 Some(needle) => format!("must contain {needle}"),
386 None => "must contain the required value".to_string(),
387 },
388 "does_not_contain" => match validation_param(error, "needle") {
389 Some(needle) => format!("must not contain {needle}"),
390 None => "contains a forbidden value".to_string(),
391 },
392 "must_match" => match validation_param(error, "other") {
393 Some(other) => format!("must match {other}"),
394 None => "does not match".to_string(),
395 },
396 "ip" => "must be a valid IP address".to_string(),
397 "ipv4" => "must be a valid IPv4 address".to_string(),
398 "ipv6" => "must be a valid IPv6 address".to_string(),
399 code => format!("is invalid ({code})"),
400 }
401}
402
403#[cfg(feature = "validator")]
404fn format_length_validation_error(error: &validator::ValidationError) -> String {
405 match (
406 validation_param(error, "equal"),
407 validation_param(error, "min"),
408 validation_param(error, "max"),
409 ) {
410 (Some(equal), _, _) => format!("must be exactly {equal} characters"),
411 (None, Some(min), Some(max)) => {
412 format!("must be between {min} and {max} characters")
413 }
414 (None, Some(min), None) => format!("must be at least {min} characters"),
415 (None, None, Some(max)) => format!("must be at most {max} characters"),
416 (None, None, None) => "has an invalid length".to_string(),
417 }
418}
419
420#[cfg(feature = "validator")]
421fn validation_param(error: &validator::ValidationError, name: &str) -> Option<String> {
422 let value = error.params.get(name)?;
423
424 match value {
425 serde_json::Value::String(value) => Some(value.clone()),
426 serde_json::Value::Number(value) => Some(value.to_string()),
427 serde_json::Value::Bool(value) => Some(value.to_string()),
428 serde_json::Value::Null => None,
429 value => Some(value.to_string()),
430 }
431}
432
433pub struct NotImplementedException;
434impl NotImplementedException {
435 pub fn new(message: impl Into<String>) -> HttpException {
436 HttpException::new(StatusCode::NOT_IMPLEMENTED, "Not Implemented", message)
437 }
438}
439
440pub struct BadGatewayException;
441impl BadGatewayException {
442 pub fn new(message: impl Into<String>) -> HttpException {
443 HttpException::new(StatusCode::BAD_GATEWAY, "Bad Gateway", message)
444 }
445}
446
447pub struct ServiceUnavailableException;
448impl ServiceUnavailableException {
449 pub fn new(message: impl Into<String>) -> HttpException {
450 HttpException::new(
451 StatusCode::SERVICE_UNAVAILABLE,
452 "Service Unavailable",
453 message,
454 )
455 }
456}
457
458pub struct GatewayTimeoutException;
459impl GatewayTimeoutException {
460 pub fn new(message: impl Into<String>) -> HttpException {
461 HttpException::new(StatusCode::GATEWAY_TIMEOUT, "Gateway Timeout", message)
462 }
463}
464
465pub struct HttpVersionNotSupportedException;
466impl HttpVersionNotSupportedException {
467 pub fn new(message: impl Into<String>) -> HttpException {
468 HttpException::new(
469 StatusCode::HTTP_VERSION_NOT_SUPPORTED,
470 "HTTP Version Not Supported",
471 message,
472 )
473 }
474}
475
476pub struct VariantAlsoNegotiatesException;
477impl VariantAlsoNegotiatesException {
478 pub fn new(message: impl Into<String>) -> HttpException {
479 HttpException::new(
480 StatusCode::VARIANT_ALSO_NEGOTIATES,
481 "Variant Also Negotiates",
482 message,
483 )
484 }
485}
486
487pub struct InsufficientStorageException;
488impl InsufficientStorageException {
489 pub fn new(message: impl Into<String>) -> HttpException {
490 HttpException::new(
491 StatusCode::INSUFFICIENT_STORAGE,
492 "Insufficient Storage",
493 message,
494 )
495 }
496}
497
498pub struct LoopDetectedException;
499impl LoopDetectedException {
500 pub fn new(message: impl Into<String>) -> HttpException {
501 HttpException::new(StatusCode::LOOP_DETECTED, "Loop Detected", message)
502 }
503}
504
505pub struct NotExtendedException;
506impl NotExtendedException {
507 pub fn new(message: impl Into<String>) -> HttpException {
508 HttpException::new(StatusCode::NOT_EXTENDED, "Not Extended", message)
509 }
510}
511
512pub struct NetworkAuthenticationRequiredException;
513impl NetworkAuthenticationRequiredException {
514 pub fn new(message: impl Into<String>) -> HttpException {
515 HttpException::new(
516 StatusCode::NETWORK_AUTHENTICATION_REQUIRED,
517 "Network Authentication Required",
518 message,
519 )
520 }
521}