Repository: graphql-rust/juniper (language: rust)

Issue:
juniper_axum breaks on valid Content-Type Header
Given a valid POST with Content-Type header like: `application/json; charset=utf-8`.

juniper_axum responds with:

415 Unsupported Media Type

> `Content-Type` header is expected to be either `application/json` or `application/graphql`

This is because we match on the full header value instead of reading substring and matching on that:

https://github.com/graphql-rust/juniper/blob/257bc69dded08423945d848f8775df9461215a68/juniper_axum/src/extract.rs#L125-L132

Axum handles this case by using a `starts_with` (which matches expectations here: https://httpwg.org/specs/rfc9110.html#media.type)

https://github.com/tokio-rs/axum/blob/280d16a61059f57230819a79b15aa12a263e8cca/examples/parse-body-based-on-content-type/src/main.rs#L63-L76


Discussion / hints:

Fix patch:
diff --git a/juniper_axum/CHANGELOG.md b/juniper_axum/CHANGELOG.md
index 38b8b089b..ad9f4cac5 100644
--- a/juniper_axum/CHANGELOG.md
+++ b/juniper_axum/CHANGELOG.md
@@ -16,6 +16,10 @@ All user visible changes to `juniper_axum` crate will be documented in this file
 
 - Building on `wasm32-unknown-unknown` and `wasm32-wasi` targets. ([#1283], [#1282])
 
+### Fixed
+
+- `Content-Type` header reading full value instead of just the media type. ([#1288])
+
 [#1272]: /../../pull/1272
 [#1282]: /../../issues/1282
 [#1283]: /../../pull/1283
diff --git a/juniper_axum/src/extract.rs b/juniper_axum/src/extract.rs
index fe087f672..1029d05e0 100644
--- a/juniper_axum/src/extract.rs
+++ b/juniper_axum/src/extract.rs
@@ -6,7 +6,7 @@ use axum::{
     async_trait,
     body::Body,
     extract::{FromRequest, FromRequestParts, Query},
-    http::{HeaderValue, Method, Request, StatusCode},
+    http::{header, HeaderValue, Method, Request, StatusCode},
     response::{IntoResponse as _, Response},
     Json, RequestExt as _,
 };
@@ -85,7 +85,7 @@ where
     async fn from_request(mut req: Request<Body>, state: &State) -> Result<Self, Self::Rejection> {
         let content_type = req
             .headers()
-            .get("content-type")
+            .get(header::CONTENT_TYPE)
             .map(HeaderValue::to_str)
             .transpose()
             .map_err(|_| {
@@ -122,7 +122,7 @@ where
                                 .into_response()
                         })
                 }),
-            (&Method::POST, Some("application/json")) => {
+            (&Method::POST, Some(x)) if x.starts_with("application/json") => {
                 Json::<GraphQLBatchRequest<S>>::from_request(req, state)
                     .await
                     .map(|req| Self(req.0))
@@ -130,14 +130,16 @@ where
                         (StatusCode::BAD_REQUEST, format!("Invalid JSON body: {e}")).into_response()
                     })
             }
-            (&Method::POST, Some("application/graphql")) => String::from_request(req, state)
-                .await
-                .map(|body| {
-                    Self(GraphQLBatchRequest::Single(GraphQLRequest::new(
-                        body, None, None,
-                    )))
-                })
-                .map_err(|_| (StatusCode::BAD_REQUEST, "Not valid UTF-8 body").into_response()),
+            (&Method::POST, Some(x)) if x.starts_with("application/graphql") => {
+                String::from_request(req, state)
+                    .await
+                    .map(|body| {
+                        Self(GraphQLBatchRequest::Single(GraphQLRequest::new(
+                            body, None, None,
+                        )))
+                    })
+                    .map_err(|_| (StatusCode::BAD_REQUEST, "Not valid UTF-8 body").into_response())
+            }
             (&Method::POST, _) => Err((
                 StatusCode::UNSUPPORTED_MEDIA_TYPE,
                 "`Content-Type` header is expected to be either `application/json` or \
@@ -246,6 +248,22 @@ mod juniper_request_tests {
         assert_eq!(do_from_request(req).await, expected);
     }
 
+    #[tokio::test]
+    async fn from_json_post_request_with_charset() {
+        let req = Request::post("/")
+            .header("content-type", "application/json; charset=utf-8")
+            .body(Body::from(r#"{"query": "{ add(a: 2, b: 3) }"}"#))
+            .unwrap_or_else(|e| panic!("cannot build `Request`: {e}"));
+
+        let expected = JuniperRequest(GraphQLBatchRequest::Single(GraphQLRequest::new(
+            "{ add(a: 2, b: 3) }".to_string(),
+            None,
+            None,
+        )));
+
+        assert_eq!(do_from_request(req).await, expected);
+    }
+
     #[tokio::test]
     async fn from_graphql_post_request() {
         let req = Request::post("/")
@@ -262,6 +280,22 @@ mod juniper_request_tests {
         assert_eq!(do_from_request(req).await, expected);
     }
 
+    #[tokio::test]
+    async fn from_graphql_post_request_with_charset() {
+        let req = Request::post("/")
+            .header("content-type", "application/graphql; charset=utf-8")
+            .body(Body::from(r#"{ add(a: 2, b: 3) }"#))
+            .unwrap_or_else(|e| panic!("cannot build `Request`: {e}"));
+
+        let expected = JuniperRequest(GraphQLBatchRequest::Single(GraphQLRequest::new(
+            "{ add(a: 2, b: 3) }".to_string(),
+            None,
+            None,
+        )));
+
+        assert_eq!(do_from_request(req).await, expected);
+    }
+
     /// Performs [`JuniperRequest::from_request()`].
     async fn do_from_request(req: Request<Body>) -> JuniperRequest {
         match JuniperRequest::from_request(req, &()).await {


Test patch: