api_tools/server/axum/layers/
http_errors.rs1use crate::server::axum::response::ApiError;
4use axum::body::Body;
5use axum::http::{Request, StatusCode};
6use axum::response::{IntoResponse, Response};
7use futures::future::BoxFuture;
8use std::task::{Context, Poll};
9use tower::{Layer, Service};
10
11#[derive(Clone, Debug)]
13pub struct HttpErrorsConfig {
14 pub body_max_size: usize,
16}
17
18#[derive(Clone)]
19pub struct HttpErrorsLayer {
20 pub config: HttpErrorsConfig,
21}
22
23impl HttpErrorsLayer {
24 pub fn new(config: &HttpErrorsConfig) -> Self {
26 Self { config: config.clone() }
27 }
28}
29
30impl<S> Layer<S> for HttpErrorsLayer {
31 type Service = HttpErrorsMiddleware<S>;
32
33 fn layer(&self, inner: S) -> Self::Service {
34 HttpErrorsMiddleware {
35 inner,
36 config: self.config.clone(),
37 }
38 }
39}
40
41#[derive(Clone)]
42pub struct HttpErrorsMiddleware<S> {
43 inner: S,
44 config: HttpErrorsConfig,
45}
46
47impl<S> Service<Request<Body>> for HttpErrorsMiddleware<S>
48where
49 S: Service<Request<Body>, Response = Response> + Send + Clone + 'static,
50 S::Future: Send + 'static,
51{
52 type Response = S::Response;
53 type Error = S::Error;
54 type Future = BoxFuture<'static, Result<Self::Response, Self::Error>>;
56
57 fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
58 self.inner.poll_ready(cx)
59 }
60
61 fn call(&mut self, request: Request<Body>) -> Self::Future {
62 let mut inner = self.inner.clone();
63 let config = self.config.clone();
64
65 Box::pin(async move {
66 let response: Response = inner.call(request).await?;
67
68 let headers = response.headers();
70 if let Some(content_type) = headers.get("content-type") {
71 let content_type = content_type.to_str().unwrap_or_default();
72 if content_type.starts_with("image/")
73 || content_type.starts_with("audio/")
74 || content_type.starts_with("video/")
75 {
76 return Ok(response);
77 }
78 }
79
80 let (parts, body) = response.into_parts();
81 match axum::body::to_bytes(body, config.body_max_size).await {
82 Ok(body) => match String::from_utf8(body.to_vec()) {
83 Ok(body) => match parts.status {
84 StatusCode::METHOD_NOT_ALLOWED => Ok(ApiError::MethodNotAllowed.into_response()),
85 StatusCode::UNPROCESSABLE_ENTITY => Ok(ApiError::UnprocessableEntity(body).into_response()),
86 StatusCode::NOT_FOUND if body.is_empty() => {
87 Ok(ApiError::NotFound("Resource Not Found".to_owned()).into_response())
88 }
89 _ => Ok(Response::from_parts(parts, Body::from(body))),
90 },
91 Err(err) => Ok(ApiError::InternalServerError(err.to_string()).into_response()),
92 },
93 Err(_) => Ok(ApiError::PayloadTooLarge.into_response()),
94 }
95 })
96 }
97}
98
99#[cfg(test)]
100mod tests {
101 use super::*;
102 use axum::http::header;
103 use std::convert::Infallible;
104 use tower::{ServiceBuilder, ServiceExt};
105
106 fn layer() -> HttpErrorsLayer {
107 HttpErrorsLayer::new(&HttpErrorsConfig { body_max_size: 1024 })
108 }
109
110 async fn read_body(response: Response) -> String {
111 let body = axum::body::to_bytes(response.into_body(), 4096).await.unwrap();
112 String::from_utf8(body.to_vec()).unwrap()
113 }
114
115 #[tokio::test]
116 async fn ok_response_passes_through_unchanged() {
117 let svc = ServiceBuilder::new()
118 .layer(layer())
119 .service(tower::service_fn(|_req: Request<Body>| async {
120 Ok::<_, Infallible>(
121 Response::builder()
122 .status(StatusCode::OK)
123 .body(Body::from("hello"))
124 .unwrap(),
125 )
126 }));
127
128 let response = svc
129 .oneshot(Request::builder().uri("/").body(Body::empty()).unwrap())
130 .await
131 .unwrap();
132
133 assert_eq!(response.status(), StatusCode::OK);
134 assert_eq!(read_body(response).await, "hello");
135 }
136
137 #[tokio::test]
138 async fn empty_404_is_rewritten_as_json_api_error() {
139 let svc = ServiceBuilder::new()
140 .layer(layer())
141 .service(tower::service_fn(|_req: Request<Body>| async {
142 Ok::<_, Infallible>(
143 Response::builder()
144 .status(StatusCode::NOT_FOUND)
145 .body(Body::empty())
146 .unwrap(),
147 )
148 }));
149
150 let response = svc
151 .oneshot(Request::builder().uri("/missing").body(Body::empty()).unwrap())
152 .await
153 .unwrap();
154
155 assert_eq!(response.status(), StatusCode::NOT_FOUND);
156 assert_eq!(
157 response.headers().get(header::CONTENT_TYPE).unwrap(),
158 "application/json",
159 );
160
161 let body = read_body(response).await;
162 assert!(body.contains("\"code\":404"), "body was: {body}");
163 assert!(body.contains("Resource Not Found"), "body was: {body}");
164 }
165
166 #[tokio::test]
169 async fn non_empty_404_is_passed_through() {
170 let svc = ServiceBuilder::new()
171 .layer(layer())
172 .service(tower::service_fn(|_req: Request<Body>| async {
173 Ok::<_, Infallible>(
174 Response::builder()
175 .status(StatusCode::NOT_FOUND)
176 .body(Body::from("custom 404"))
177 .unwrap(),
178 )
179 }));
180
181 let response = svc
182 .oneshot(Request::builder().uri("/").body(Body::empty()).unwrap())
183 .await
184 .unwrap();
185
186 assert_eq!(response.status(), StatusCode::NOT_FOUND);
187 assert_eq!(read_body(response).await, "custom 404");
188 }
189
190 #[tokio::test]
191 async fn method_not_allowed_is_rewritten_as_json_api_error() {
192 let svc = ServiceBuilder::new()
193 .layer(layer())
194 .service(tower::service_fn(|_req: Request<Body>| async {
195 Ok::<_, Infallible>(
196 Response::builder()
197 .status(StatusCode::METHOD_NOT_ALLOWED)
198 .body(Body::empty())
199 .unwrap(),
200 )
201 }));
202
203 let response = svc
204 .oneshot(Request::builder().uri("/").body(Body::empty()).unwrap())
205 .await
206 .unwrap();
207
208 assert_eq!(response.status(), StatusCode::METHOD_NOT_ALLOWED);
209 assert_eq!(
210 response.headers().get(header::CONTENT_TYPE).unwrap(),
211 "application/json",
212 );
213
214 let body = read_body(response).await;
215 assert!(body.contains("\"code\":405"), "body was: {body}");
216 assert!(body.contains("Method not allowed"), "body was: {body}");
217 }
218
219 #[tokio::test]
220 async fn unprocessable_entity_body_is_wrapped_into_json_message() {
221 let svc = ServiceBuilder::new()
222 .layer(layer())
223 .service(tower::service_fn(|_req: Request<Body>| async {
224 Ok::<_, Infallible>(
225 Response::builder()
226 .status(StatusCode::UNPROCESSABLE_ENTITY)
227 .body(Body::from("validation failed"))
228 .unwrap(),
229 )
230 }));
231
232 let response = svc
233 .oneshot(Request::builder().uri("/").body(Body::empty()).unwrap())
234 .await
235 .unwrap();
236
237 assert_eq!(response.status(), StatusCode::UNPROCESSABLE_ENTITY);
238
239 let body = read_body(response).await;
240 assert!(body.contains("\"code\":422"), "body was: {body}");
241 assert!(body.contains("validation failed"), "body was: {body}");
242 }
243
244 #[tokio::test]
247 async fn image_content_type_short_circuits_without_touching_body() {
248 let payload = vec![0u8, 1, 2, 3, 4];
249 let payload_clone = payload.clone();
250
251 let svc = ServiceBuilder::new()
252 .layer(layer())
253 .service(tower::service_fn(move |_req: Request<Body>| {
254 let payload = payload_clone.clone();
255 async move {
256 Ok::<_, Infallible>(
257 Response::builder()
258 .status(StatusCode::NOT_FOUND)
259 .header(header::CONTENT_TYPE, "image/png")
260 .body(Body::from(payload))
261 .unwrap(),
262 )
263 }
264 }));
265
266 let response = svc
267 .oneshot(Request::builder().uri("/").body(Body::empty()).unwrap())
268 .await
269 .unwrap();
270
271 assert_eq!(response.status(), StatusCode::NOT_FOUND);
272 let body = axum::body::to_bytes(response.into_body(), 4096).await.unwrap();
273 assert_eq!(body.to_vec(), payload);
274 }
275
276 #[tokio::test]
277 async fn body_exceeding_max_size_returns_payload_too_large() {
278 let small_layer = HttpErrorsLayer::new(&HttpErrorsConfig { body_max_size: 4 });
279 let svc = ServiceBuilder::new()
280 .layer(small_layer)
281 .service(tower::service_fn(|_req: Request<Body>| async {
282 Ok::<_, Infallible>(
283 Response::builder()
284 .status(StatusCode::OK)
285 .body(Body::from("hello world"))
286 .unwrap(),
287 )
288 }));
289
290 let response = svc
291 .oneshot(Request::builder().uri("/").body(Body::empty()).unwrap())
292 .await
293 .unwrap();
294
295 assert_eq!(response.status(), StatusCode::PAYLOAD_TOO_LARGE);
296 let body = read_body(response).await;
297 assert!(body.contains("\"code\":413"), "body was: {body}");
298 }
299}