Skip to main content

fraiseql_server/middleware/
content_type.rs

1//! CSRF protection via Content-Type enforcement.
2//!
3//! Rejects POST requests that do not carry `Content-Type: application/json`.
4//! This prevents cross-site request forgery via `text/plain` or
5//! `application/x-www-form-urlencoded` form submissions.
6
7use axum::{
8    body::Body,
9    http::{Method, Request, StatusCode, header::CONTENT_TYPE},
10    middleware::Next,
11    response::{IntoResponse, Response},
12};
13
14/// Middleware that rejects POST requests without a JSON Content-Type.
15///
16/// Non-POST methods pass through unconditionally.
17/// POST requests must have `Content-Type` starting with `application/json`
18/// (e.g. `application/json` or `application/json; charset=utf-8`).
19///
20/// # Errors
21///
22/// Returns a `415 Unsupported Media Type` response if the POST request does not carry a JSON
23/// `Content-Type`.
24pub async fn require_json_content_type(
25    req: Request<Body>,
26    next: Next,
27) -> Result<Response, Response> {
28    if req.method() != Method::POST {
29        return Ok(next.run(req).await);
30    }
31
32    let content_type = req.headers().get(CONTENT_TYPE).and_then(|v| v.to_str().ok()).unwrap_or("");
33
34    if !content_type.starts_with("application/json") {
35        let body = serde_json::json!({
36            "errors": [{
37                "message": "Content-Type must be application/json",
38                "extensions": { "code": "UNSUPPORTED_MEDIA_TYPE" }
39            }]
40        });
41        return Err((
42            StatusCode::UNSUPPORTED_MEDIA_TYPE,
43            [(CONTENT_TYPE, "application/json")],
44            serde_json::to_string(&body).unwrap_or_else(|_| {
45                r#"{"errors":[{"message":"Unsupported Media Type"}]}"#.to_owned()
46            }),
47        )
48            .into_response());
49    }
50
51    Ok(next.run(req).await)
52}