soma_http_api/
response.rs1use axum::{
4 extract::rejection::JsonRejection,
5 http::StatusCode,
6 response::{IntoResponse, Response},
7 Json,
8};
9use serde::Serialize;
10
11use crate::problem::ErrorBody;
12
13pub fn json_rejection_response(error: JsonRejection) -> Response {
19 let status = if error.status() == StatusCode::PAYLOAD_TOO_LARGE {
20 StatusCode::PAYLOAD_TOO_LARGE
21 } else {
22 StatusCode::BAD_REQUEST
23 };
24 ErrorBody::new(error.to_string()).into_response_with_status(status)
25}
26
27pub fn validation_error_response(message: impl Into<String>) -> Response {
29 ErrorBody::new("validation_error")
30 .with_message(message)
31 .into_response_with_status(StatusCode::BAD_REQUEST)
32}
33
34pub fn json_response(status: StatusCode, body: impl Serialize) -> Response {
36 (status, Json(body)).into_response()
37}
38
39pub fn application_error_status(code: &str) -> StatusCode {
52 match code {
53 "unknown_action" | "surface_not_exposed" | "upstream_missing" | "unknown_upstream" => {
54 StatusCode::NOT_FOUND
55 }
56 "insufficient_scope" | "capability_denied" | "admin_required" | "not_exposed" => {
57 StatusCode::FORBIDDEN
58 }
59 "input_too_large" | "response_too_large" => StatusCode::PAYLOAD_TOO_LARGE,
60 "input_schema_failed"
61 | "confirmation_required"
62 | "invalid_param"
63 | "spawn_validation_failed"
64 | "upstream_exists"
65 | "invalid_config" => StatusCode::BAD_REQUEST,
66 "unsupported_transport" => StatusCode::NOT_IMPLEMENTED,
67 "gateway_reloading"
68 | "store_not_mounted"
69 | "oauth_runtime_error"
70 | "not_routable"
71 | "upstream_connect_failed"
72 | "upstream_call_failed"
73 | "engine_unavailable" => StatusCode::SERVICE_UNAVAILABLE,
74 _ => StatusCode::INTERNAL_SERVER_ERROR,
75 }
76}
77
78#[cfg(test)]
79mod tests {
80 use axum::{
81 body::{to_bytes, Body},
82 extract::DefaultBodyLimit,
83 http::Request,
84 routing::post,
85 Router,
86 };
87 use tower::ServiceExt;
88
89 use super::*;
90
91 #[tokio::test]
92 async fn validation_error_response_is_bad_request_with_message() {
93 let response = validation_error_response("name is required");
94 assert_eq!(response.status(), StatusCode::BAD_REQUEST);
95 let bytes = to_bytes(response.into_body(), usize::MAX).await.unwrap();
96 let value: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
97 assert_eq!(value["error"], "validation_error");
98 assert_eq!(value["message"], "name is required");
99 }
100
101 #[tokio::test]
102 async fn json_response_serializes_status_and_body() {
103 let response = json_response(StatusCode::CREATED, serde_json::json!({"ok": true}));
104 assert_eq!(response.status(), StatusCode::CREATED);
105 let bytes = to_bytes(response.into_body(), usize::MAX).await.unwrap();
106 let value: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
107 assert_eq!(value["ok"], true);
108 }
109
110 #[test]
111 fn application_error_status_maps_known_codes() {
112 assert_eq!(
113 application_error_status("unknown_action"),
114 StatusCode::NOT_FOUND
115 );
116 assert_eq!(
117 application_error_status("capability_denied"),
118 StatusCode::FORBIDDEN
119 );
120 assert_eq!(
121 application_error_status("input_too_large"),
122 StatusCode::PAYLOAD_TOO_LARGE
123 );
124 assert_eq!(
125 application_error_status("invalid_param"),
126 StatusCode::BAD_REQUEST
127 );
128 assert_eq!(
129 application_error_status("unsupported_transport"),
130 StatusCode::NOT_IMPLEMENTED
131 );
132 assert_eq!(
133 application_error_status("engine_unavailable"),
134 StatusCode::SERVICE_UNAVAILABLE
135 );
136 }
137
138 #[test]
139 fn application_error_status_defaults_to_internal_server_error() {
140 assert_eq!(
141 application_error_status("something_unmapped"),
142 StatusCode::INTERNAL_SERVER_ERROR
143 );
144 }
145
146 #[tokio::test]
154 async fn json_rejection_response_maps_oversized_body_to_413() {
155 async fn handler(body: Result<Json<serde_json::Value>, JsonRejection>) -> Response {
156 match body {
157 Ok(_) => StatusCode::OK.into_response(),
158 Err(error) => json_rejection_response(error),
159 }
160 }
161
162 let app = Router::new()
163 .route("/", post(handler))
164 .layer(DefaultBodyLimit::max(16));
165 let request = Request::builder()
166 .method("POST")
167 .uri("/")
168 .header("content-type", "application/json")
169 .body(Body::from(vec![b'x'; 100]))
170 .unwrap();
171
172 let response = app.oneshot(request).await.unwrap();
173
174 assert_eq!(response.status(), StatusCode::PAYLOAD_TOO_LARGE);
175 }
176
177 #[tokio::test]
178 async fn json_rejection_response_maps_malformed_json_to_400() {
179 async fn handler(body: Result<Json<serde_json::Value>, JsonRejection>) -> Response {
180 match body {
181 Ok(_) => StatusCode::OK.into_response(),
182 Err(error) => json_rejection_response(error),
183 }
184 }
185
186 let app = Router::new().route("/", post(handler));
187 let request = Request::builder()
188 .method("POST")
189 .uri("/")
190 .header("content-type", "application/json")
191 .body(Body::from("not json"))
192 .unwrap();
193
194 let response = app.oneshot(request).await.unwrap();
195
196 assert_eq!(response.status(), StatusCode::BAD_REQUEST);
197 }
198}