armature_core/
error.rs

1// Error types for the Armature framework
2
3use crate::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    // 4xx Client Errors
50    #[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    // 5xx Server Errors
140    #[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    /// Get the HTTP status code for this error
175    pub fn status_code(&self) -> u16 {
176        match self {
177            // Legacy mappings
178            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            // 4xx Client Errors
185            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            // 5xx Server Errors
214            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            // Default to 500 for unmapped errors
228            _ => HttpStatus::InternalServerError.code(),
229        }
230    }
231
232    /// Get the HttpStatus enum for this error
233    pub fn http_status(&self) -> HttpStatus {
234        HttpStatus::from_code(self.status_code()).unwrap_or(HttpStatus::InternalServerError)
235    }
236
237    /// Check if this is a client error (4xx)
238    pub fn is_client_error(&self) -> bool {
239        self.http_status().is_client_error()
240    }
241
242    /// Check if this is a server error (5xx)
243    pub fn is_server_error(&self) -> bool {
244        self.http_status().is_server_error()
245    }
246
247    // ============================================================================
248    // Convenience Constructors
249    // ============================================================================
250
251    /// Create a bad request error with a message.
252    pub fn bad_request(msg: impl Into<String>) -> Self {
253        Self::BadRequest(msg.into())
254    }
255
256    /// Create an unauthorized error with a message.
257    pub fn unauthorized(msg: impl Into<String>) -> Self {
258        Self::Unauthorized(msg.into())
259    }
260
261    /// Create a forbidden error with a message.
262    pub fn forbidden(msg: impl Into<String>) -> Self {
263        Self::Forbidden(msg.into())
264    }
265
266    /// Create a not found error with a message.
267    pub fn not_found(msg: impl Into<String>) -> Self {
268        Self::NotFound(msg.into())
269    }
270
271    /// Create a conflict error with a message.
272    pub fn conflict(msg: impl Into<String>) -> Self {
273        Self::Conflict(msg.into())
274    }
275
276    /// Create an internal server error with a message.
277    pub fn internal(msg: impl Into<String>) -> Self {
278        Self::Internal(msg.into())
279    }
280
281    /// Create a validation error with a message.
282    pub fn validation(msg: impl Into<String>) -> Self {
283        Self::Validation(msg.into())
284    }
285
286    /// Create a timeout error with a message.
287    pub fn timeout(msg: impl Into<String>) -> Self {
288        Self::RequestTimeout(msg.into())
289    }
290
291    /// Create a rate limit error with a message.
292    pub fn rate_limited(msg: impl Into<String>) -> Self {
293        Self::TooManyRequests(msg.into())
294    }
295
296    /// Create a service unavailable error with a message.
297    pub fn unavailable(msg: impl Into<String>) -> Self {
298        Self::ServiceUnavailable(msg.into())
299    }
300
301    /// Get a help message with suggestions for resolving this error.
302    pub fn help(&self) -> Option<&'static str> {
303        match self {
304            Error::ProviderNotFound(_) => Some(
305                "Make sure to:\n\
306                 1. Add the provider to your module's providers() method\n\
307                 2. Or register it directly: container.register(MyService::new())\n\
308                 3. Check that the type matches exactly (including generics)",
309            ),
310            Error::RouteNotFound(_) => Some(
311                "Check that:\n\
312                 1. The route is registered in a controller\n\
313                 2. The controller is added to a module\n\
314                 3. The module is imported into your app module\n\
315                 4. The HTTP method matches (GET, POST, etc.)",
316            ),
317            Error::Deserialization(_) => Some(
318                "Verify that:\n\
319                 1. The request body is valid JSON\n\
320                 2. Field names match your struct (check #[serde(rename)] attributes)\n\
321                 3. Data types match (strings vs numbers, etc.)\n\
322                 4. Required fields are present",
323            ),
324            Error::Unauthorized(_) => Some(
325                "To authenticate:\n\
326                 1. Include 'Authorization: Bearer <token>' header\n\
327                 2. Ensure the token is not expired\n\
328                 3. Check that the token has the required scopes",
329            ),
330            Error::TooManyRequests(_) => Some(
331                "To resolve rate limiting:\n\
332                 1. Wait for the retry-after duration\n\
333                 2. Reduce request frequency\n\
334                 3. Check the X-RateLimit-* headers for limits",
335            ),
336            _ => None,
337        }
338    }
339}
340
341#[cfg(test)]
342mod tests {
343    use super::*;
344
345    #[test]
346    fn test_internal_error_status() {
347        let err = Error::Internal("test".to_string());
348        assert_eq!(err.status_code(), 500);
349        assert!(err.is_server_error());
350        assert!(!err.is_client_error());
351    }
352
353    #[test]
354    fn test_not_found_error() {
355        let err = Error::NotFound("resource".to_string());
356        assert_eq!(err.status_code(), 404);
357        assert!(err.is_client_error());
358        assert!(!err.is_server_error());
359    }
360
361    #[test]
362    fn test_unauthorized_error() {
363        let err = Error::Unauthorized("auth required".to_string());
364        assert_eq!(err.status_code(), 401);
365        assert!(err.is_client_error());
366    }
367
368    #[test]
369    fn test_forbidden_error() {
370        let err = Error::Forbidden("access denied".to_string());
371        assert_eq!(err.status_code(), 403);
372        assert!(err.is_client_error());
373    }
374
375    #[test]
376    fn test_bad_request_error() {
377        let err = Error::BadRequest("invalid input".to_string());
378        assert_eq!(err.status_code(), 400);
379    }
380
381    #[test]
382    fn test_conflict_error() {
383        let err = Error::Conflict("resource conflict".to_string());
384        assert_eq!(err.status_code(), 409);
385    }
386
387    #[test]
388    fn test_gone_error() {
389        let err = Error::Gone("resource deleted".to_string());
390        assert_eq!(err.status_code(), 410);
391    }
392
393    #[test]
394    fn test_payload_too_large() {
395        let err = Error::PayloadTooLarge("file too big".to_string());
396        assert_eq!(err.status_code(), 413);
397    }
398
399    #[test]
400    fn test_unsupported_media_type() {
401        let err = Error::UnsupportedMediaType("invalid content-type".to_string());
402        assert_eq!(err.status_code(), 415);
403    }
404
405    #[test]
406    fn test_too_many_requests() {
407        let err = Error::TooManyRequests("rate limited".to_string());
408        assert_eq!(err.status_code(), 429);
409    }
410
411    #[test]
412    fn test_not_implemented() {
413        let err = Error::NotImplemented("feature not ready".to_string());
414        assert_eq!(err.status_code(), 501);
415        assert!(err.is_server_error());
416    }
417
418    #[test]
419    fn test_bad_gateway() {
420        let err = Error::BadGateway("upstream error".to_string());
421        assert_eq!(err.status_code(), 502);
422    }
423
424    #[test]
425    fn test_service_unavailable() {
426        let err = Error::ServiceUnavailable("maintenance".to_string());
427        assert_eq!(err.status_code(), 503);
428    }
429
430    #[test]
431    fn test_gateway_timeout() {
432        let err = Error::GatewayTimeout("upstream timeout".to_string());
433        assert_eq!(err.status_code(), 504);
434    }
435
436    #[test]
437    fn test_method_not_allowed() {
438        let err = Error::MethodNotAllowed("POST not allowed".to_string());
439        assert_eq!(err.status_code(), 405);
440    }
441
442    #[test]
443    fn test_not_acceptable() {
444        let err = Error::NotAcceptable("format not supported".to_string());
445        assert_eq!(err.status_code(), 406);
446    }
447
448    #[test]
449    fn test_request_timeout() {
450        let err = Error::RequestTimeout("request took too long".to_string());
451        assert_eq!(err.status_code(), 408);
452    }
453
454    #[test]
455    fn test_unprocessable_entity() {
456        let err = Error::UnprocessableEntity("validation failed".to_string());
457        assert_eq!(err.status_code(), 422);
458    }
459
460    #[test]
461    fn test_locked() {
462        let err = Error::Locked("resource locked".to_string());
463        assert_eq!(err.status_code(), 423);
464    }
465
466    #[test]
467    fn test_upgrade_required() {
468        let err = Error::UpgradeRequired("http/2 required".to_string());
469        assert_eq!(err.status_code(), 426);
470    }
471
472    #[test]
473    fn test_precondition_required() {
474        let err = Error::PreconditionRequired("if-match required".to_string());
475        assert_eq!(err.status_code(), 428);
476    }
477
478    #[test]
479    fn test_http_status_conversion() {
480        let err = Error::NotFound("test".to_string());
481        let status = err.http_status();
482        assert_eq!(status, HttpStatus::NotFound);
483    }
484
485    #[test]
486    fn test_error_display() {
487        let err = Error::Internal("something went wrong".to_string());
488        let display = format!("{}", err);
489        assert!(display.contains("something went wrong"));
490    }
491
492    #[test]
493    fn test_io_error_conversion() {
494        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
495        let err: Error = io_err.into();
496        assert!(matches!(err, Error::Io(_)));
497    }
498
499    #[test]
500    fn test_serialization_error() {
501        let err = Error::Serialization("failed to serialize".to_string());
502        assert!(format!("{}", err).contains("Serialization"));
503    }
504
505    #[test]
506    fn test_deserialization_error() {
507        let err = Error::Deserialization("failed to deserialize".to_string());
508        assert!(format!("{}", err).contains("Deserialization"));
509    }
510
511    #[test]
512    fn test_validation_error() {
513        let err = Error::Validation("validation failed".to_string());
514        assert!(format!("{}", err).contains("Validation"));
515    }
516
517    #[test]
518    fn test_http_error() {
519        let err = Error::Http("http error".to_string());
520        assert!(format!("{}", err).contains("HTTP error"));
521    }
522
523    #[test]
524    fn test_route_not_found_error() {
525        let err = Error::RouteNotFound("/api/users".to_string());
526        assert!(format!("{}", err).contains("Route not found"));
527    }
528
529    #[test]
530    fn test_im_a_teapot() {
531        let err = Error::ImATeapot("I'm a teapot".to_string());
532        assert_eq!(err.status_code(), 418);
533    }
534
535    #[test]
536    fn test_misdirected_request() {
537        let err = Error::MisdirectedRequest("wrong server".to_string());
538        assert_eq!(err.status_code(), 421);
539    }
540
541    #[test]
542    fn test_failed_dependency() {
543        let err = Error::FailedDependency("dependent request failed".to_string());
544        assert_eq!(err.status_code(), 424);
545    }
546
547    #[test]
548    fn test_too_early() {
549        let err = Error::TooEarly("request too early".to_string());
550        assert_eq!(err.status_code(), 425);
551    }
552
553    #[test]
554    fn test_request_header_fields_too_large() {
555        let err = Error::RequestHeaderFieldsTooLarge("headers too big".to_string());
556        assert_eq!(err.status_code(), 431);
557    }
558
559    #[test]
560    fn test_unavailable_for_legal_reasons() {
561        let err = Error::UnavailableForLegalReasons("blocked by law".to_string());
562        assert_eq!(err.status_code(), 451);
563    }
564
565    #[test]
566    fn test_http_version_not_supported() {
567        let err = Error::HttpVersionNotSupported("http/0.9 not supported".to_string());
568        assert_eq!(err.status_code(), 505);
569    }
570
571    #[test]
572    fn test_variant_also_negotiates() {
573        let err = Error::VariantAlsoNegotiates("circular reference".to_string());
574        assert_eq!(err.status_code(), 506);
575    }
576
577    #[test]
578    fn test_insufficient_storage() {
579        let err = Error::InsufficientStorage("disk full".to_string());
580        assert_eq!(err.status_code(), 507);
581    }
582
583    #[test]
584    fn test_loop_detected() {
585        let err = Error::LoopDetected("infinite loop".to_string());
586        assert_eq!(err.status_code(), 508);
587    }
588
589    #[test]
590    fn test_not_extended() {
591        let err = Error::NotExtended("extension required".to_string());
592        assert_eq!(err.status_code(), 510);
593    }
594
595    #[test]
596    fn test_network_authentication_required() {
597        let err = Error::NetworkAuthenticationRequired("proxy auth required".to_string());
598        assert_eq!(err.status_code(), 511);
599    }
600
601    #[test]
602    fn test_length_required() {
603        let err = Error::LengthRequired("content-length missing".to_string());
604        assert_eq!(err.status_code(), 411);
605    }
606
607    #[test]
608    fn test_precondition_failed() {
609        let err = Error::PreconditionFailed("if-match failed".to_string());
610        assert_eq!(err.status_code(), 412);
611    }
612
613    #[test]
614    fn test_uri_too_long() {
615        let err = Error::UriTooLong("url too long".to_string());
616        assert_eq!(err.status_code(), 414);
617    }
618
619    #[test]
620    fn test_range_not_satisfiable() {
621        let err = Error::RangeNotSatisfiable("invalid range".to_string());
622        assert_eq!(err.status_code(), 416);
623    }
624
625    #[test]
626    fn test_expectation_failed() {
627        let err = Error::ExpectationFailed("expect header failed".to_string());
628        assert_eq!(err.status_code(), 417);
629    }
630
631    #[test]
632    fn test_proxy_authentication_required() {
633        let err = Error::ProxyAuthenticationRequired("proxy auth needed".to_string());
634        assert_eq!(err.status_code(), 407);
635    }
636
637    #[test]
638    fn test_client_error_range() {
639        for code in 400..500 {
640            if let Some(_status) = HttpStatus::from_code(code) {
641                let err = Error::BadRequest("test".to_string());
642                if err.status_code() == code {
643                    assert!(err.is_client_error());
644                    assert!(!err.is_server_error());
645                }
646            }
647        }
648    }
649
650    #[test]
651    fn test_server_error_range() {
652        for code in 500..600 {
653            if let Some(_status) = HttpStatus::from_code(code) {
654                let err = Error::Internal("test".to_string());
655                if err.status_code() == code {
656                    assert!(err.is_server_error());
657                    assert!(!err.is_client_error());
658                }
659            }
660        }
661    }
662}