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

Issue:
Support Content-Type: application/graphql
**Is your feature request related to a problem? Please describe.**

I'm trying out Juniper + Hyper and discovered that it doesn't support `Content-Type: application/graphql`.

When submitting an HTTP POST request containing a JSON GraphQL query, like so, it works as expected:

```
$ curl -X POST -H "Content-Type: application/json" -d '{"query":"{user(id:\"1\") {name email}}"}' http://localhost:3000/graphql
{
  "data": {
    "user": {
      "name": "name",
      "email": "name@example.com"
    }
  }
}
```

However, other GraphQL implementations also support `Content-Type: application/graphql` where the entire POST body is treated as the GraphQL query. When I tried it with Juniper + Hyper, I get:

```
$ curl -X POST -H "Content-Type: application/graphql" -d '{user(id: "1") {name email}}' http://localhost:3000/graphql
key must be a string at line 1 column 2
```

It seems that Juniper + Hyper is still treating the POST body as JSON and thus encountering a parsing error.

**Describe the solution you'd like**

Juniper + Hyper should accept `Content-Type: application/graphql` and handle it accordingly, similar to a `GET` query.

**Describe alternatives you've considered**

While JSON payloads are more powerful (since you can include variables), `application/graphql` _is_ commonly accepted by other GraphQL implementations in other languages.

**Additional context**

I have a working juniper-hyper project at https://github.com/aisrael/juniper-hyper-demo


Discussion / hints:
Thanks for the report! Should be an easy PR. I'm a bit slammed at work so I am not sure when I can get around to it. Patches welcome!
Is this mentioned in the spec, or is there a PR in the specs repo?

How does one pass non-inline variable values with this approach?
@theduke Am not sure if it's in the `graphql-spec` anywhere but it is mentioned in the docs: https://graphql.org/learn/serving-over-http/#post-request
That's not official GraphQL docs, that's Apollo FWIW.
@LegNeato , so do you think this update would be nice to have? If so, I could spend some time on it.

Fix patch:
diff --git a/juniper/CHANGELOG.md b/juniper/CHANGELOG.md
index 794042581..8d6f2d094 100644
--- a/juniper/CHANGELOG.md
+++ b/juniper/CHANGELOG.md
@@ -67,6 +67,10 @@ See [#618](https://github.com/graphql-rust/juniper/pull/618).
   `#[graphql(arguments(argA(name = "test")))]`
   (see [#631](https://github.com/graphql-rust/juniper/pull/631))
 
+- Integration tests:
+  Rename `http::tests::HTTPIntegration` as `http::tests::HttpIntegration`
+  and add support for `application/graphql` POST request.
+
 # [[0.14.2] 2019-12-16](https://github.com/graphql-rust/juniper/releases/tag/juniper-0.14.2)
 
 - Fix incorrect validation with non-executed operations [#455](https://github.com/graphql-rust/juniper/issues/455)
diff --git a/juniper/src/http/mod.rs b/juniper/src/http/mod.rs
index 79e3d8d88..ff829d673 100644
--- a/juniper/src/http/mod.rs
+++ b/juniper/src/http/mod.rs
@@ -367,15 +367,23 @@ pub mod tests {
         pub content_type: String,
     }
 
-    /// Normalized way to make requests to the http framework
-    /// integration we are testing.
-    pub trait HTTPIntegration {
+    /// Normalized way to make requests to the HTTP framework integration we are testing.
+    pub trait HttpIntegration {
+        /// Sends GET HTTP request to this integration with the provided `url` parameters string,
+        /// and returns response returned by this integration.
         fn get(&self, url: &str) -> TestResponse;
-        fn post(&self, url: &str, body: &str) -> TestResponse;
+
+        /// Sends POST HTTP request to this integration with the provided JSON-encoded `body`, and
+        /// returns response returned by this integration.
+        fn post_json(&self, url: &str, body: &str) -> TestResponse;
+
+        /// Sends POST HTTP request to this integration with the provided raw GraphQL query as
+        /// `body`, and returns response returned by this integration.
+        fn post_graphql(&self, url: &str, body: &str) -> TestResponse;
     }
 
     #[allow(missing_docs)]
-    pub fn run_http_test_suite<T: HTTPIntegration>(integration: &T) {
+    pub fn run_http_test_suite<T: HttpIntegration>(integration: &T) {
         println!("Running HTTP Test suite for integration");
 
         println!("  - test_simple_get");
@@ -404,6 +412,12 @@ pub mod tests {
 
         println!("  - test_duplicate_keys");
         test_duplicate_keys(integration);
+
+        println!("  - test_graphql_post");
+        test_graphql_post(integration);
+
+        println!("  - test_invalid_graphql_post");
+        test_invalid_graphql_post(integration);
     }
 
     fn unwrap_json_response(response: &TestResponse) -> Json {
@@ -416,7 +430,7 @@ pub mod tests {
         .expect("Could not parse JSON object")
     }
 
-    fn test_simple_get<T: HTTPIntegration>(integration: &T) {
+    fn test_simple_get<T: HttpIntegration>(integration: &T) {
         // {hero{name}}
         let response = integration.get("/?query=%7Bhero%7Bname%7D%7D");
 
@@ -430,7 +444,7 @@ pub mod tests {
         );
     }
 
-    fn test_encoded_get<T: HTTPIntegration>(integration: &T) {
+    fn test_encoded_get<T: HttpIntegration>(integration: &T) {
         // query { human(id: "1000") { id, name, appearsIn, homePlanet } }
         let response = integration.get(
             "/?query=query%20%7B%20human(id%3A%20%221000%22)%20%7B%20id%2C%20name%2C%20appearsIn%2C%20homePlanet%20%7D%20%7D");
@@ -460,7 +474,7 @@ pub mod tests {
         );
     }
 
-    fn test_get_with_variables<T: HTTPIntegration>(integration: &T) {
+    fn test_get_with_variables<T: HttpIntegration>(integration: &T) {
         // query($id: String!) { human(id: $id) { id, name, appearsIn, homePlanet } }
         // with variables = { "id": "1000" }
         let response = integration.get(
@@ -491,8 +505,8 @@ pub mod tests {
         );
     }
 
-    fn test_simple_post<T: HTTPIntegration>(integration: &T) {
-        let response = integration.post("/", r#"{"query": "{hero{name}}"}"#);
+    fn test_simple_post<T: HttpIntegration>(integration: &T) {
+        let response = integration.post_json("/", r#"{"query": "{hero{name}}"}"#);
 
         assert_eq!(response.status_code, 200);
         assert_eq!(response.content_type, "application/json");
@@ -500,12 +514,12 @@ pub mod tests {
         assert_eq!(
             unwrap_json_response(&response),
             serde_json::from_str::<Json>(r#"{"data": {"hero": {"name": "R2-D2"}}}"#)
-                .expect("Invalid JSON constant in test")
+                .expect("Invalid JSON constant in test"),
         );
     }
 
-    fn test_batched_post<T: HTTPIntegration>(integration: &T) {
-        let response = integration.post(
+    fn test_batched_post<T: HttpIntegration>(integration: &T) {
+        let response = integration.post_json(
             "/",
             r#"[{"query": "{hero{name}}"}, {"query": "{hero{name}}"}]"#,
         );
@@ -516,42 +530,57 @@ pub mod tests {
         assert_eq!(
             unwrap_json_response(&response),
             serde_json::from_str::<Json>(
-                r#"[{"data": {"hero": {"name": "R2-D2"}}}, {"data": {"hero": {"name": "R2-D2"}}}]"#
+                r#"[{"data": {"hero": {"name": "R2-D2"}}}, {"data": {"hero": {"name": "R2-D2"}}}]"#,
             )
-            .expect("Invalid JSON constant in test")
+            .expect("Invalid JSON constant in test"),
         );
     }
 
-    fn test_empty_batched_post<T: HTTPIntegration>(integration: &T) {
-        let response = integration.post("/", "[]");
+    fn test_empty_batched_post<T: HttpIntegration>(integration: &T) {
+        let response = integration.post_json("/", "[]");
         assert_eq!(response.status_code, 400);
     }
 
-    fn test_invalid_json<T: HTTPIntegration>(integration: &T) {
+    fn test_invalid_json<T: HttpIntegration>(integration: &T) {
         let response = integration.get("/?query=blah");
         assert_eq!(response.status_code, 400);
-        let response = integration.post("/", r#"blah"#);
+        let response = integration.post_json("/", r#"blah"#);
         assert_eq!(response.status_code, 400);
     }
 
-    fn test_invalid_field<T: HTTPIntegration>(integration: &T) {
+    fn test_invalid_field<T: HttpIntegration>(integration: &T) {
         // {hero{blah}}
         let response = integration.get("/?query=%7Bhero%7Bblah%7D%7D");
         assert_eq!(response.status_code, 400);
-        let response = integration.post("/", r#"{"query": "{hero{blah}}"}"#);
+        let response = integration.post_json("/", r#"{"query": "{hero{blah}}"}"#);
         assert_eq!(response.status_code, 400);
     }
 
-    fn test_duplicate_keys<T: HTTPIntegration>(integration: &T) {
+    fn test_duplicate_keys<T: HttpIntegration>(integration: &T) {
         // {hero{name}}
         let response = integration.get("/?query=%7B%22query%22%3A%20%22%7Bhero%7Bname%7D%7D%22%2C%20%22query%22%3A%20%22%7Bhero%7Bname%7D%7D%22%7D");
         assert_eq!(response.status_code, 400);
-        let response = integration.post(
-            "/",
-            r#"
-            {"query": "{hero{name}}", "query": "{hero{name}}"}
-        "#,
-        );
+        let response =
+            integration.post_json("/", r#"{"query": "{hero{name}}", "query": "{hero{name}}"}"#);
         assert_eq!(response.status_code, 400);
     }
+
+    fn test_graphql_post<T: HttpIntegration>(integration: &T) {
+        let resp = integration.post_graphql("/", r#"{hero{name}}"#);
+
+        assert_eq!(resp.status_code, 200);
+        assert_eq!(resp.content_type, "application/json");
+
+        assert_eq!(
+            unwrap_json_response(&resp),
+            serde_json::from_str::<Json>(r#"{"data": {"hero": {"name": "R2-D2"}}}"#)
+                .expect("Invalid JSON constant in test"),
+        );
+    }
+
+    fn test_invalid_graphql_post<T: HttpIntegration>(integration: &T) {
+        let resp = integration.post_graphql("/", r#"{hero{name}"#);
+
+        assert_eq!(resp.status_code, 400);
+    }
 }
diff --git a/juniper_actix/src/lib.rs b/juniper_actix/src/lib.rs
index 8b2cce094..e85f536d0 100644
--- a/juniper_actix/src/lib.rs
+++ b/juniper_actix/src/lib.rs
@@ -127,15 +127,13 @@ where
     let req = GraphQLRequest::from(get_req.into_inner());
     let gql_response = req.execute(schema, context).await;
     let body_response = serde_json::to_string(&gql_response)?;
-    let response = match gql_response.is_ok() {
-        true => HttpResponse::Ok()
-            .content_type("application/json")
-            .body(body_response),
-        false => HttpResponse::BadRequest()
-            .content_type("application/json")
-            .body(body_response),
+    let mut response = match gql_response.is_ok() {
+        true => HttpResponse::Ok(),
+        false => HttpResponse::BadRequest(),
     };
-    Ok(response)
+    Ok(response
+        .content_type("application/json")
+        .body(body_response))
 }
 
 /// Actix GraphQL Handler for POST requests
@@ -160,13 +158,15 @@ where
         .get(CONTENT_TYPE)
         .and_then(|hv| hv.to_str().ok());
     let req = match content_type_header {
-        Some("application/json") | Some("application/graphql") => {
-            let body_string = String::from_request(&req, &mut payload.into_inner()).await;
-            let body_string = body_string?;
-            match serde_json::from_str::<GraphQLBatchRequest<S>>(&body_string) {
-                Ok(req) => Ok(req),
-                Err(err) => Err(ErrorBadRequest(err)),
-            }
+        Some("application/json") => {
+            let body = String::from_request(&req, &mut payload.into_inner()).await?;
+            serde_json::from_str::<GraphQLBatchRequest<S>>(&body).map_err(ErrorBadRequest)
+        }
+        Some("application/graphql") => {
+            let body = String::from_request(&req, &mut payload.into_inner()).await?;
+            Ok(GraphQLBatchRequest::Single(GraphQLRequest::new(
+                body, None, None,
+            )))
         }
         _ => Err(ErrorUnsupportedMediaType(
             "GraphQL requests should have content type `application/json` or `application/graphql`",
@@ -223,7 +223,7 @@ mod tests {
     use actix_web::{dev::ServiceResponse, http, http::header::CONTENT_TYPE, test, App};
     use futures::StreamExt;
     use juniper::{
-        http::tests::{run_http_test_suite, HTTPIntegration, TestResponse},
+        http::tests::{run_http_test_suite, HttpIntegration, TestResponse},
         tests::{model::Database, schema::Query},
         EmptyMutation, EmptySubscription, RootNode,
     };
@@ -446,63 +446,54 @@ mod tests {
         assert!(result.is_err());
     }
 
-    pub struct TestActixWebIntegration {}
+    pub struct TestActixWebIntegration;
 
-    impl HTTPIntegration for TestActixWebIntegration {
-        fn get(&self, url: &str) -> TestResponse {
-            let url = url.to_string();
-            actix_rt::System::new("get_request").block_on(async move {
-                let schema: Schema = RootNode::new(
+    impl TestActixWebIntegration {
+        fn make_request(&self, req: test::TestRequest) -> TestResponse {
+            actix_rt::System::new("request").block_on(async move {
+                let schema = RootNode::new(
                     Query,
                     EmptyMutation::<Database>::new(),
                     EmptySubscription::<Database>::new(),
                 );
-                let req = test::TestRequest::get()
-                    .header("content-type", "application/json")
-                    .uri(&url.clone())
-                    .to_request();
 
                 let mut app =
-                    test::init_service(App::new().data(schema).route("/", web::get().to(index)))
-                        .await;
+                    test::init_service(App::new().data(schema).route("/", web::to(index))).await;
 
-                let resp = test::call_service(&mut app, req).await;
-                let test_response = make_test_response(resp).await;
-                test_response
+                let resp = test::call_service(&mut app, req.to_request()).await;
+                make_test_response(resp).await
             })
         }
+    }
 
-        fn post(&self, url: &str, body: &str) -> TestResponse {
-            let url = url.to_string();
-            let body = body.to_string();
-            actix_rt::System::new("post_request").block_on(async move {
-                let schema: Schema = RootNode::new(
-                    Query,
-                    EmptyMutation::<Database>::new(),
-                    EmptySubscription::<Database>::new(),
-                );
+    impl HttpIntegration for TestActixWebIntegration {
+        fn get(&self, url: &str) -> TestResponse {
+            self.make_request(test::TestRequest::get().uri(url))
+        }
 
-                let req = test::TestRequest::post()
+        fn post_json(&self, url: &str, body: &str) -> TestResponse {
+            self.make_request(
+                test::TestRequest::post()
                     .header("content-type", "application/json")
-                    .set_payload(body)
-                    .uri(&url.clone())
-                    .to_request();
-
-                let mut app =
-                    test::init_service(App::new().data(schema).route("/", web::post().to(index)))
-                        .await;
+                    .set_payload(body.to_string())
+                    .uri(url),
+            )
+        }
 
-                let resp = test::call_service(&mut app, req).await;
-                let test_response = make_test_response(resp).await;
-                test_response
-            })
+        fn post_graphql(&self, url: &str, body: &str) -> TestResponse {
+            self.make_request(
+                test::TestRequest::post()
+                    .header("content-type", "application/graphql")
+                    .set_payload(body.to_string())
+                    .uri(url),
+            )
         }
     }
 
-    async fn make_test_response(mut response: ServiceResponse) -> TestResponse {
-        let body = take_response_body_string(&mut response).await;
-        let status_code = response.status().as_u16();
-        let content_type = response.headers().get(CONTENT_TYPE).unwrap();
+    async fn make_test_response(mut resp: ServiceResponse) -> TestResponse {
+        let body = take_response_body_string(&mut resp).await;
+        let status_code = resp.status().as_u16();
+        let content_type = resp.headers().get(CONTENT_TYPE).unwrap();
         TestResponse {
             status_code: status_code as i32,
             body: Some(body),
@@ -512,6 +503,6 @@ mod tests {
 
     #[test]
     fn test_actix_web_integration() {
-        run_http_test_suite(&TestActixWebIntegration {});
+        run_http_test_suite(&TestActixWebIntegration);
     }
 }
diff --git a/juniper_hyper/CHANGELOG.md b/juniper_hyper/CHANGELOG.md
index 1acd9f3b4..a6d0d298a 100644
--- a/juniper_hyper/CHANGELOG.md
+++ b/juniper_hyper/CHANGELOG.md
@@ -4,7 +4,9 @@
 
 ## Breaking Changes
 
-- `juniper_hyper::graphiql` now requires a second parameter for subscriptions
+- `juniper_hyper::graphiql` now requires a second parameter for subscriptions.
+- `juniper_hyper::graphql` now executes the schema asynchronously. For blocking synchronous execution consider `juniper_hyper::graphql_sync` for use.
+- `400 Bad Request` is now returned if POST HTTP request contains no or invalid `Content-Type` header.
 
 # [[0.5.2] 2019-12-16](https://github.com/graphql-rust/juniper/releases/tag/juniper_hyper-0.5.2)
 
diff --git a/juniper_hyper/src/lib.rs b/juniper_hyper/src/lib.rs
index 710a609c1..3d1f5554d 100644
--- a/juniper_hyper/src/lib.rs
+++ b/juniper_hyper/src/lib.rs
@@ -8,17 +8,17 @@ use hyper::{
     Body, Method, Request, Response, StatusCode,
 };
 use juniper::{
-    http::{GraphQLBatchRequest, GraphQLRequest as JuniperGraphQLRequest},
+    http::{GraphQLBatchRequest, GraphQLRequest as JuniperGraphQLRequest, GraphQLRequest},
     GraphQLSubscriptionType, GraphQLType, GraphQLTypeAsync, InputValue, RootNode, ScalarValue,
 };
 use serde_json::error::Error as SerdeError;
 use std::{error::Error, fmt, string::FromUtf8Error, sync::Arc};
 use url::form_urlencoded;
 
-pub async fn graphql<CtxT, QueryT, MutationT, SubscriptionT, S>(
+pub async fn graphql_sync<CtxT, QueryT, MutationT, SubscriptionT, S>(
     root_node: Arc<RootNode<'static, QueryT, MutationT, SubscriptionT, S>>,
     context: Arc<CtxT>,
-    request: Request<Body>,
+    req: Request<Body>,
 ) -> Result<Response<Body>, hyper::Error>
 where
     S: ScalarValue + Send + Sync + 'static,
@@ -30,31 +30,16 @@ where
     MutationT::TypeInfo: Send + Sync,
     SubscriptionT::TypeInfo: Send + Sync,
 {
-    match *request.method() {
-        Method::GET => {
-            let gql_req = parse_get_req(request);
-
-            match gql_req {
-                Ok(gql_req) => Ok(execute_request(root_node, context, gql_req).await),
-                Err(err) => Ok(render_error(err)),
-            }
-        }
-        Method::POST => {
-            let gql_req = parse_post_req(request.into_body()).await;
-
-            match gql_req {
-                Ok(gql_req) => Ok(execute_request(root_node, context, gql_req).await),
-                Err(err) => Ok(render_error(err)),
-            }
-        }
-        _ => Ok(new_response(StatusCode::METHOD_NOT_ALLOWED)),
-    }
+    Ok(match parse_req(req).await {
+        Ok(req) => execute_request_sync(root_node, context, req).await,
+        Err(resp) => resp,
+    })
 }
 
-pub async fn graphql_async<CtxT, QueryT, MutationT, SubscriptionT, S>(
+pub async fn graphql<CtxT, QueryT, MutationT, SubscriptionT, S>(
     root_node: Arc<RootNode<'static, QueryT, MutationT, SubscriptionT, S>>,
     context: Arc<CtxT>,
-    request: Request<Body>,
+    req: Request<Body>,
 ) -> Result<Response<Body>, hyper::Error>
 where
     S: ScalarValue + Send + Sync + 'static,
@@ -66,25 +51,31 @@ where
     MutationT::TypeInfo: Send + Sync,
     SubscriptionT::TypeInfo: Send + Sync,
 {
-    match *request.method() {
-        Method::GET => {
-            let gql_req = parse_get_req(request);
+    Ok(match parse_req(req).await {
+        Ok(req) => execute_request(root_node, context, req).await,
+        Err(resp) => resp,
+    })
+}
 
-            match gql_req {
-                Ok(gql_req) => Ok(execute_request_async(root_node, context, gql_req).await),
-                Err(err) => Ok(render_error(err)),
-            }
-        }
+async fn parse_req<S: ScalarValue>(
+    req: Request<Body>,
+) -> Result<GraphQLBatchRequest<S>, Response<Body>> {
+    match *req.method() {
+        Method::GET => parse_get_req(req),
         Method::POST => {
-            let gql_req = parse_post_req(request.into_body()).await;
-
-            match gql_req {
-                Ok(gql_req) => Ok(execute_request_async(root_node, context, gql_req).await),
-                Err(err) => Ok(render_error(err)),
+            let content_type = req
+                .headers()
+                .get(header::CONTENT_TYPE)
+                .map(HeaderValue::to_str);
+            match content_type {
+                Some(Ok("application/json")) => parse_post_json_req(req.into_body()).await,
+                Some(Ok("application/graphql")) => parse_post_graphql_req(req.into_body()).await,
+                _ => return Err(new_response(StatusCode::BAD_REQUEST)),
             }
         }
-        _ => Ok(new_response(StatusCode::METHOD_NOT_ALLOWED)),
+        _ => return Err(new_response(StatusCode::METHOD_NOT_ALLOWED)),
     }
+    .map_err(|e| render_error(e))
 }
 
 fn parse_get_req<S: ScalarValue>(
@@ -100,7 +91,7 @@ fn parse_get_req<S: ScalarValue>(
         })
 }
 
-async fn parse_post_req<S: ScalarValue>(
+async fn parse_post_json_req<S: ScalarValue>(
     body: Body,
 ) -> Result<GraphQLBatchRequest<S>, GraphQLRequestError> {
     let chunk = hyper::body::to_bytes(body)
@@ -114,6 +105,21 @@ async fn parse_post_req<S: ScalarValue>(
         .map_err(GraphQLRequestError::BodyJSONError)
 }
 
+async fn parse_post_graphql_req<S: ScalarValue>(
+    body: Body,
+) -> Result<GraphQLBatchRequest<S>, GraphQLRequestError> {
+    let chunk = hyper::body::to_bytes(body)
+        .await
+        .map_err(GraphQLRequestError::BodyHyper)?;
+
+    let query = String::from_utf8(chunk.iter().cloned().collect())
+        .map_err(GraphQLRequestError::BodyUtf8)?;
+
+    Ok(GraphQLBatchRequest::Single(GraphQLRequest::new(
+        query, None, None,
+    )))
+}
+
 pub async fn graphiql(
     graphql_endpoint: &str,
     subscriptions_endpoint: Option<&str>,
@@ -146,7 +152,7 @@ fn render_error(err: GraphQLRequestError) -> Response<Body> {
     resp
 }
 
-async fn execute_request<CtxT, QueryT, MutationT, SubscriptionT, S>(
+async fn execute_request_sync<CtxT, QueryT, MutationT, SubscriptionT, S>(
     root_node: Arc<RootNode<'static, QueryT, MutationT, SubscriptionT, S>>,
     context: Arc<CtxT>,
     request: GraphQLBatchRequest<S>,
@@ -177,7 +183,7 @@ where
     resp
 }
 
-async fn execute_request_async<CtxT, QueryT, MutationT, SubscriptionT, S>(
+async fn execute_request<CtxT, QueryT, MutationT, SubscriptionT, S>(
     root_node: Arc<RootNode<'static, QueryT, MutationT, SubscriptionT, S>>,
     context: Arc<CtxT>,
     request: GraphQLBatchRequest<S>,
@@ -319,19 +325,34 @@ mod tests {
     use reqwest::{self, Response as ReqwestResponse};
     use std::{net::SocketAddr, sync::Arc, thread, time::Duration};
 
-    struct TestHyperIntegration;
+    struct TestHyperIntegration {
+        port: u16,
+    }
 
-    impl http_tests::HTTPIntegration for TestHyperIntegration {
+    impl http_tests::HttpIntegration for TestHyperIntegration {
         fn get(&self, url: &str) -> http_tests::TestResponse {
-            let url = format!("http://127.0.0.1:3001/graphql{}", url);
+            let url = format!("http://127.0.0.1:{}/graphql{}", self.port, url);
             make_test_response(reqwest::get(&url).expect(&format!("failed GET {}", url)))
         }
 
-        fn post(&self, url: &str, body: &str) -> http_tests::TestResponse {
-            let url = format!("http://127.0.0.1:3001/graphql{}", url);
+        fn post_json(&self, url: &str, body: &str) -> http_tests::TestResponse {
+            let url = format!("http://127.0.0.1:{}/graphql{}", self.port, url);
+            let client = reqwest::Client::new();
+            let res = client
+                .post(&url)
+                .header(reqwest::header::CONTENT_TYPE, "application/json")
+                .body(body.to_string())
+                .send()
+                .expect(&format!("failed POST {}", url));
+            make_test_response(res)
+        }
+
+        fn post_graphql(&self, url: &str, body: &str) -> http_tests::TestResponse {
+            let url = format!("http://127.0.0.1:{}/graphql{}", self.port, url);
             let client = reqwest::Client::new();
             let res = client
                 .post(&url)
+                .header(reqwest::header::CONTENT_TYPE, "application/graphql")
                 .body(body.to_string())
                 .send()
                 .expect(&format!("failed POST {}", url));
@@ -356,9 +377,9 @@ mod tests {
         }
     }
 
-    #[tokio::test]
-    async fn test_hyper_integration() {
-        let addr: SocketAddr = ([127, 0, 0, 1], 3001).into();
+    async fn run_hyper_integration(is_sync: bool) {
+        let port = if is_sync { 3002 } else { 3001 };
+        let addr: SocketAddr = ([127, 0, 0, 1], port).into();
 
         let db = Arc::new(Database::new());
         let root_node = Arc::new(RootNode::new(
@@ -386,11 +407,15 @@ mod tests {
                     };
                     async move {
                         if matches {
-                            super::graphql(root_node, ctx, req).await
+                            if is_sync {
+                                super::graphql_sync(root_node, ctx, req).await
+                            } else {
+                                super::graphql(root_node, ctx, req).await
+                            }
                         } else {
-                            let mut response = Response::new(Body::empty());
-                            *response.status_mut() = StatusCode::NOT_FOUND;
-                            Ok(response)
+                            let mut resp = Response::new(Body::empty());
+                            *resp.status_mut() = StatusCode::NOT_FOUND;
+                            Ok(resp)
                         }
                     }
                 }))
@@ -409,7 +434,7 @@ mod tests {
 
         tokio::task::spawn_blocking(move || {
             thread::sleep(Duration::from_millis(10)); // wait 10ms for server to bind
-            let integration = TestHyperIntegration;
+            let integration = TestHyperIntegration { port };
             http_tests::run_http_test_suite(&integration);
             shutdown.abort();
         });
@@ -418,4 +443,14 @@ mod tests {
             eprintln!("server error: {}", e);
         }
     }
+
+    #[tokio::test]
+    async fn test_hyper_integration() {
+        run_hyper_integration(false).await
+    }
+
+    #[tokio::test]
+    async fn test_sync_hyper_integration() {
+        run_hyper_integration(true).await
+    }
 }
diff --git a/juniper_iron/CHANGELOG.md b/juniper_iron/CHANGELOG.md
index 83d83dad9..5c4574d90 100644
--- a/juniper_iron/CHANGELOG.md
+++ b/juniper_iron/CHANGELOG.md
@@ -4,7 +4,8 @@
 
 ## Breaking Changes
 
-- `juniper_iron::GraphiQLHandler::new` now requires a second parameter for subscriptions
+- `juniper_iron::GraphiQLHandler::new` now requires a second parameter for subscriptions.
+- `400 Bad Request` is now returned if POST HTTP request contains no or invalid `Content-Type` header.
 
 # [[0.6.2] 2019-12-16](https://github.com/graphql-rust/juniper/releases/tag/juniper_iron-0.6.2)
 
diff --git a/juniper_iron/src/lib.rs b/juniper_iron/src/lib.rs
index de7045740..dc506465f 100644
--- a/juniper_iron/src/lib.rs
+++ b/juniper_iron/src/lib.rs
@@ -114,10 +114,17 @@ extern crate iron_test;
 #[cfg(test)]
 extern crate url;
 
-use iron::{itry, method, middleware::Handler, mime::Mime, prelude::*, status};
+use iron::{
+    headers::ContentType,
+    itry, method,
+    middleware::Handler,
+    mime::{Mime, TopLevel},
+    prelude::*,
+    status,
+};
 use urlencoded::{UrlDecodingError, UrlEncodedQuery};
 
-use std::{error::Error, fmt, io::Read};
+use std::{error::Error, fmt, io::Read, ops::Deref as _};
 
 use serde_json::error::Error as SerdeError;
 
@@ -228,32 +235,41 @@ where
     }
 
     fn handle_get(&self, req: &mut Request) -> IronResult<GraphQLBatchRequest<S>> {
-        let url_query_string = req
+        let url_query = req
             .get_mut::<UrlEncodedQuery>()
             .map_err(GraphQLIronError::Url)?;
 
-        let input_query = parse_url_param(url_query_string.remove("query"))?
+        let query = parse_url_param(url_query.remove("query"))?
             .ok_or_else(|| GraphQLIronError::InvalidData("No query provided"))?;
-        let operation_name = parse_url_param(url_query_string.remove("operationName"))?;
-        let variables = parse_variable_param(url_query_string.remove("variables"))?;
+        let operation_name = parse_url_param(url_query.remove("operationName"))?;
+        let variables = parse_variable_param(url_query.remove("variables"))?;
 
         Ok(GraphQLBatchRequest::Single(http::GraphQLRequest::new(
-            input_query,
+            query,
             operation_name,
             variables,
         )))
     }
 
-    fn handle_post(&self, req: &mut Request) -> IronResult<GraphQLBatchRequest<S>> {
-        let mut request_payload = String::new();
-        itry!(req.body.read_to_string(&mut request_payload));
+    fn handle_post_json(&self, req: &mut Request) -> IronResult<GraphQLBatchRequest<S>> {
+        let mut payload = String::new();
+        itry!(req.body.read_to_string(&mut payload));
 
         Ok(
-            serde_json::from_str::<GraphQLBatchRequest<S>>(request_payload.as_str())
+            serde_json::from_str::<GraphQLBatchRequest<S>>(payload.as_str())
                 .map_err(GraphQLIronError::Serde)?,
         )
     }
 
+    fn handle_post_graphql(&self, req: &mut Request) -> IronResult<GraphQLBatchRequest<S>> {
+        let mut payload = String::new();
+        itry!(req.body.read_to_string(&mut payload));
+
+        Ok(GraphQLBatchRequest::Single(http::GraphQLRequest::new(
+            payload, None, None,
+        )))
+    }
+
     fn execute_sync(
         &self,
         context: &CtxT,
@@ -313,7 +329,14 @@ where
 
         let graphql_request = match req.method {
             method::Get => self.handle_get(&mut req)?,
-            method::Post => self.handle_post(&mut req)?,
+            method::Post => match req.headers.get::<ContentType>().map(ContentType::deref) {
+                Some(Mime(TopLevel::Application, sub_lvl, _)) => match sub_lvl.as_str() {
+                    "json" => self.handle_post_json(&mut req)?,
+                    "graphql" => self.handle_post_graphql(&mut req)?,
+                    _ => return Ok(Response::with(status::BadRequest)),
+                },
+                _ => return Ok(Response::with(status::BadRequest)),
+            },
             _ => return Ok(Response::with(status::MethodNotAllowed)),
         };
 
@@ -388,7 +411,11 @@ impl From<GraphQLIronError> for IronError {
 #[cfg(test)]
 mod tests {
     use super::*;
-    use iron::{Handler, Headers, Url};
+    use iron::{
+        headers::ContentType,
+        mime::{Mime, SubLevel, TopLevel},
+        Handler, Headers, Url,
+    };
     use iron_test::{request, response};
     use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS};
 
@@ -411,7 +438,7 @@ mod tests {
             .path()
             .iter()
             .map(|x| (*x).to_string())
-            .collect::<Vec<String>>()
+            .collect::<Vec<_>>()
             .join("/");
         format!(
             "http://localhost:3000{}?{}",
@@ -422,21 +449,31 @@ mod tests {
 
     struct TestIronIntegration;
 
-    impl http_tests::HTTPIntegration for TestIronIntegration {
+    impl http_tests::HttpIntegration for TestIronIntegration {
         fn get(&self, url: &str) -> http_tests::TestResponse {
-            let result = request::get(&fixup_url(url), Headers::new(), &make_handler());
-            match result {
-                Ok(response) => make_test_response(response),
-                Err(e) => make_test_error_response(e),
-            }
+            request::get(&fixup_url(url), Headers::new(), &make_handler())
+                .map(make_test_response)
+                .unwrap_or_else(make_test_error_response)
+        }
+
+        fn post_json(&self, url: &str, body: &str) -> http_tests::TestResponse {
+            let mut headers = Headers::new();
+            headers.set(ContentType::json());
+            request::post(&fixup_url(url), headers, body, &make_handler())
+                .map(make_test_response)
+                .unwrap_or_else(make_test_error_response)
         }
 
-        fn post(&self, url: &str, body: &str) -> http_tests::TestResponse {
-            let result = request::post(&fixup_url(url), Headers::new(), body, &make_handler());
-            match result {
-                Ok(response) => make_test_response(response),
-                Err(e) => make_test_error_response(e),
-            }
+        fn post_graphql(&self, url: &str, body: &str) -> http_tests::TestResponse {
+            let mut headers = Headers::new();
+            headers.set(ContentType(Mime(
+                TopLevel::Application,
+                SubLevel::Ext("graphql".into()),
+                vec![],
+            )));
+            request::post(&fixup_url(url), headers, body, &make_handler())
+                .map(make_test_response)
+                .unwrap_or_else(make_test_error_response)
         }
     }
 
diff --git a/juniper_rocket/CHANGELOG.md b/juniper_rocket/CHANGELOG.md
index 5359987bd..880ef3411 100644
--- a/juniper_rocket/CHANGELOG.md
+++ b/juniper_rocket/CHANGELOG.md
@@ -2,6 +2,7 @@
 
 - Compatibility with the latest `juniper`.
 - Rocket integration does not require default features.
+- Support `application/graphql` POST requests.
 
 ## Breaking Changes
 
diff --git a/juniper_rocket/src/lib.rs b/juniper_rocket/src/lib.rs
index 8816ac8df..e6d29ca2b 100644
--- a/juniper_rocket/src/lib.rs
+++ b/juniper_rocket/src/lib.rs
@@ -47,7 +47,7 @@ use rocket::{
     request::{FormItems, FromForm, FromFormValue},
     response::{content, Responder, Response},
     Data,
-    Outcome::{Failure, Forward, Success},
+    Outcome::{Forward, Success},
     Request,
 };
 
@@ -271,20 +271,26 @@ where
 {
     type Error = String;
 
-    fn from_data(request: &Request, data: Data) -> FromDataOutcome<Self, Self::Error> {
-        if !request.content_type().map_or(false, |ct| ct.is_json()) {
-            return Forward(data);
-        }
+    fn from_data(req: &Request, data: Data) -> FromDataOutcome<Self, Self::Error> {
+        let content_type = req
+            .content_type()
+            .map(|ct| (ct.top().as_str(), ct.sub().as_str()));
+        let is_json = match content_type {
+            Some(("application", "json")) => true,
+            Some(("application", "graphql")) => false,
+            _ => return Forward(data),
+        };
 
         let mut body = String::new();
-        if let Err(e) = data.open().read_to_string(&mut body) {
-            return Failure((Status::InternalServerError, format!("{:?}", e)));
-        }
+        data.open()
+            .read_to_string(&mut body)
+            .map_err(|e| Err((Status::InternalServerError, format!("{:?}", e))))?;
 
-        match serde_json::from_str(&body) {
-            Ok(value) => Success(GraphQLRequest(value)),
-            Err(failure) => Failure((Status::BadRequest, format!("{}", failure))),
-        }
+        Success(GraphQLRequest(if is_json {
+            serde_json::from_str(&body).map_err(|e| Err((Status::BadRequest, format!("{}", e))))?
+        } else {
+            GraphQLBatchRequest::Single(http::GraphQLRequest::new(body, None, None))
+        }))
     }
 }
 
@@ -456,16 +462,25 @@ mod tests {
         client: Client,
     }
 
-    impl http_tests::HTTPIntegration for TestRocketIntegration {
+    impl http_tests::HttpIntegration for TestRocketIntegration {
         fn get(&self, url: &str) -> http_tests::TestResponse {
             let req = &self.client.get(url);
             make_test_response(req)
         }
 
-        fn post(&self, url: &str, body: &str) -> http_tests::TestResponse {
+        fn post_json(&self, url: &str, body: &str) -> http_tests::TestResponse {
             let req = &self.client.post(url).header(ContentType::JSON).body(body);
             make_test_response(req)
         }
+
+        fn post_graphql(&self, url: &str, body: &str) -> http_tests::TestResponse {
+            let req = &self
+                .client
+                .post(url)
+                .header(ContentType::new("application", "graphql"))
+                .body(body);
+            make_test_response(req)
+        }
     }
 
     #[test]
diff --git a/juniper_rocket_async/CHANGELOG.md b/juniper_rocket_async/CHANGELOG.md
index 6778af051..d2889166f 100644
--- a/juniper_rocket_async/CHANGELOG.md
+++ b/juniper_rocket_async/CHANGELOG.md
@@ -2,6 +2,7 @@
 
 - Compatibility with the latest `juniper`.
 - Rocket integration does not require default features.
+- Support `application/graphql` POST requests.
 
 # [[0.5.1] 2019-10-24](https://github.com/graphql-rust/juniper/releases/tag/juniper_rocket-0.5.1)
 
diff --git a/juniper_rocket_async/src/lib.rs b/juniper_rocket_async/src/lib.rs
index efbb8c303..1a9788153 100644
--- a/juniper_rocket_async/src/lib.rs
+++ b/juniper_rocket_async/src/lib.rs
@@ -294,12 +294,17 @@ where
 {
     type Error = String;
 
-    fn from_data(request: &Request, data: Data) -> FromDataFuture<'static, Self, Self::Error> {
+    fn from_data(req: &Request, data: Data) -> FromDataFuture<'static, Self, Self::Error> {
         use tokio::io::AsyncReadExt as _;
 
-        if !request.content_type().map_or(false, |ct| ct.is_json()) {
-            return Box::pin(async move { Forward(data) });
-        }
+        let content_type = req
+            .content_type()
+            .map(|ct| (ct.top().as_str(), ct.sub().as_str()));
+        let is_json = match content_type {
+            Some(("application", "json")) => true,
+            Some(("application", "graphql")) => false,
+            _ => return Box::pin(async move { Forward(data) }),
+        };
 
         Box::pin(async move {
             let mut body = String::new();
@@ -308,10 +313,14 @@ where
                 return Failure((Status::InternalServerError, format!("{:?}", e)));
             }
 
-            match serde_json::from_str(&body) {
-                Ok(value) => Success(GraphQLRequest(value)),
-                Err(failure) => Failure((Status::BadRequest, format!("{}", failure))),
-            }
+            Success(GraphQLRequest(if is_json {
+                match serde_json::from_str(&body) {
+                    Ok(req) => req,
+                    Err(e) => return Failure((Status::BadRequest, format!("{}", e))),
+                }
+            } else {
+                GraphQLBatchRequest::Single(http::GraphQLRequest::new(body, None, None))
+            }))
         })
     }
 }
@@ -495,18 +504,28 @@ mod tests {
         client: Client,
     }
 
-    impl http_tests::HTTPIntegration for TestRocketIntegration {
+    impl http_tests::HttpIntegration for TestRocketIntegration {
         fn get(&self, url: &str) -> http_tests::TestResponse {
             let req = self.client.get(url);
             let req = futures::executor::block_on(req.dispatch());
             futures::executor::block_on(make_test_response(req))
         }
 
-        fn post(&self, url: &str, body: &str) -> http_tests::TestResponse {
+        fn post_json(&self, url: &str, body: &str) -> http_tests::TestResponse {
             let req = self.client.post(url).header(ContentType::JSON).body(body);
             let req = futures::executor::block_on(req.dispatch());
             futures::executor::block_on(make_test_response(req))
         }
+
+        fn post_graphql(&self, url: &str, body: &str) -> http_tests::TestResponse {
+            let req = self
+                .client
+                .post(url)
+                .header(ContentType::new("application", "graphql"))
+                .body(body);
+            let req = futures::executor::block_on(req.dispatch());
+            futures::executor::block_on(make_test_response(req))
+        }
     }
 
     #[test]
diff --git a/juniper_warp/CHANGELOG.md b/juniper_warp/CHANGELOG.md
index 9cbd277eb..70a8fe905 100644
--- a/juniper_warp/CHANGELOG.md
+++ b/juniper_warp/CHANGELOG.md
@@ -6,10 +6,11 @@ to `juniper` to be reused in other http integrations, since this implementation
 
 ## Breaking Changes
 
-- Update `playground_filter` to support subscription endpoint URLs
-- Update `warp` to 0.2
+- Update `playground_filter` to support subscription endpoint URLs.
+- Update `warp` to 0.2.
 - Rename synchronous `execute` to `execute_sync`, add asynchronous `execute`
-- `juniper_warp::graphiql_filter` now requires a second parameter for subscriptions
+- `juniper_warp::graphiql_filter` now requires a second parameter for subscriptions.
+- `make_graphql_filter` and `make_graphql_filter_sync` now ignore POST HTTP requests with no or invalid `Content-Type` header.
 
 # [[0.5.2] 2019-12-16](https://github.com/graphql-rust/juniper/releases/tag/juniper_warp-0.5.2)
 
diff --git a/juniper_warp/Cargo.toml b/juniper_warp/Cargo.toml
index f1de53c27..d27cf2338 100644
--- a/juniper_warp/Cargo.toml
+++ b/juniper_warp/Cargo.toml
@@ -12,18 +12,19 @@ edition = "2018"
 subscriptions = ["juniper_subscriptions"]
 
 [dependencies]
-warp = "0.2"
+bytes = "0.5"
+failure = "0.1.7"
 futures = "0.3.1"
 juniper = { version = "0.14.2", path = "../juniper", default-features = false  }
-juniper_subscriptions = { path = "../juniper_subscriptions", optional = true}
-tokio = { version = "0.2", features = ["rt-core", "blocking"] }
+juniper_subscriptions = { path = "../juniper_subscriptions", optional = true }
 serde = { version = "1.0.75", features = ["derive"] }
 serde_json = "1.0.24"
-failure = "0.1.7"
+tokio = { version = "0.2", features = ["blocking", "rt-core"] }
+warp = "0.2"
 
 [dev-dependencies]
-juniper = { version = "0.14.2", path = "../juniper", features = ["expose-test-schema", "serde_json"] }
 env_logger = "0.5.11"
+juniper = { version = "0.14.2", path = "../juniper", features = ["expose-test-schema", "serde_json"] }
 log = "0.4.3"
 percent-encoding = "1.0"
-tokio = { version = "0.2", features = ["rt-core", "macros", "blocking"] }
+tokio = { version = "0.2", features = ["blocking", "macros", "rt-core"] }
diff --git a/juniper_warp/src/lib.rs b/juniper_warp/src/lib.rs
index 471d68bc6..8579be708 100644
--- a/juniper_warp/src/lib.rs
+++ b/juniper_warp/src/lib.rs
@@ -40,12 +40,16 @@ Check the LICENSE file for details.
 #![deny(warnings)]
 #![doc(html_root_url = "https://docs.rs/juniper_warp/0.2.0")]
 
-use std::{pin::Pin, sync::Arc};
-
-use futures::{Future, FutureExt as _, TryFutureExt};
-use juniper::{http::GraphQLBatchRequest, ScalarValue};
+use std::{collections::HashMap, str, sync::Arc};
+
+use bytes::Bytes;
+use futures::{FutureExt as _, TryFutureExt};
+use juniper::{
+    http::{GraphQLBatchRequest, GraphQLRequest},
+    ScalarValue,
+};
 use tokio::task;
-use warp::{filters::BoxedFilter, Filter};
+use warp::{body, filters::BoxedFilter, header, http, query, Filter};
 
 /// Make a filter for graphql queries/mutations.
 ///
@@ -110,7 +114,7 @@ use warp::{filters::BoxedFilter, Filter};
 pub fn make_graphql_filter<Query, Mutation, Subscription, Context, S>(
     schema: juniper::RootNode<'static, Query, Mutation, Subscription, S>,
     context_extractor: BoxedFilter<(Context,)>,
-) -> BoxedFilter<(warp::http::Response<Vec<u8>>,)>
+) -> BoxedFilter<(http::Response<Vec<u8>>,)>
 where
     S: ScalarValue + Send + Sync + 'static,
     Context: Send + Sync + 'static,
@@ -122,65 +126,90 @@ where
     Subscription::TypeInfo: Send + Sync,
 {
     let schema = Arc::new(schema);
-    let post_schema = schema.clone();
-
-    let handle_post_request = move |context: Context, request: GraphQLBatchRequest<S>| {
-        let schema = post_schema.clone();
+    let post_json_schema = schema.clone();
+    let post_graphql_schema = schema.clone();
 
-        Box::pin(async move {
-            let res = request.execute(&schema, &context).await;
+    let handle_post_json_request = move |context: Context, req: GraphQLBatchRequest<S>| {
+        let schema = post_json_schema.clone();
+        async move {
+            let resp = req.execute(&schema, &context).await;
 
             Ok::<_, warp::Rejection>(build_response(
-                serde_json::to_vec(&res)
-                    .map(|json| (json, res.is_ok()))
+                serde_json::to_vec(&resp)
+                    .map(|json| (json, resp.is_ok()))
                     .map_err(Into::into),
             ))
-        })
+        }
     };
-
-    let post_filter = warp::post()
+    let post_json_filter = warp::post()
+        .and(header::exact_ignore_case(
+            "content-type",
+            "application/json",
+        ))
         .and(context_extractor.clone())
-        .and(warp::body::json())
-        .and_then(handle_post_request);
-
-    let handle_get_request =
-        move |context: Context, mut request: std::collections::HashMap<String, String>| {
-            let schema = schema.clone();
-
-            async move {
-                let variables = match request.remove("variables") {
-                    None => None,
-                    Some(vs) => serde_json::from_str(&vs)?,
-                };
-
-                let graphql_request = juniper::http::GraphQLRequest::new(
-                    request.remove("query").ok_or_else(|| {
-                        failure::format_err!("Missing GraphQL query string in query parameters")
-                    })?,
-                    request.get("operation_name").map(|s| s.to_owned()),
-                    variables,
-                );
-
-                let response = graphql_request.execute(&schema, &context).await;
-
-                Ok((serde_json::to_vec(&response)?, response.is_ok()))
-            }
-            .then(|result| async move { Ok::<_, warp::Rejection>(build_response(result)) })
-        };
+        .and(body::json())
+        .and_then(handle_post_json_request);
+
+    let handle_post_graphql_request = move |context: Context, body: Bytes| {
+        let schema = post_graphql_schema.clone();
+        async move {
+            let query = str::from_utf8(body.as_ref()).map_err(|e| {
+                failure::format_err!("Request body query is not a valid UTF-8 string: {}", e)
+            })?;
+            let req = GraphQLRequest::new(query.into(), None, None);
+
+            let resp = req.execute(&schema, &context).await;
+
+            Ok((serde_json::to_vec(&resp)?, resp.is_ok()))
+        }
+        .then(|res| async { Ok::<_, warp::Rejection>(build_response(res)) })
+    };
+    let post_graphql_filter = warp::post()
+        .and(header::exact_ignore_case(
+            "content-type",
+            "application/graphql",
+        ))
+        .and(context_extractor.clone())
+        .and(body::bytes())
+        .and_then(handle_post_graphql_request);
 
+    let handle_get_request = move |context: Context, mut qry: HashMap<String, String>| {
+        let schema = schema.clone();
+        async move {
+            let req = GraphQLRequest::new(
+                qry.remove("query").ok_or_else(|| {
+                    failure::format_err!("Missing GraphQL query string in query parameters")
+                })?,
+                qry.remove("operation_name"),
+                qry.remove("variables")
+                    .map(|vs| serde_json::from_str(&vs))
+                    .transpose()?,
+            );
+
+            let resp = req.execute(&schema, &context).await;
+
+            Ok((serde_json::to_vec(&resp)?, resp.is_ok()))
+        }
+        .then(|res| async move { Ok::<_, warp::Rejection>(build_response(res)) })
+    };
     let get_filter = warp::get()
         .and(context_extractor)
-        .and(warp::filters::query::query())
+        .and(query::query())
         .and_then(handle_get_request);
 
-    get_filter.or(post_filter).unify().boxed()
+    get_filter
+        .or(post_json_filter)
+        .unify()
+        .or(post_graphql_filter)
+        .unify()
+        .boxed()
 }
 
 /// Make a synchronous filter for graphql endpoint.
 pub fn make_graphql_filter_sync<Query, Mutation, Subscription, Context, S>(
     schema: juniper::RootNode<'static, Query, Mutation, Subscription, S>,
     context_extractor: BoxedFilter<(Context,)>,
-) -> BoxedFilter<(warp::http::Response<Vec<u8>>,)>
+) -> BoxedFilter<(http::Response<Vec<u8>>,)>
 where
     S: ScalarValue + Send + Sync + 'static,
     Context: Send + Sync + 'static,
@@ -189,105 +218,116 @@ where
     Subscription: juniper::GraphQLType<S, Context = Context, TypeInfo = ()> + Send + Sync + 'static,
 {
     let schema = Arc::new(schema);
-    let post_schema = schema.clone();
-
-    let handle_post_request =
-        move |context: Context, request: GraphQLBatchRequest<S>| -> Response {
-            let schema = post_schema.clone();
-
-            Box::pin(
-                async move {
-                    let result = task::spawn_blocking(move || {
-                        let response = request.execute_sync(&schema, &context);
-                        Ok((serde_json::to_vec(&response)?, response.is_ok()))
-                    })
-                    .await?;
-
-                    Ok(build_response(result))
-                }
-                .map_err(|e: task::JoinError| warp::reject::custom(JoinError(e))),
-            )
-        };
-
-    let post_filter = warp::post()
+    let post_json_schema = schema.clone();
+    let post_graphql_schema = schema.clone();
+
+    let handle_post_json_request = move |context: Context, req: GraphQLBatchRequest<S>| {
+        let schema = post_json_schema.clone();
+        async move {
+            let res = task::spawn_blocking(move || {
+                let resp = req.execute_sync(&schema, &context);
+                Ok((serde_json::to_vec(&resp)?, resp.is_ok()))
+            })
+            .await?;
+
+            Ok(build_response(res))
+        }
+        .map_err(|e: task::JoinError| warp::reject::custom(JoinError(e)))
+    };
+    let post_json_filter = warp::post()
+        .and(header::exact_ignore_case(
+            "content-type",
+            "application/json",
+        ))
         .and(context_extractor.clone())
-        .and(warp::body::json())
-        .and_then(handle_post_request);
+        .and(body::json())
+        .and_then(handle_post_json_request);
+
+    let handle_post_graphql_request = move |context: Context, body: Bytes| {
+        let schema = post_graphql_schema.clone();
+        async move {
+            let res = task::spawn_blocking(move || {
+                let query = str::from_utf8(body.as_ref()).map_err(|e| {
+                    failure::format_err!("Request body is not a valid UTF-8 string: {}", e)
+                })?;
+                let req = GraphQLRequest::new(query.into(), None, None);
+
+                let resp = req.execute_sync(&schema, &context);
+                Ok((serde_json::to_vec(&resp)?, resp.is_ok()))
+            })
+            .await?;
+
+            Ok(build_response(res))
+        }
+        .map_err(|e: task::JoinError| warp::reject::custom(JoinError(e)))
+    };
+    let post_graphql_filter = warp::post()
+        .and(header::exact_ignore_case(
+            "content-type",
+            "application/graphql",
+        ))
+        .and(context_extractor.clone())
+        .and(body::bytes())
+        .and_then(handle_post_graphql_request);
 
-    let handle_get_request = move |context: Context,
-                                   mut request: std::collections::HashMap<String, String>|
-          -> Response {
+    let handle_get_request = move |context: Context, mut qry: HashMap<String, String>| {
         let schema = schema.clone();
+        async move {
+            let res = task::spawn_blocking(move || {
+                let req = GraphQLRequest::new(
+                    qry.remove("query").ok_or_else(|| {
+                        failure::format_err!("Missing GraphQL query string in query parameters")
+                    })?,
+                    qry.remove("operation_name"),
+                    qry.remove("variables")
+                        .map(|vs| serde_json::from_str(&vs))
+                        .transpose()?,
+                );
 
-        Box::pin(
-            async move {
-                let result = task::spawn_blocking(move || {
-                    let variables = match request.remove("variables") {
-                        None => None,
-                        Some(vs) => serde_json::from_str(&vs)?,
-                    };
-
-                    let graphql_request = juniper::http::GraphQLRequest::new(
-                        request.remove("query").ok_or_else(|| {
-                            failure::format_err!("Missing GraphQL query string in query parameters")
-                        })?,
-                        request.get("operation_name").map(|s| s.to_owned()),
-                        variables,
-                    );
-
-                    let response = graphql_request.execute_sync(&schema, &context);
-                    Ok((serde_json::to_vec(&response)?, response.is_ok()))
-                })
-                .await?;
+                let resp = req.execute_sync(&schema, &context);
+                Ok((serde_json::to_vec(&resp)?, resp.is_ok()))
+            })
+            .await?;
 
-                Ok(build_response(result))
-            }
-            .map_err(|e: task::JoinError| warp::reject::custom(JoinError(e))),
-        )
+            Ok(build_response(res))
+        }
+        .map_err(|e: task::JoinError| warp::reject::custom(JoinError(e)))
     };
-
     let get_filter = warp::get()
         .and(context_extractor)
-        .and(warp::filters::query::query())
+        .and(query::query())
         .and_then(handle_get_request);
 
-    get_filter.or(post_filter).unify().boxed()
+    get_filter
+        .or(post_json_filter)
+        .unify()
+        .or(post_graphql_filter)
+        .unify()
+        .boxed()
 }
 
-/// Error raised by `tokio_threadpool` if the thread pool
-/// has been shutdown
+/// Error raised by `tokio_threadpool` if the thread pool has been shutdown.
 ///
-/// Wrapper type is needed as inner type does not implement `warp::reject::Reject`
+/// Wrapper type is needed as inner type does not implement `warp::reject::Reject`.
+#[derive(Debug)]
 pub struct JoinError(task::JoinError);
 
 impl warp::reject::Reject for JoinError {}
 
-impl std::fmt::Debug for JoinError {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        write!(f, "JoinError({:?})", self.0)
-    }
-}
-
-fn build_response(
-    response: Result<(Vec<u8>, bool), failure::Error>,
-) -> warp::http::Response<Vec<u8>> {
+fn build_response(response: Result<(Vec<u8>, bool), failure::Error>) -> http::Response<Vec<u8>> {
     match response {
-        Ok((body, is_ok)) => warp::http::Response::builder()
+        Ok((body, is_ok)) => http::Response::builder()
             .status(if is_ok { 200 } else { 400 })
             .header("content-type", "application/json")
             .body(body)
             .expect("response is valid"),
-        Err(_) => warp::http::Response::builder()
-            .status(warp::http::StatusCode::INTERNAL_SERVER_ERROR)
+        Err(_) => http::Response::builder()
+            .status(http::StatusCode::INTERNAL_SERVER_ERROR)
             .body(Vec::new())
             .expect("status code is valid"),
     }
 }
 
-type Response = Pin<
-    Box<dyn Future<Output = Result<warp::http::Response<Vec<u8>>, warp::reject::Rejection>> + Send>,
->;
-
 /// Create a filter that replies with an HTML page containing GraphiQL. This does not handle routing, so you can mount it on any endpoint.
 ///
 /// For example:
@@ -318,7 +358,7 @@ type Response = Pin<
 pub fn graphiql_filter(
     graphql_endpoint_url: &'static str,
     subscriptions_endpoint: Option<&'static str>,
-) -> warp::filters::BoxedFilter<(warp::http::Response<Vec<u8>>,)> {
+) -> warp::filters::BoxedFilter<(http::Response<Vec<u8>>,)> {
     warp::any()
         .map(move || graphiql_response(graphql_endpoint_url, subscriptions_endpoint))
         .boxed()
@@ -327,8 +367,8 @@ pub fn graphiql_filter(
 fn graphiql_response(
     graphql_endpoint_url: &'static str,
     subscriptions_endpoint: Option<&'static str>,
-) -> warp::http::Response<Vec<u8>> {
-    warp::http::Response::builder()
+) -> http::Response<Vec<u8>> {
+    http::Response::builder()
         .header("content-type", "text/html;charset=utf-8")
         .body(
             juniper::http::graphiql::graphiql_source(graphql_endpoint_url, subscriptions_endpoint)
@@ -341,7 +381,7 @@ fn graphiql_response(
 pub fn playground_filter(
     graphql_endpoint_url: &'static str,
     subscriptions_endpoint_url: Option<&'static str>,
-) -> warp::filters::BoxedFilter<(warp::http::Response<Vec<u8>>,)> {
+) -> warp::filters::BoxedFilter<(http::Response<Vec<u8>>,)> {
     warp::any()
         .map(move || playground_response(graphql_endpoint_url, subscriptions_endpoint_url))
         .boxed()
@@ -350,8 +390,8 @@ pub fn playground_filter(
 fn playground_response(
     graphql_endpoint_url: &'static str,
     subscriptions_endpoint_url: Option<&'static str>,
-) -> warp::http::Response<Vec<u8>> {
-    warp::http::Response::builder()
+) -> http::Response<Vec<u8>> {
+    http::Response::builder()
         .header("content-type", "text/html;charset=utf-8")
         .body(
             juniper::http::playground::playground_source(
@@ -780,100 +820,122 @@ mod tests {
     }
 }
 
-//TODO: update warp tests
-//#[cfg(test)]
-//mod tests_http_harness {
-//    use super::*;
-//    use juniper::{
-//        http::tests::{run_http_test_suite, HTTPIntegration, TestResponse},
-//        tests::{model::Database, schema::Query},
-//        EmptyMutation, EmptySubscription, RootNode,
-//    };
-//    use warp::{self, Filter};
-//
-//    type Schema =
-//        juniper::RootNode<'static, Query, EmptyMutation<Database>, EmptySubscription<Database>>;
-//
-//    fn warp_server() -> warp::filters::BoxedFilter<(warp::http::Response<Vec<u8>>,)> {
-//        let schema: Schema = RootNode::new(
-//            Query,
-//            EmptyMutation::<Database>::new(),
-//            EmptySubscription::<Database>::new(),
-//        );
-//
-//        let state = warp::any().map(move || Database::new());
-//        let filter = warp::filters::path::end().and(make_graphql_filter(schema, state.boxed()));
-//
-//        filter.boxed()
-//    }
-//
-//    struct TestWarpIntegration {
-//        filter: warp::filters::BoxedFilter<(warp::http::Response<Vec<u8>>,)>,
-//    }
-//
-//    // This can't be implemented with the From trait since TestResponse is not defined in this crate.
-//    fn test_response_from_http_response(response: warp::http::Response<Vec<u8>>) -> TestResponse {
-//        TestResponse {
-//            status_code: response.status().as_u16() as i32,
-//            body: Some(String::from_utf8(response.body().to_owned()).unwrap()),
-//            content_type: response
-//                .headers()
-//                .get("content-type")
-//                .expect("missing content-type header in warp response")
-//                .to_str()
-//                .expect("invalid content-type string")
-//                .to_owned(),
-//        }
-//    }
-//
-//    impl HTTPIntegration for TestWarpIntegration {
-//        fn get(&self, url: &str) -> TestResponse {
-//            use percent_encoding::{percent_encode, DEFAULT_ENCODE_SET};
-//            let url: String = percent_encode(url.replace("/?", "").as_bytes(), DEFAULT_ENCODE_SET)
-//                .into_iter()
-//                .collect::<Vec<_>>()
-//                .join("");
-//
-//            let response = warp::test::request()
-//                .method("GET")
-//                .path(&format!("/?{}", url))
-//                .filter(&self.filter)
-//                .await
-//                .unwrap_or_else(|rejection| {
-//                    warp::http::Response::builder()
-//                        .status(rejection.status())
-//                        .header("content-type", "application/json")
-//                        .body(Vec::new())
-//                        .unwrap()
-//                });
-//            test_response_from_http_response(response)
-//        }
-//
-//        fn post(&self, url: &str, body: &str) -> TestResponse {
-//            let response = warp::test::request()
-//                .method("POST")
-//                .header("content-type", "application/json")
-//                .path(url)
-//                .body(body)
-//                .filter(&self.filter)
-//                .await
-//                .unwrap_or_else(|rejection| {
-//                    warp::http::Response::builder()
-//                        .status(rejection.status())
-//                        .header("content-type", "application/json")
-//                        .body(Vec::new())
-//                        .unwrap()
-//                });
-//            test_response_from_http_response(response)
-//        }
-//    }
-//
-//    #[test]
-//    fn test_warp_integration() {
-//        let integration = TestWarpIntegration {
-//            filter: warp_server(),
-//        };
-//
-//        run_http_test_suite(&integration);
-//    }
-//}
+#[cfg(test)]
+mod tests_http_harness {
+    use super::*;
+    use juniper::{
+        http::tests::{run_http_test_suite, HttpIntegration, TestResponse},
+        tests::{model::Database, schema::Query},
+        EmptyMutation, EmptySubscription, RootNode,
+    };
+    use warp::{
+        self,
+        filters::{path, BoxedFilter},
+        Filter,
+    };
+
+    struct TestWarpIntegration {
+        filter: BoxedFilter<(http::Response<Vec<u8>>,)>,
+    }
+
+    impl TestWarpIntegration {
+        fn new(is_sync: bool) -> Self {
+            let schema = RootNode::new(
+                Query,
+                EmptyMutation::<Database>::new(),
+                EmptySubscription::<Database>::new(),
+            );
+            let state = warp::any().map(move || Database::new());
+
+            let filter = path::end().and(if is_sync {
+                make_graphql_filter_sync(schema, state.boxed())
+            } else {
+                make_graphql_filter(schema, state.boxed())
+            });
+            Self {
+                filter: filter.boxed(),
+            }
+        }
+
+        fn make_request(&self, req: warp::test::RequestBuilder) -> TestResponse {
+            let mut rt = tokio::runtime::Runtime::new().expect("Failed to create tokio::Runtime");
+            make_test_response(rt.block_on(async move {
+                req.filter(&self.filter).await.unwrap_or_else(|rejection| {
+                    let code = if rejection.is_not_found() {
+                        http::StatusCode::NOT_FOUND
+                    } else if let Some(body::BodyDeserializeError { .. }) = rejection.find() {
+                        http::StatusCode::BAD_REQUEST
+                    } else {
+                        http::StatusCode::INTERNAL_SERVER_ERROR
+                    };
+                    http::Response::builder()
+                        .status(code)
+                        .header("content-type", "application/json")
+                        .body(Vec::new())
+                        .unwrap()
+                })
+            }))
+        }
+    }
+
+    impl HttpIntegration for TestWarpIntegration {
+        fn get(&self, url: &str) -> TestResponse {
+            use percent_encoding::{utf8_percent_encode, QUERY_ENCODE_SET};
+
+            let url: String = utf8_percent_encode(&url.replace("/?", ""), QUERY_ENCODE_SET)
+                .into_iter()
+                .collect::<Vec<_>>()
+                .join("");
+
+            self.make_request(
+                warp::test::request()
+                    .method("GET")
+                    .path(&format!("/?{}", url)),
+            )
+        }
+
+        fn post_json(&self, url: &str, body: &str) -> TestResponse {
+            self.make_request(
+                warp::test::request()
+                    .method("POST")
+                    .header("content-type", "application/json")
+                    .path(url)
+                    .body(body),
+            )
+        }
+
+        fn post_graphql(&self, url: &str, body: &str) -> TestResponse {
+            self.make_request(
+                warp::test::request()
+                    .method("POST")
+                    .header("content-type", "application/graphql")
+                    .path(url)
+                    .body(body),
+            )
+        }
+    }
+
+    fn make_test_response(resp: http::Response<Vec<u8>>) -> TestResponse {
+        TestResponse {
+            status_code: resp.status().as_u16() as i32,
+            body: Some(String::from_utf8(resp.body().to_owned()).unwrap()),
+            content_type: resp
+                .headers()
+                .get("content-type")
+                .expect("missing content-type header in warp response")
+                .to_str()
+                .expect("invalid content-type string")
+                .to_owned(),
+        }
+    }
+
+    #[test]
+    fn test_warp_integration() {
+        run_http_test_suite(&TestWarpIntegration::new(false));
+    }
+
+    #[test]
+    fn test_sync_warp_integration() {
+        run_http_test_suite(&TestWarpIntegration::new(true));
+    }
+}


Test patch: