1use crate::{HttpResponse, HttpStatus};
4use thiserror::Error;
5
6#[derive(Error, Debug)]
7pub enum Error {
8 #[error("HTTP error: {0}")]
9 Http(String),
10
11 #[error(
12 "Route not found: '{0}'. Check your controller paths and ensure the route is registered."
13 )]
14 RouteNotFound(String),
15
16 #[error(
17 "Method {0} not allowed. Verify the HTTP method matches your route definition (#[get], #[post], etc.)."
18 )]
19 MethodNotAllowed(String),
20
21 #[error(
22 "Dependency injection error: {0}. Ensure all dependencies are registered with the container."
23 )]
24 DependencyInjection(String),
25
26 #[error(
27 "Provider not found: '{0}'. Did you forget to register it? Use container.register() or add it to your module's providers()."
28 )]
29 ProviderNotFound(String),
30
31 #[error("Serialization error: {0}. Ensure your type implements Serialize correctly.")]
32 Serialization(String),
33
34 #[error("Deserialization error: {0}. Check that the request body matches the expected format.")]
35 Deserialization(String),
36
37 #[error("Validation error: {0}")]
38 Validation(String),
39
40 #[error("Internal server error: {0}. Check server logs for details.")]
41 Internal(String),
42
43 #[error("Forbidden: {0}. User lacks required permissions for this resource.")]
44 Forbidden(String),
45
46 #[error("IO error: {0}")]
47 Io(#[from] std::io::Error),
48
49 #[error("Bad Request: {0}. Check the request parameters and body format.")]
51 BadRequest(String),
52
53 #[error(
54 "Unauthorized: {0}. Include valid authentication credentials (e.g., Bearer token in Authorization header)."
55 )]
56 Unauthorized(String),
57
58 #[error("Payment Required: {0}")]
59 PaymentRequired(String),
60
61 #[error("Not Found: {0}. Verify the resource exists and the URL is correct.")]
62 NotFound(String),
63
64 #[error("Not Acceptable: {0}. Check the Accept header matches available response formats.")]
65 NotAcceptable(String),
66
67 #[error("Proxy Authentication Required: {0}")]
68 ProxyAuthenticationRequired(String),
69
70 #[error("Request Timeout: {0}")]
71 RequestTimeout(String),
72
73 #[error("Conflict: {0}")]
74 Conflict(String),
75
76 #[error("Gone: {0}")]
77 Gone(String),
78
79 #[error("Length Required: {0}")]
80 LengthRequired(String),
81
82 #[error("Precondition Failed: {0}")]
83 PreconditionFailed(String),
84
85 #[error(
86 "Payload Too Large: {0}. Reduce the request body size or increase the server's body_limit."
87 )]
88 PayloadTooLarge(String),
89
90 #[error("URI Too Long: {0}. Use POST with a request body instead of query parameters.")]
91 UriTooLong(String),
92
93 #[error(
94 "Unsupported Media Type: {0}. Set Content-Type header to a supported format (e.g., application/json)."
95 )]
96 UnsupportedMediaType(String),
97
98 #[error("Range Not Satisfiable: {0}")]
99 RangeNotSatisfiable(String),
100
101 #[error("Expectation Failed: {0}")]
102 ExpectationFailed(String),
103
104 #[error("I'm a teapot: {0}")]
105 ImATeapot(String),
106
107 #[error("Misdirected Request: {0}")]
108 MisdirectedRequest(String),
109
110 #[error("Unprocessable Entity: {0}")]
111 UnprocessableEntity(String),
112
113 #[error("Locked: {0}")]
114 Locked(String),
115
116 #[error("Failed Dependency: {0}")]
117 FailedDependency(String),
118
119 #[error("Too Early: {0}")]
120 TooEarly(String),
121
122 #[error("Upgrade Required: {0}")]
123 UpgradeRequired(String),
124
125 #[error("Precondition Required: {0}")]
126 PreconditionRequired(String),
127
128 #[error(
129 "Too Many Requests: {0}. Rate limit exceeded. Wait before retrying or reduce request frequency."
130 )]
131 TooManyRequests(String),
132
133 #[error("Request Header Fields Too Large: {0}")]
134 RequestHeaderFieldsTooLarge(String),
135
136 #[error("Unavailable For Legal Reasons: {0}")]
137 UnavailableForLegalReasons(String),
138
139 #[error("Not Implemented: {0}. This feature is not yet available.")]
141 NotImplemented(String),
142
143 #[error("Bad Gateway: {0}. The upstream server returned an invalid response.")]
144 BadGateway(String),
145
146 #[error(
147 "Service Unavailable: {0}. Server is temporarily unable to handle requests. Try again later."
148 )]
149 ServiceUnavailable(String),
150
151 #[error("Gateway Timeout: {0}. The upstream server did not respond in time.")]
152 GatewayTimeout(String),
153
154 #[error("HTTP Version Not Supported: {0}")]
155 HttpVersionNotSupported(String),
156
157 #[error("Variant Also Negotiates: {0}")]
158 VariantAlsoNegotiates(String),
159
160 #[error("Insufficient Storage: {0}")]
161 InsufficientStorage(String),
162
163 #[error("Loop Detected: {0}")]
164 LoopDetected(String),
165
166 #[error("Not Extended: {0}")]
167 NotExtended(String),
168
169 #[error("Network Authentication Required: {0}")]
170 NetworkAuthenticationRequired(String),
171}
172
173impl Error {
174 pub fn status_code(&self) -> u16 {
176 match self {
177 Error::RouteNotFound(_) => HttpStatus::NotFound.code(),
179 Error::MethodNotAllowed(_) => HttpStatus::MethodNotAllowed.code(),
180 Error::Validation(_) => HttpStatus::BadRequest.code(),
181 Error::Deserialization(_) => HttpStatus::BadRequest.code(),
182 Error::Forbidden(_) => HttpStatus::Forbidden.code(),
183
184 Error::BadRequest(_) => HttpStatus::BadRequest.code(),
186 Error::Unauthorized(_) => HttpStatus::Unauthorized.code(),
187 Error::PaymentRequired(_) => HttpStatus::PaymentRequired.code(),
188 Error::NotFound(_) => HttpStatus::NotFound.code(),
189 Error::NotAcceptable(_) => HttpStatus::NotAcceptable.code(),
190 Error::ProxyAuthenticationRequired(_) => HttpStatus::ProxyAuthenticationRequired.code(),
191 Error::RequestTimeout(_) => HttpStatus::RequestTimeout.code(),
192 Error::Conflict(_) => HttpStatus::Conflict.code(),
193 Error::Gone(_) => HttpStatus::Gone.code(),
194 Error::LengthRequired(_) => HttpStatus::LengthRequired.code(),
195 Error::PreconditionFailed(_) => HttpStatus::PreconditionFailed.code(),
196 Error::PayloadTooLarge(_) => HttpStatus::PayloadTooLarge.code(),
197 Error::UriTooLong(_) => HttpStatus::UriTooLong.code(),
198 Error::UnsupportedMediaType(_) => HttpStatus::UnsupportedMediaType.code(),
199 Error::RangeNotSatisfiable(_) => HttpStatus::RangeNotSatisfiable.code(),
200 Error::ExpectationFailed(_) => HttpStatus::ExpectationFailed.code(),
201 Error::ImATeapot(_) => HttpStatus::ImATeapot.code(),
202 Error::MisdirectedRequest(_) => HttpStatus::MisdirectedRequest.code(),
203 Error::UnprocessableEntity(_) => HttpStatus::UnprocessableEntity.code(),
204 Error::Locked(_) => HttpStatus::Locked.code(),
205 Error::FailedDependency(_) => HttpStatus::FailedDependency.code(),
206 Error::TooEarly(_) => HttpStatus::TooEarly.code(),
207 Error::UpgradeRequired(_) => HttpStatus::UpgradeRequired.code(),
208 Error::PreconditionRequired(_) => HttpStatus::PreconditionRequired.code(),
209 Error::TooManyRequests(_) => HttpStatus::TooManyRequests.code(),
210 Error::RequestHeaderFieldsTooLarge(_) => HttpStatus::RequestHeaderFieldsTooLarge.code(),
211 Error::UnavailableForLegalReasons(_) => HttpStatus::UnavailableForLegalReasons.code(),
212
213 Error::NotImplemented(_) => HttpStatus::NotImplemented.code(),
215 Error::BadGateway(_) => HttpStatus::BadGateway.code(),
216 Error::ServiceUnavailable(_) => HttpStatus::ServiceUnavailable.code(),
217 Error::GatewayTimeout(_) => HttpStatus::GatewayTimeout.code(),
218 Error::HttpVersionNotSupported(_) => HttpStatus::HttpVersionNotSupported.code(),
219 Error::VariantAlsoNegotiates(_) => HttpStatus::VariantAlsoNegotiates.code(),
220 Error::InsufficientStorage(_) => HttpStatus::InsufficientStorage.code(),
221 Error::LoopDetected(_) => HttpStatus::LoopDetected.code(),
222 Error::NotExtended(_) => HttpStatus::NotExtended.code(),
223 Error::NetworkAuthenticationRequired(_) => {
224 HttpStatus::NetworkAuthenticationRequired.code()
225 }
226
227 _ => HttpStatus::InternalServerError.code(),
229 }
230 }
231
232 pub fn http_status(&self) -> HttpStatus {
234 HttpStatus::from_code(self.status_code()).unwrap_or(HttpStatus::InternalServerError)
235 }
236
237 pub fn is_client_error(&self) -> bool {
239 self.http_status().is_client_error()
240 }
241
242 pub fn is_server_error(&self) -> bool {
244 self.http_status().is_server_error()
245 }
246
247 pub fn to_client_response(&self) -> HttpResponse {
261 let status = self.status_code();
262 let message = if status >= 500 {
263 "Internal Server Error".to_string()
264 } else {
265 self.to_string()
266 };
267 let body = serde_json::json!({
268 "error": message,
269 "status": status,
270 });
271 HttpResponse::new(status)
272 .with_json(&body)
273 .unwrap_or_else(|_| HttpResponse::internal_server_error())
274 }
275
276 pub fn bad_request(msg: impl Into<String>) -> Self {
282 Self::BadRequest(msg.into())
283 }
284
285 pub fn unauthorized(msg: impl Into<String>) -> Self {
287 Self::Unauthorized(msg.into())
288 }
289
290 pub fn forbidden(msg: impl Into<String>) -> Self {
292 Self::Forbidden(msg.into())
293 }
294
295 pub fn not_found(msg: impl Into<String>) -> Self {
297 Self::NotFound(msg.into())
298 }
299
300 pub fn conflict(msg: impl Into<String>) -> Self {
302 Self::Conflict(msg.into())
303 }
304
305 pub fn internal(msg: impl Into<String>) -> Self {
307 Self::Internal(msg.into())
308 }
309
310 pub fn validation(msg: impl Into<String>) -> Self {
312 Self::Validation(msg.into())
313 }
314
315 pub fn timeout(msg: impl Into<String>) -> Self {
317 Self::RequestTimeout(msg.into())
318 }
319
320 pub fn rate_limited(msg: impl Into<String>) -> Self {
322 Self::TooManyRequests(msg.into())
323 }
324
325 pub fn unavailable(msg: impl Into<String>) -> Self {
327 Self::ServiceUnavailable(msg.into())
328 }
329
330 pub fn help(&self) -> Option<&'static str> {
332 match self {
333 Error::ProviderNotFound(_) => Some(
334 "Make sure to:\n\
335 1. Add the provider to your module's providers() method\n\
336 2. Or register it directly: container.register(MyService::new())\n\
337 3. Check that the type matches exactly (including generics)",
338 ),
339 Error::RouteNotFound(_) => Some(
340 "Check that:\n\
341 1. The route is registered in a controller\n\
342 2. The controller is added to a module\n\
343 3. The module is imported into your app module\n\
344 4. The HTTP method matches (GET, POST, etc.)",
345 ),
346 Error::Deserialization(_) => Some(
347 "Verify that:\n\
348 1. The request body is valid JSON\n\
349 2. Field names match your struct (check #[serde(rename)] attributes)\n\
350 3. Data types match (strings vs numbers, etc.)\n\
351 4. Required fields are present",
352 ),
353 Error::Unauthorized(_) => Some(
354 "To authenticate:\n\
355 1. Include 'Authorization: Bearer <token>' header\n\
356 2. Ensure the token is not expired\n\
357 3. Check that the token has the required scopes",
358 ),
359 Error::TooManyRequests(_) => Some(
360 "To resolve rate limiting:\n\
361 1. Wait for the retry-after duration\n\
362 2. Reduce request frequency\n\
363 3. Check the X-RateLimit-* headers for limits",
364 ),
365 _ => None,
366 }
367 }
368}
369
370#[cfg(test)]
371mod tests {
372 use super::*;
373
374 #[test]
375 fn test_internal_error_status() {
376 let err = Error::Internal("test".to_string());
377 assert_eq!(err.status_code(), 500);
378 assert!(err.is_server_error());
379 assert!(!err.is_client_error());
380 }
381
382 #[test]
383 fn test_not_found_error() {
384 let err = Error::NotFound("resource".to_string());
385 assert_eq!(err.status_code(), 404);
386 assert!(err.is_client_error());
387 assert!(!err.is_server_error());
388 }
389
390 #[test]
391 fn test_unauthorized_error() {
392 let err = Error::Unauthorized("auth required".to_string());
393 assert_eq!(err.status_code(), 401);
394 assert!(err.is_client_error());
395 }
396
397 #[test]
398 fn test_forbidden_error() {
399 let err = Error::Forbidden("access denied".to_string());
400 assert_eq!(err.status_code(), 403);
401 assert!(err.is_client_error());
402 }
403
404 #[test]
405 fn test_bad_request_error() {
406 let err = Error::BadRequest("invalid input".to_string());
407 assert_eq!(err.status_code(), 400);
408 }
409
410 #[test]
411 fn test_conflict_error() {
412 let err = Error::Conflict("resource conflict".to_string());
413 assert_eq!(err.status_code(), 409);
414 }
415
416 #[test]
417 fn test_gone_error() {
418 let err = Error::Gone("resource deleted".to_string());
419 assert_eq!(err.status_code(), 410);
420 }
421
422 #[test]
423 fn test_payload_too_large() {
424 let err = Error::PayloadTooLarge("file too big".to_string());
425 assert_eq!(err.status_code(), 413);
426 }
427
428 #[test]
429 fn test_unsupported_media_type() {
430 let err = Error::UnsupportedMediaType("invalid content-type".to_string());
431 assert_eq!(err.status_code(), 415);
432 }
433
434 #[test]
435 fn test_too_many_requests() {
436 let err = Error::TooManyRequests("rate limited".to_string());
437 assert_eq!(err.status_code(), 429);
438 }
439
440 #[test]
441 fn test_not_implemented() {
442 let err = Error::NotImplemented("feature not ready".to_string());
443 assert_eq!(err.status_code(), 501);
444 assert!(err.is_server_error());
445 }
446
447 #[test]
448 fn test_bad_gateway() {
449 let err = Error::BadGateway("upstream error".to_string());
450 assert_eq!(err.status_code(), 502);
451 }
452
453 #[test]
454 fn test_service_unavailable() {
455 let err = Error::ServiceUnavailable("maintenance".to_string());
456 assert_eq!(err.status_code(), 503);
457 }
458
459 #[test]
460 fn test_gateway_timeout() {
461 let err = Error::GatewayTimeout("upstream timeout".to_string());
462 assert_eq!(err.status_code(), 504);
463 }
464
465 #[test]
466 fn test_method_not_allowed() {
467 let err = Error::MethodNotAllowed("POST not allowed".to_string());
468 assert_eq!(err.status_code(), 405);
469 }
470
471 #[test]
472 fn test_not_acceptable() {
473 let err = Error::NotAcceptable("format not supported".to_string());
474 assert_eq!(err.status_code(), 406);
475 }
476
477 #[test]
478 fn test_request_timeout() {
479 let err = Error::RequestTimeout("request took too long".to_string());
480 assert_eq!(err.status_code(), 408);
481 }
482
483 #[test]
484 fn test_unprocessable_entity() {
485 let err = Error::UnprocessableEntity("validation failed".to_string());
486 assert_eq!(err.status_code(), 422);
487 }
488
489 #[test]
490 fn test_locked() {
491 let err = Error::Locked("resource locked".to_string());
492 assert_eq!(err.status_code(), 423);
493 }
494
495 #[test]
496 fn test_upgrade_required() {
497 let err = Error::UpgradeRequired("http/2 required".to_string());
498 assert_eq!(err.status_code(), 426);
499 }
500
501 #[test]
502 fn test_precondition_required() {
503 let err = Error::PreconditionRequired("if-match required".to_string());
504 assert_eq!(err.status_code(), 428);
505 }
506
507 #[test]
508 fn test_http_status_conversion() {
509 let err = Error::NotFound("test".to_string());
510 let status = err.http_status();
511 assert_eq!(status, HttpStatus::NotFound);
512 }
513
514 #[test]
515 fn test_error_display() {
516 let err = Error::Internal("something went wrong".to_string());
517 let display = format!("{}", err);
518 assert!(display.contains("something went wrong"));
519 }
520
521 #[test]
522 fn test_io_error_conversion() {
523 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
524 let err: Error = io_err.into();
525 assert!(matches!(err, Error::Io(_)));
526 }
527
528 #[test]
529 fn test_serialization_error() {
530 let err = Error::Serialization("failed to serialize".to_string());
531 assert!(format!("{}", err).contains("Serialization"));
532 }
533
534 #[test]
535 fn test_deserialization_error() {
536 let err = Error::Deserialization("failed to deserialize".to_string());
537 assert!(format!("{}", err).contains("Deserialization"));
538 }
539
540 #[test]
541 fn test_validation_error() {
542 let err = Error::Validation("validation failed".to_string());
543 assert!(format!("{}", err).contains("Validation"));
544 }
545
546 #[test]
547 fn test_http_error() {
548 let err = Error::Http("http error".to_string());
549 assert!(format!("{}", err).contains("HTTP error"));
550 }
551
552 #[test]
553 fn test_route_not_found_error() {
554 let err = Error::RouteNotFound("/api/users".to_string());
555 assert!(format!("{}", err).contains("Route not found"));
556 }
557
558 #[test]
559 fn test_im_a_teapot() {
560 let err = Error::ImATeapot("I'm a teapot".to_string());
561 assert_eq!(err.status_code(), 418);
562 }
563
564 #[test]
565 fn test_misdirected_request() {
566 let err = Error::MisdirectedRequest("wrong server".to_string());
567 assert_eq!(err.status_code(), 421);
568 }
569
570 #[test]
571 fn test_failed_dependency() {
572 let err = Error::FailedDependency("dependent request failed".to_string());
573 assert_eq!(err.status_code(), 424);
574 }
575
576 #[test]
577 fn test_too_early() {
578 let err = Error::TooEarly("request too early".to_string());
579 assert_eq!(err.status_code(), 425);
580 }
581
582 #[test]
583 fn test_request_header_fields_too_large() {
584 let err = Error::RequestHeaderFieldsTooLarge("headers too big".to_string());
585 assert_eq!(err.status_code(), 431);
586 }
587
588 #[test]
589 fn test_unavailable_for_legal_reasons() {
590 let err = Error::UnavailableForLegalReasons("blocked by law".to_string());
591 assert_eq!(err.status_code(), 451);
592 }
593
594 #[test]
595 fn test_http_version_not_supported() {
596 let err = Error::HttpVersionNotSupported("http/0.9 not supported".to_string());
597 assert_eq!(err.status_code(), 505);
598 }
599
600 #[test]
601 fn test_variant_also_negotiates() {
602 let err = Error::VariantAlsoNegotiates("circular reference".to_string());
603 assert_eq!(err.status_code(), 506);
604 }
605
606 #[test]
607 fn test_insufficient_storage() {
608 let err = Error::InsufficientStorage("disk full".to_string());
609 assert_eq!(err.status_code(), 507);
610 }
611
612 #[test]
613 fn test_loop_detected() {
614 let err = Error::LoopDetected("infinite loop".to_string());
615 assert_eq!(err.status_code(), 508);
616 }
617
618 #[test]
619 fn test_not_extended() {
620 let err = Error::NotExtended("extension required".to_string());
621 assert_eq!(err.status_code(), 510);
622 }
623
624 #[test]
625 fn test_network_authentication_required() {
626 let err = Error::NetworkAuthenticationRequired("proxy auth required".to_string());
627 assert_eq!(err.status_code(), 511);
628 }
629
630 #[test]
631 fn test_length_required() {
632 let err = Error::LengthRequired("content-length missing".to_string());
633 assert_eq!(err.status_code(), 411);
634 }
635
636 #[test]
637 fn test_precondition_failed() {
638 let err = Error::PreconditionFailed("if-match failed".to_string());
639 assert_eq!(err.status_code(), 412);
640 }
641
642 #[test]
643 fn test_uri_too_long() {
644 let err = Error::UriTooLong("url too long".to_string());
645 assert_eq!(err.status_code(), 414);
646 }
647
648 #[test]
649 fn test_range_not_satisfiable() {
650 let err = Error::RangeNotSatisfiable("invalid range".to_string());
651 assert_eq!(err.status_code(), 416);
652 }
653
654 #[test]
655 fn test_expectation_failed() {
656 let err = Error::ExpectationFailed("expect header failed".to_string());
657 assert_eq!(err.status_code(), 417);
658 }
659
660 #[test]
661 fn test_proxy_authentication_required() {
662 let err = Error::ProxyAuthenticationRequired("proxy auth needed".to_string());
663 assert_eq!(err.status_code(), 407);
664 }
665
666 #[test]
667 fn test_to_client_response_redacts_5xx() {
668 let err = Error::Internal("db password auth failed for user 'app'".to_string());
669 let response = err.to_client_response();
670 assert_eq!(response.status, 500);
671 let body = String::from_utf8(response.into_body_bytes().to_vec()).unwrap();
672 assert!(!body.contains("db password"));
673 let parsed: serde_json::Value = serde_json::from_str(&body).unwrap();
674 assert_eq!(parsed["error"], "Internal Server Error");
675 assert_eq!(parsed["status"], 500);
676 }
677
678 #[test]
679 fn test_to_client_response_keeps_4xx_message_and_status_field() {
680 let err = Error::NotFound("User 42 not found".to_string());
681 let response = err.to_client_response();
682 assert_eq!(response.status, 404);
683 let body = String::from_utf8(response.into_body_bytes().to_vec()).unwrap();
684 let parsed: serde_json::Value = serde_json::from_str(&body).unwrap();
685 assert!(
686 parsed["error"]
687 .as_str()
688 .unwrap()
689 .contains("User 42 not found")
690 );
691 assert_eq!(parsed["status"], 404);
692 }
693
694 #[test]
695 fn test_to_client_response_escapes_json() {
696 let err = Error::Validation(r#"bad "quoted" input"#.to_string());
697 let response = err.to_client_response();
698 assert_eq!(response.status, 400);
699 let body = String::from_utf8(response.into_body_bytes().to_vec()).unwrap();
700 let parsed: serde_json::Value = serde_json::from_str(&body).unwrap();
702 assert!(
703 parsed["error"]
704 .as_str()
705 .unwrap()
706 .contains(r#"bad "quoted" input"#)
707 );
708 }
709
710 #[test]
711 fn test_client_error_range() {
712 for code in 400..500 {
713 if let Some(_status) = HttpStatus::from_code(code) {
714 let err = Error::BadRequest("test".to_string());
715 if err.status_code() == code {
716 assert!(err.is_client_error());
717 assert!(!err.is_server_error());
718 }
719 }
720 }
721 }
722
723 #[test]
724 fn test_server_error_range() {
725 for code in 500..600 {
726 if let Some(_status) = HttpStatus::from_code(code) {
727 let err = Error::Internal("test".to_string());
728 if err.status_code() == code {
729 assert!(err.is_server_error());
730 assert!(!err.is_client_error());
731 }
732 }
733 }
734 }
735}