Skip to main content

armature_core/
error.rs

1// Error types for the Armature framework
2
3use 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    // 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    /// Build a client-safe HTTP response for this error.
248    ///
249    /// The status code comes from [`Error::status_code`]. 4xx client errors
250    /// keep their descriptive message so callers can correct their request;
251    /// 5xx server errors are redacted to a generic `"Internal Server Error"`
252    /// message so internal detail (connection strings, upstream errors, stack
253    /// context) never leaks to the client. The full error should be logged
254    /// separately at the call site.
255    ///
256    /// The JSON body is always `{"error": <message>, "status": <code>}`. This
257    /// is the single canonical error-to-response mapping shared by every server
258    /// transport (HTTP/1, HTTP/2, HTTP/3, and the micro server) so their error
259    /// responses never drift apart.
260    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    // ============================================================================
277    // Convenience Constructors
278    // ============================================================================
279
280    /// Create a bad request error with a message.
281    pub fn bad_request(msg: impl Into<String>) -> Self {
282        Self::BadRequest(msg.into())
283    }
284
285    /// Create an unauthorized error with a message.
286    pub fn unauthorized(msg: impl Into<String>) -> Self {
287        Self::Unauthorized(msg.into())
288    }
289
290    /// Create a forbidden error with a message.
291    pub fn forbidden(msg: impl Into<String>) -> Self {
292        Self::Forbidden(msg.into())
293    }
294
295    /// Create a not found error with a message.
296    pub fn not_found(msg: impl Into<String>) -> Self {
297        Self::NotFound(msg.into())
298    }
299
300    /// Create a conflict error with a message.
301    pub fn conflict(msg: impl Into<String>) -> Self {
302        Self::Conflict(msg.into())
303    }
304
305    /// Create an internal server error with a message.
306    pub fn internal(msg: impl Into<String>) -> Self {
307        Self::Internal(msg.into())
308    }
309
310    /// Create a validation error with a message.
311    pub fn validation(msg: impl Into<String>) -> Self {
312        Self::Validation(msg.into())
313    }
314
315    /// Create a timeout error with a message.
316    pub fn timeout(msg: impl Into<String>) -> Self {
317        Self::RequestTimeout(msg.into())
318    }
319
320    /// Create a rate limit error with a message.
321    pub fn rate_limited(msg: impl Into<String>) -> Self {
322        Self::TooManyRequests(msg.into())
323    }
324
325    /// Create a service unavailable error with a message.
326    pub fn unavailable(msg: impl Into<String>) -> Self {
327        Self::ServiceUnavailable(msg.into())
328    }
329
330    /// Get a help message with suggestions for resolving this error.
331    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        // Body must be valid JSON despite quotes in the message.
701        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}