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
8pub 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 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}