1use axum::http::{HeaderName, HeaderValue, Method, Request, header};
7use tower_http::cors::{AllowHeaders, AllowMethods, AllowOrigin, CorsLayer};
8use tower_http::request_id::{
9 MakeRequestId, PropagateRequestIdLayer, RequestId, SetRequestIdLayer,
10};
11use tower_http::set_header::SetResponseHeaderLayer;
12
13pub use axum::middleware::*;
16
17pub static X_REQUEST_ID: HeaderName = HeaderName::from_static("x-request-id");
19
20#[derive(Debug, Clone)]
26pub struct CorsConfig {
27 pub allow_origins: AllowOrigin,
29 pub allow_methods: AllowMethods,
31 pub allow_headers: AllowHeaders,
33}
34
35impl Default for CorsConfig {
36 fn default() -> Self {
38 Self {
39 allow_origins: AllowOrigin::any(),
40 allow_methods: AllowMethods::list([
41 Method::GET,
42 Method::POST,
43 Method::PUT,
44 Method::PATCH,
45 Method::DELETE,
46 Method::OPTIONS,
47 ]),
48 allow_headers: AllowHeaders::list([
49 header::CONTENT_TYPE,
50 header::AUTHORIZATION,
51 header::ACCEPT,
52 X_REQUEST_ID.clone(),
53 ]),
54 }
55 }
56}
57
58pub fn cors() -> CorsLayer {
62 cors_with_config(CorsConfig::default())
63}
64
65pub fn cors_with_config(config: CorsConfig) -> CorsLayer {
67 CorsLayer::new()
68 .allow_origin(config.allow_origins)
69 .allow_methods(config.allow_methods)
70 .allow_headers(config.allow_headers)
71}
72
73#[derive(Clone, Copy)]
79pub struct UuidRequestId;
80
81impl MakeRequestId for UuidRequestId {
82 fn make_request_id<B>(&mut self, _request: &Request<B>) -> Option<RequestId> {
83 let id = uuid::Uuid::new_v4().to_string();
84 Some(RequestId::new(HeaderValue::from_str(&id).ok()?))
85 }
86}
87
88pub fn request_id() -> (SetRequestIdLayer<UuidRequestId>, PropagateRequestIdLayer) {
94 (
95 SetRequestIdLayer::new(X_REQUEST_ID.clone(), UuidRequestId),
96 PropagateRequestIdLayer::new(X_REQUEST_ID.clone()),
97 )
98}
99
100#[cfg(feature = "compression")]
111pub fn compression() -> tower_http::compression::CompressionLayer {
112 tower_http::compression::CompressionLayer::new()
113}
114
115pub fn timeout(duration: std::time::Duration) -> tower_http::timeout::TimeoutLayer {
128 tower_http::timeout::TimeoutLayer::with_status_code(
129 axum::http::StatusCode::REQUEST_TIMEOUT,
130 duration,
131 )
132}
133
134#[expect(
144 clippy::expect_used,
145 reason = "caller-provided directive is validated at the API boundary"
146)]
147pub fn cache_control(directive: &str) -> SetResponseHeaderLayer<HeaderValue> {
148 let value = HeaderValue::from_str(directive).expect("valid Cache-Control header value");
149 SetResponseHeaderLayer::if_not_present(header::CACHE_CONTROL, value)
150}
151
152pub fn no_cache() -> SetResponseHeaderLayer<HeaderValue> {
155 cache_control("no-cache, no-store, must-revalidate")
156}
157
158pub fn no_store() -> SetResponseHeaderLayer<HeaderValue> {
161 cache_control("no-store")
162}
163
164#[cfg(feature = "decompression")]
172pub fn decompression() -> tower_http::decompression::RequestDecompressionLayer {
173 tower_http::decompression::RequestDecompressionLayer::new()
174}
175
176#[cfg(test)]
177mod tests {
178 use super::*;
179 use axum::Router;
180 use axum::body::Body;
181 use axum::http::{self, StatusCode};
182 use axum::routing::get;
183 use tower::ServiceExt;
184
185 async fn ok_handler() -> &'static str {
186 "ok"
187 }
188
189 #[tokio::test]
190 async fn cors_allows_cross_origin_with_default_config() {
191 let app = Router::new().route("/", get(ok_handler)).layer(cors());
192
193 let request = http::Request::builder()
194 .uri("/")
195 .header(header::ORIGIN, "http://example.com")
196 .body(Body::empty())
197 .unwrap();
198
199 let response = app.oneshot(request).await.unwrap();
200 assert_eq!(response.status(), StatusCode::OK);
201 assert!(
202 response
203 .headers()
204 .get(header::ACCESS_CONTROL_ALLOW_ORIGIN)
205 .is_some()
206 );
207 }
208
209 #[tokio::test]
210 async fn cors_can_be_customized_with_specific_origins() {
211 let config = CorsConfig {
212 allow_origins: AllowOrigin::exact("http://allowed.com".parse().unwrap()),
213 ..CorsConfig::default()
214 };
215 let app = Router::new()
216 .route("/", get(ok_handler))
217 .layer(cors_with_config(config));
218
219 let request = http::Request::builder()
221 .uri("/")
222 .header(header::ORIGIN, "http://allowed.com")
223 .body(Body::empty())
224 .unwrap();
225
226 let response = app.oneshot(request).await.unwrap();
227 assert_eq!(response.status(), StatusCode::OK);
228 assert_eq!(
229 response
230 .headers()
231 .get(header::ACCESS_CONTROL_ALLOW_ORIGIN)
232 .unwrap(),
233 "http://allowed.com"
234 );
235 }
236
237 #[tokio::test]
238 async fn request_id_adds_header_to_response() {
239 let (set_id, propagate_id) = request_id();
240 let app = Router::new()
241 .route("/", get(ok_handler))
242 .layer(propagate_id)
243 .layer(set_id);
244
245 let request = http::Request::builder()
246 .uri("/")
247 .body(Body::empty())
248 .unwrap();
249
250 let response = app.oneshot(request).await.unwrap();
251 assert_eq!(response.status(), StatusCode::OK);
252
253 let rid = response
254 .headers()
255 .get("x-request-id")
256 .expect("x-request-id header should be present");
257 let parsed = uuid::Uuid::parse_str(rid.to_str().unwrap());
259 assert!(parsed.is_ok(), "x-request-id should be a valid UUID");
260 }
261
262 #[tokio::test]
263 async fn request_id_preserves_existing_header() {
264 let existing_id = "my-custom-request-id-123";
265 let (set_id, propagate_id) = request_id();
266 let app = Router::new()
267 .route("/", get(ok_handler))
268 .layer(propagate_id)
269 .layer(set_id);
270
271 let request = http::Request::builder()
272 .uri("/")
273 .header("x-request-id", existing_id)
274 .body(Body::empty())
275 .unwrap();
276
277 let response = app.oneshot(request).await.unwrap();
278 assert_eq!(response.status(), StatusCode::OK);
279 assert_eq!(
280 response
281 .headers()
282 .get("x-request-id")
283 .unwrap()
284 .to_str()
285 .unwrap(),
286 existing_id
287 );
288 }
289
290 #[tokio::test]
291 async fn middleware_composes_with_app_layer() {
292 let (set_id, propagate_id) = request_id();
293 let app = crate::App::new()
294 .routes(Router::new().route("/", get(ok_handler)))
295 .layer(cors())
296 .layer(propagate_id)
297 .layer(set_id)
298 .build();
299
300 let request = http::Request::builder()
301 .uri("/")
302 .header(header::ORIGIN, "http://example.com")
303 .body(Body::empty())
304 .unwrap();
305
306 let response = ServiceExt::<http::Request<Body>>::oneshot(app, request)
307 .await
308 .unwrap();
309 assert_eq!(response.status(), StatusCode::OK);
310
311 assert!(
313 response
314 .headers()
315 .get(header::ACCESS_CONTROL_ALLOW_ORIGIN)
316 .is_some()
317 );
318 assert!(response.headers().get("x-request-id").is_some());
320 }
321
322 #[tokio::test]
323 async fn axum_from_fn_is_accessible() {
324 async fn noop_middleware(
326 request: http::Request<Body>,
327 next: axum::middleware::Next,
328 ) -> axum::response::Response {
329 next.run(request).await
330 }
331
332 let _layer = from_fn::<_, ()>(noop_middleware);
333 }
334
335 #[tokio::test]
336 async fn cache_control_sets_header_on_response() {
337 let app = Router::new()
338 .route("/", get(ok_handler))
339 .layer(cache_control("max-age=3600"));
340
341 let request = http::Request::builder()
342 .uri("/")
343 .body(Body::empty())
344 .unwrap();
345
346 let response = app.oneshot(request).await.unwrap();
347 assert_eq!(response.status(), StatusCode::OK);
348 assert_eq!(
349 response
350 .headers()
351 .get(header::CACHE_CONTROL)
352 .unwrap()
353 .to_str()
354 .unwrap(),
355 "max-age=3600"
356 );
357 }
358
359 #[tokio::test]
360 async fn cache_control_does_not_overwrite_handler_set_header() {
361 async fn handler_with_cache() -> impl axum::response::IntoResponse {
362 ([(header::CACHE_CONTROL, "private, max-age=60")], "ok")
363 }
364
365 let app = Router::new()
366 .route("/", get(handler_with_cache))
367 .layer(no_cache());
368
369 let request = http::Request::builder()
370 .uri("/")
371 .body(Body::empty())
372 .unwrap();
373
374 let response = app.oneshot(request).await.unwrap();
375 assert_eq!(response.status(), StatusCode::OK);
376 assert_eq!(
377 response
378 .headers()
379 .get(header::CACHE_CONTROL)
380 .unwrap()
381 .to_str()
382 .unwrap(),
383 "private, max-age=60"
384 );
385 }
386
387 #[tokio::test]
388 async fn no_cache_sets_full_directive() {
389 let app = Router::new().route("/", get(ok_handler)).layer(no_cache());
390
391 let request = http::Request::builder()
392 .uri("/")
393 .body(Body::empty())
394 .unwrap();
395
396 let response = app.oneshot(request).await.unwrap();
397 assert_eq!(response.status(), StatusCode::OK);
398 assert_eq!(
399 response
400 .headers()
401 .get(header::CACHE_CONTROL)
402 .unwrap()
403 .to_str()
404 .unwrap(),
405 "no-cache, no-store, must-revalidate"
406 );
407 }
408}