Skip to main content

crw_server/
error.rs

1use axum::Json;
2use axum::extract::rejection::JsonRejection;
3use axum::http::StatusCode;
4use axum::response::{IntoResponse, Response};
5use crw_core::error::CrwError;
6use crw_core::types::ApiResponse;
7
8/// Wrapper to implement IntoResponse for CrwError in the server crate.
9pub struct AppError(pub CrwError);
10
11impl From<CrwError> for AppError {
12    fn from(e: CrwError) -> Self {
13        Self(e)
14    }
15}
16
17impl From<JsonRejection> for AppError {
18    fn from(rejection: JsonRejection) -> Self {
19        let msg = match &rejection {
20            JsonRejection::JsonDataError(_) => {
21                let raw = rejection.body_text();
22                // Strip internal Rust type paths, keep the user-readable part.
23                if let Some(pos) = raw.find(": ") {
24                    format!("Invalid request body: {}", &raw[pos + 2..])
25                } else {
26                    format!("Invalid request body: {raw}")
27                }
28            }
29            JsonRejection::JsonSyntaxError(_) => "Invalid JSON syntax in request body".to_string(),
30            JsonRejection::MissingJsonContentType(_) => {
31                "Missing Content-Type: application/json header".to_string()
32            }
33            _ => "Invalid request body".to_string(),
34        };
35        Self(CrwError::InvalidRequest(msg))
36    }
37}
38
39impl IntoResponse for AppError {
40    fn into_response(self) -> Response {
41        let status = match &self.0 {
42            CrwError::InvalidRequest(_) => StatusCode::BAD_REQUEST,
43            CrwError::NotFound(_) => StatusCode::NOT_FOUND,
44            CrwError::Timeout(_) => StatusCode::GATEWAY_TIMEOUT,
45            CrwError::HttpError(_) => StatusCode::BAD_GATEWAY,
46            CrwError::TargetUnreachable(_) => StatusCode::UNPROCESSABLE_ENTITY,
47            CrwError::ExtractionError(_) => StatusCode::UNPROCESSABLE_ENTITY,
48            CrwError::RateLimited => StatusCode::TOO_MANY_REQUESTS,
49            CrwError::SearchDisabled(_) => StatusCode::SERVICE_UNAVAILABLE,
50            CrwError::SearchDegraded(_) => StatusCode::SERVICE_UNAVAILABLE,
51            _ => StatusCode::INTERNAL_SERVER_ERROR,
52        };
53
54        let error_code = self.0.error_code().to_string();
55        let body = ApiResponse::<()>::err_with_code(self.0.to_string(), error_code);
56        (status, Json(body)).into_response()
57    }
58}
59
60#[cfg(test)]
61mod tests {
62    use super::*;
63    use axum::http::StatusCode;
64
65    fn status_for(err: CrwError) -> StatusCode {
66        let app_err = AppError(err);
67        let response = app_err.into_response();
68        response.status()
69    }
70
71    #[test]
72    fn app_error_invalid_request_400() {
73        assert_eq!(
74            status_for(CrwError::InvalidRequest("bad".into())),
75            StatusCode::BAD_REQUEST
76        );
77    }
78
79    #[test]
80    fn app_error_not_found_404() {
81        assert_eq!(
82            status_for(CrwError::NotFound("missing".into())),
83            StatusCode::NOT_FOUND
84        );
85    }
86
87    #[test]
88    fn app_error_timeout_504() {
89        assert_eq!(
90            status_for(CrwError::Timeout(5000)),
91            StatusCode::GATEWAY_TIMEOUT
92        );
93    }
94
95    #[test]
96    fn app_error_http_error_502() {
97        assert_eq!(
98            status_for(CrwError::HttpError("fail".into())),
99            StatusCode::BAD_GATEWAY
100        );
101    }
102
103    #[test]
104    fn app_error_extraction_422() {
105        assert_eq!(
106            status_for(CrwError::ExtractionError("parse fail".into())),
107            StatusCode::UNPROCESSABLE_ENTITY
108        );
109    }
110
111    #[test]
112    fn app_error_internal_500() {
113        assert_eq!(
114            status_for(CrwError::Internal("oops".into())),
115            StatusCode::INTERNAL_SERVER_ERROR
116        );
117    }
118
119    #[test]
120    fn app_error_renderer_500() {
121        assert_eq!(
122            status_for(CrwError::RendererError("cdp fail".into())),
123            StatusCode::INTERNAL_SERVER_ERROR
124        );
125    }
126
127    #[tokio::test]
128    async fn app_error_body_is_api_response() {
129        let app_err = AppError(CrwError::InvalidRequest("test error".into()));
130        let response = app_err.into_response();
131        assert_eq!(response.status(), StatusCode::BAD_REQUEST);
132
133        let body = axum::body::to_bytes(response.into_body(), 1024 * 1024)
134            .await
135            .unwrap();
136        let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
137        assert_eq!(json["success"], false);
138        assert!(json["error"].as_str().unwrap().contains("test error"));
139    }
140}