Support "Content-Type: application/graphql" (#426) (#654)

* Support 'application/graphql' POST requests for 'juniper_warp'

* Add integration tests for 'application/graphql' POST requests and revive HttpIntegration test suite for 'juniper_warp'

* Support 'application/graphql' POST requests for 'juniper_hyper' and run its tests for both sync and async versions

* Run integration tests for both sync and async versions of 'juniper_warp' and update its CHANGELOG

* Support 'application/graphql' POST requests for 'juniper_iron'

* Fix 'application/graphql' POST requests support for 'juniper_actix'

* Support 'application/graphql' POST requests in 'juniper_rocket' and 'juniper_rocket_async'

* Upd juniper's CHANGELOG
This commit is contained in:
Kai Ren 2020-05-08 19:00:49 +03:00 committed by GitHub
parent d13305f202
commit aedb2d1eb4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 622 additions and 423 deletions

View file

@ -67,6 +67,10 @@ See [#618](https://github.com/graphql-rust/juniper/pull/618).
`#[graphql(arguments(argA(name = "test")))]` `#[graphql(arguments(argA(name = "test")))]`
(see [#631](https://github.com/graphql-rust/juniper/pull/631)) (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) # [[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) - Fix incorrect validation with non-executed operations [#455](https://github.com/graphql-rust/juniper/issues/455)

View file

@ -367,15 +367,23 @@ pub mod tests {
pub content_type: String, pub content_type: String,
} }
/// Normalized way to make requests to the http framework /// Normalized way to make requests to the HTTP framework integration we are testing.
/// integration we are testing. pub trait HttpIntegration {
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 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)] #[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!("Running HTTP Test suite for integration");
println!(" - test_simple_get"); println!(" - test_simple_get");
@ -404,6 +412,12 @@ pub mod tests {
println!(" - test_duplicate_keys"); println!(" - test_duplicate_keys");
test_duplicate_keys(integration); 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 { fn unwrap_json_response(response: &TestResponse) -> Json {
@ -416,7 +430,7 @@ pub mod tests {
.expect("Could not parse JSON object") .expect("Could not parse JSON object")
} }
fn test_simple_get<T: HTTPIntegration>(integration: &T) { fn test_simple_get<T: HttpIntegration>(integration: &T) {
// {hero{name}} // {hero{name}}
let response = integration.get("/?query=%7Bhero%7Bname%7D%7D"); 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 } } // query { human(id: "1000") { id, name, appearsIn, homePlanet } }
let response = integration.get( 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"); "/?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 } } // query($id: String!) { human(id: $id) { id, name, appearsIn, homePlanet } }
// with variables = { "id": "1000" } // with variables = { "id": "1000" }
let response = integration.get( let response = integration.get(
@ -491,8 +505,8 @@ pub mod tests {
); );
} }
fn test_simple_post<T: HTTPIntegration>(integration: &T) { fn test_simple_post<T: HttpIntegration>(integration: &T) {
let response = integration.post("/", r#"{"query": "{hero{name}}"}"#); let response = integration.post_json("/", r#"{"query": "{hero{name}}"}"#);
assert_eq!(response.status_code, 200); assert_eq!(response.status_code, 200);
assert_eq!(response.content_type, "application/json"); assert_eq!(response.content_type, "application/json");
@ -500,12 +514,12 @@ pub mod tests {
assert_eq!( assert_eq!(
unwrap_json_response(&response), unwrap_json_response(&response),
serde_json::from_str::<Json>(r#"{"data": {"hero": {"name": "R2-D2"}}}"#) 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) { fn test_batched_post<T: HttpIntegration>(integration: &T) {
let response = integration.post( let response = integration.post_json(
"/", "/",
r#"[{"query": "{hero{name}}"}, {"query": "{hero{name}}"}]"#, r#"[{"query": "{hero{name}}"}, {"query": "{hero{name}}"}]"#,
); );
@ -516,42 +530,57 @@ pub mod tests {
assert_eq!( assert_eq!(
unwrap_json_response(&response), unwrap_json_response(&response),
serde_json::from_str::<Json>( 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) { fn test_empty_batched_post<T: HttpIntegration>(integration: &T) {
let response = integration.post("/", "[]"); let response = integration.post_json("/", "[]");
assert_eq!(response.status_code, 400); 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"); let response = integration.get("/?query=blah");
assert_eq!(response.status_code, 400); 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); assert_eq!(response.status_code, 400);
} }
fn test_invalid_field<T: HTTPIntegration>(integration: &T) { fn test_invalid_field<T: HttpIntegration>(integration: &T) {
// {hero{blah}} // {hero{blah}}
let response = integration.get("/?query=%7Bhero%7Bblah%7D%7D"); let response = integration.get("/?query=%7Bhero%7Bblah%7D%7D");
assert_eq!(response.status_code, 400); 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); assert_eq!(response.status_code, 400);
} }
fn test_duplicate_keys<T: HTTPIntegration>(integration: &T) { fn test_duplicate_keys<T: HttpIntegration>(integration: &T) {
// {hero{name}} // {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"); 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); assert_eq!(response.status_code, 400);
let response = integration.post( let response =
"/", integration.post_json("/", r#"{"query": "{hero{name}}", "query": "{hero{name}}"}"#);
r#"
{"query": "{hero{name}}", "query": "{hero{name}}"}
"#,
);
assert_eq!(response.status_code, 400); 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);
}
} }

View file

@ -127,15 +127,13 @@ where
let req = GraphQLRequest::from(get_req.into_inner()); let req = GraphQLRequest::from(get_req.into_inner());
let gql_response = req.execute(schema, context).await; let gql_response = req.execute(schema, context).await;
let body_response = serde_json::to_string(&gql_response)?; let body_response = serde_json::to_string(&gql_response)?;
let response = match gql_response.is_ok() { let mut response = match gql_response.is_ok() {
true => HttpResponse::Ok() true => HttpResponse::Ok(),
.content_type("application/json") false => HttpResponse::BadRequest(),
.body(body_response),
false => HttpResponse::BadRequest()
.content_type("application/json")
.body(body_response),
}; };
Ok(response) Ok(response
.content_type("application/json")
.body(body_response))
} }
/// Actix GraphQL Handler for POST requests /// Actix GraphQL Handler for POST requests
@ -160,13 +158,15 @@ where
.get(CONTENT_TYPE) .get(CONTENT_TYPE)
.and_then(|hv| hv.to_str().ok()); .and_then(|hv| hv.to_str().ok());
let req = match content_type_header { let req = match content_type_header {
Some("application/json") | Some("application/graphql") => { Some("application/json") => {
let body_string = String::from_request(&req, &mut payload.into_inner()).await; let body = String::from_request(&req, &mut payload.into_inner()).await?;
let body_string = body_string?; serde_json::from_str::<GraphQLBatchRequest<S>>(&body).map_err(ErrorBadRequest)
match serde_json::from_str::<GraphQLBatchRequest<S>>(&body_string) { }
Ok(req) => Ok(req), Some("application/graphql") => {
Err(err) => Err(ErrorBadRequest(err)), let body = String::from_request(&req, &mut payload.into_inner()).await?;
} Ok(GraphQLBatchRequest::Single(GraphQLRequest::new(
body, None, None,
)))
} }
_ => Err(ErrorUnsupportedMediaType( _ => Err(ErrorUnsupportedMediaType(
"GraphQL requests should have content type `application/json` or `application/graphql`", "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 actix_web::{dev::ServiceResponse, http, http::header::CONTENT_TYPE, test, App};
use futures::StreamExt; use futures::StreamExt;
use juniper::{ use juniper::{
http::tests::{run_http_test_suite, HTTPIntegration, TestResponse}, http::tests::{run_http_test_suite, HttpIntegration, TestResponse},
tests::{model::Database, schema::Query}, tests::{model::Database, schema::Query},
EmptyMutation, EmptySubscription, RootNode, EmptyMutation, EmptySubscription, RootNode,
}; };
@ -446,63 +446,54 @@ mod tests {
assert!(result.is_err()); assert!(result.is_err());
} }
pub struct TestActixWebIntegration {} pub struct TestActixWebIntegration;
impl HTTPIntegration for TestActixWebIntegration { impl TestActixWebIntegration {
fn get(&self, url: &str) -> TestResponse { fn make_request(&self, req: test::TestRequest) -> TestResponse {
let url = url.to_string(); actix_rt::System::new("request").block_on(async move {
actix_rt::System::new("get_request").block_on(async move { let schema = RootNode::new(
let schema: 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;
let resp = test::call_service(&mut app, req).await;
let test_response = make_test_response(resp).await;
test_response
})
}
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, Query,
EmptyMutation::<Database>::new(), EmptyMutation::<Database>::new(),
EmptySubscription::<Database>::new(), EmptySubscription::<Database>::new(),
); );
let req = test::TestRequest::post()
.header("content-type", "application/json")
.set_payload(body)
.uri(&url.clone())
.to_request();
let mut app = let mut app =
test::init_service(App::new().data(schema).route("/", web::post().to(index))) test::init_service(App::new().data(schema).route("/", web::to(index))).await;
.await;
let resp = test::call_service(&mut app, req).await; let resp = test::call_service(&mut app, req.to_request()).await;
let test_response = make_test_response(resp).await; make_test_response(resp).await
test_response
}) })
} }
} }
async fn make_test_response(mut response: ServiceResponse) -> TestResponse { impl HttpIntegration for TestActixWebIntegration {
let body = take_response_body_string(&mut response).await; fn get(&self, url: &str) -> TestResponse {
let status_code = response.status().as_u16(); self.make_request(test::TestRequest::get().uri(url))
let content_type = response.headers().get(CONTENT_TYPE).unwrap(); }
fn post_json(&self, url: &str, body: &str) -> TestResponse {
self.make_request(
test::TestRequest::post()
.header("content-type", "application/json")
.set_payload(body.to_string())
.uri(url),
)
}
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 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 { TestResponse {
status_code: status_code as i32, status_code: status_code as i32,
body: Some(body), body: Some(body),
@ -512,6 +503,6 @@ mod tests {
#[test] #[test]
fn test_actix_web_integration() { fn test_actix_web_integration() {
run_http_test_suite(&TestActixWebIntegration {}); run_http_test_suite(&TestActixWebIntegration);
} }
} }

View file

@ -4,7 +4,9 @@
## Breaking Changes ## 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) # [[0.5.2] 2019-12-16](https://github.com/graphql-rust/juniper/releases/tag/juniper_hyper-0.5.2)

View file

@ -8,17 +8,17 @@ use hyper::{
Body, Method, Request, Response, StatusCode, Body, Method, Request, Response, StatusCode,
}; };
use juniper::{ use juniper::{
http::{GraphQLBatchRequest, GraphQLRequest as JuniperGraphQLRequest}, http::{GraphQLBatchRequest, GraphQLRequest as JuniperGraphQLRequest, GraphQLRequest},
GraphQLSubscriptionType, GraphQLType, GraphQLTypeAsync, InputValue, RootNode, ScalarValue, GraphQLSubscriptionType, GraphQLType, GraphQLTypeAsync, InputValue, RootNode, ScalarValue,
}; };
use serde_json::error::Error as SerdeError; use serde_json::error::Error as SerdeError;
use std::{error::Error, fmt, string::FromUtf8Error, sync::Arc}; use std::{error::Error, fmt, string::FromUtf8Error, sync::Arc};
use url::form_urlencoded; 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>>, root_node: Arc<RootNode<'static, QueryT, MutationT, SubscriptionT, S>>,
context: Arc<CtxT>, context: Arc<CtxT>,
request: Request<Body>, req: Request<Body>,
) -> Result<Response<Body>, hyper::Error> ) -> Result<Response<Body>, hyper::Error>
where where
S: ScalarValue + Send + Sync + 'static, S: ScalarValue + Send + Sync + 'static,
@ -30,31 +30,16 @@ where
MutationT::TypeInfo: Send + Sync, MutationT::TypeInfo: Send + Sync,
SubscriptionT::TypeInfo: Send + Sync, SubscriptionT::TypeInfo: Send + Sync,
{ {
match *request.method() { Ok(match parse_req(req).await {
Method::GET => { Ok(req) => execute_request_sync(root_node, context, req).await,
let gql_req = parse_get_req(request); Err(resp) => resp,
})
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)),
}
} }
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>>, root_node: Arc<RootNode<'static, QueryT, MutationT, SubscriptionT, S>>,
context: Arc<CtxT>, context: Arc<CtxT>,
request: Request<Body>, req: Request<Body>,
) -> Result<Response<Body>, hyper::Error> ) -> Result<Response<Body>, hyper::Error>
where where
S: ScalarValue + Send + Sync + 'static, S: ScalarValue + Send + Sync + 'static,
@ -66,25 +51,31 @@ where
MutationT::TypeInfo: Send + Sync, MutationT::TypeInfo: Send + Sync,
SubscriptionT::TypeInfo: Send + Sync, SubscriptionT::TypeInfo: Send + Sync,
{ {
match *request.method() { Ok(match parse_req(req).await {
Method::GET => { Ok(req) => execute_request(root_node, context, req).await,
let gql_req = parse_get_req(request); Err(resp) => resp,
})
}
match gql_req { async fn parse_req<S: ScalarValue>(
Ok(gql_req) => Ok(execute_request_async(root_node, context, gql_req).await), req: Request<Body>,
Err(err) => Ok(render_error(err)), ) -> Result<GraphQLBatchRequest<S>, Response<Body>> {
} match *req.method() {
} Method::GET => parse_get_req(req),
Method::POST => { Method::POST => {
let gql_req = parse_post_req(request.into_body()).await; let content_type = req
.headers()
match gql_req { .get(header::CONTENT_TYPE)
Ok(gql_req) => Ok(execute_request_async(root_node, context, gql_req).await), .map(HeaderValue::to_str);
Err(err) => Ok(render_error(err)), 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>( 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, body: Body,
) -> Result<GraphQLBatchRequest<S>, GraphQLRequestError> { ) -> Result<GraphQLBatchRequest<S>, GraphQLRequestError> {
let chunk = hyper::body::to_bytes(body) let chunk = hyper::body::to_bytes(body)
@ -114,6 +105,21 @@ async fn parse_post_req<S: ScalarValue>(
.map_err(GraphQLRequestError::BodyJSONError) .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( pub async fn graphiql(
graphql_endpoint: &str, graphql_endpoint: &str,
subscriptions_endpoint: Option<&str>, subscriptions_endpoint: Option<&str>,
@ -146,7 +152,7 @@ fn render_error(err: GraphQLRequestError) -> Response<Body> {
resp 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>>, root_node: Arc<RootNode<'static, QueryT, MutationT, SubscriptionT, S>>,
context: Arc<CtxT>, context: Arc<CtxT>,
request: GraphQLBatchRequest<S>, request: GraphQLBatchRequest<S>,
@ -177,7 +183,7 @@ where
resp 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>>, root_node: Arc<RootNode<'static, QueryT, MutationT, SubscriptionT, S>>,
context: Arc<CtxT>, context: Arc<CtxT>,
request: GraphQLBatchRequest<S>, request: GraphQLBatchRequest<S>,
@ -319,19 +325,34 @@ mod tests {
use reqwest::{self, Response as ReqwestResponse}; use reqwest::{self, Response as ReqwestResponse};
use std::{net::SocketAddr, sync::Arc, thread, time::Duration}; 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 { 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))) make_test_response(reqwest::get(&url).expect(&format!("failed GET {}", url)))
} }
fn post(&self, url: &str, body: &str) -> http_tests::TestResponse { fn post_json(&self, url: &str, body: &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);
let client = reqwest::Client::new(); let client = reqwest::Client::new();
let res = client let res = client
.post(&url) .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()) .body(body.to_string())
.send() .send()
.expect(&format!("failed POST {}", url)); .expect(&format!("failed POST {}", url));
@ -356,9 +377,9 @@ mod tests {
} }
} }
#[tokio::test] async fn run_hyper_integration(is_sync: bool) {
async fn test_hyper_integration() { let port = if is_sync { 3002 } else { 3001 };
let addr: SocketAddr = ([127, 0, 0, 1], 3001).into(); let addr: SocketAddr = ([127, 0, 0, 1], port).into();
let db = Arc::new(Database::new()); let db = Arc::new(Database::new());
let root_node = Arc::new(RootNode::new( let root_node = Arc::new(RootNode::new(
@ -386,11 +407,15 @@ mod tests {
}; };
async move { async move {
if matches { 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 { } else {
let mut response = Response::new(Body::empty()); let mut resp = Response::new(Body::empty());
*response.status_mut() = StatusCode::NOT_FOUND; *resp.status_mut() = StatusCode::NOT_FOUND;
Ok(response) Ok(resp)
} }
} }
})) }))
@ -409,7 +434,7 @@ mod tests {
tokio::task::spawn_blocking(move || { tokio::task::spawn_blocking(move || {
thread::sleep(Duration::from_millis(10)); // wait 10ms for server to bind 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); http_tests::run_http_test_suite(&integration);
shutdown.abort(); shutdown.abort();
}); });
@ -418,4 +443,14 @@ mod tests {
eprintln!("server error: {}", e); 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
}
} }

View file

@ -4,7 +4,8 @@
## Breaking Changes ## 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) # [[0.6.2] 2019-12-16](https://github.com/graphql-rust/juniper/releases/tag/juniper_iron-0.6.2)

View file

@ -114,10 +114,17 @@ extern crate iron_test;
#[cfg(test)] #[cfg(test)]
extern crate url; 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 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; use serde_json::error::Error as SerdeError;
@ -228,32 +235,41 @@ where
} }
fn handle_get(&self, req: &mut Request) -> IronResult<GraphQLBatchRequest<S>> { fn handle_get(&self, req: &mut Request) -> IronResult<GraphQLBatchRequest<S>> {
let url_query_string = req let url_query = req
.get_mut::<UrlEncodedQuery>() .get_mut::<UrlEncodedQuery>()
.map_err(GraphQLIronError::Url)?; .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"))?; .ok_or_else(|| GraphQLIronError::InvalidData("No query provided"))?;
let operation_name = parse_url_param(url_query_string.remove("operationName"))?; let operation_name = parse_url_param(url_query.remove("operationName"))?;
let variables = parse_variable_param(url_query_string.remove("variables"))?; let variables = parse_variable_param(url_query.remove("variables"))?;
Ok(GraphQLBatchRequest::Single(http::GraphQLRequest::new( Ok(GraphQLBatchRequest::Single(http::GraphQLRequest::new(
input_query, query,
operation_name, operation_name,
variables, variables,
))) )))
} }
fn handle_post(&self, req: &mut Request) -> IronResult<GraphQLBatchRequest<S>> { fn handle_post_json(&self, req: &mut Request) -> IronResult<GraphQLBatchRequest<S>> {
let mut request_payload = String::new(); let mut payload = String::new();
itry!(req.body.read_to_string(&mut request_payload)); itry!(req.body.read_to_string(&mut payload));
Ok( Ok(
serde_json::from_str::<GraphQLBatchRequest<S>>(request_payload.as_str()) serde_json::from_str::<GraphQLBatchRequest<S>>(payload.as_str())
.map_err(GraphQLIronError::Serde)?, .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( fn execute_sync(
&self, &self,
context: &CtxT, context: &CtxT,
@ -313,7 +329,14 @@ where
let graphql_request = match req.method { let graphql_request = match req.method {
method::Get => self.handle_get(&mut req)?, 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)), _ => return Ok(Response::with(status::MethodNotAllowed)),
}; };
@ -388,7 +411,11 @@ impl From<GraphQLIronError> for IronError {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use iron::{Handler, Headers, Url}; use iron::{
headers::ContentType,
mime::{Mime, SubLevel, TopLevel},
Handler, Headers, Url,
};
use iron_test::{request, response}; use iron_test::{request, response};
use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS}; use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS};
@ -411,7 +438,7 @@ mod tests {
.path() .path()
.iter() .iter()
.map(|x| (*x).to_string()) .map(|x| (*x).to_string())
.collect::<Vec<String>>() .collect::<Vec<_>>()
.join("/"); .join("/");
format!( format!(
"http://localhost:3000{}?{}", "http://localhost:3000{}?{}",
@ -422,21 +449,31 @@ mod tests {
struct TestIronIntegration; struct TestIronIntegration;
impl http_tests::HTTPIntegration for TestIronIntegration { impl http_tests::HttpIntegration for TestIronIntegration {
fn get(&self, url: &str) -> http_tests::TestResponse { fn get(&self, url: &str) -> http_tests::TestResponse {
let result = request::get(&fixup_url(url), Headers::new(), &make_handler()); request::get(&fixup_url(url), Headers::new(), &make_handler())
match result { .map(make_test_response)
Ok(response) => make_test_response(response), .unwrap_or_else(make_test_error_response)
Err(e) => make_test_error_response(e),
}
} }
fn post(&self, url: &str, body: &str) -> http_tests::TestResponse { fn post_json(&self, url: &str, body: &str) -> http_tests::TestResponse {
let result = request::post(&fixup_url(url), Headers::new(), body, &make_handler()); let mut headers = Headers::new();
match result { headers.set(ContentType::json());
Ok(response) => make_test_response(response), request::post(&fixup_url(url), headers, body, &make_handler())
Err(e) => make_test_error_response(e), .map(make_test_response)
} .unwrap_or_else(make_test_error_response)
}
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)
} }
} }

View file

@ -2,6 +2,7 @@
- Compatibility with the latest `juniper`. - Compatibility with the latest `juniper`.
- Rocket integration does not require default features. - Rocket integration does not require default features.
- Support `application/graphql` POST requests.
## Breaking Changes ## Breaking Changes

View file

@ -47,7 +47,7 @@ use rocket::{
request::{FormItems, FromForm, FromFormValue}, request::{FormItems, FromForm, FromFormValue},
response::{content, Responder, Response}, response::{content, Responder, Response},
Data, Data,
Outcome::{Failure, Forward, Success}, Outcome::{Forward, Success},
Request, Request,
}; };
@ -271,20 +271,26 @@ where
{ {
type Error = String; type Error = String;
fn from_data(request: &Request, data: Data) -> FromDataOutcome<Self, Self::Error> { fn from_data(req: &Request, data: Data) -> FromDataOutcome<Self, Self::Error> {
if !request.content_type().map_or(false, |ct| ct.is_json()) { let content_type = req
return Forward(data); .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(); let mut body = String::new();
if let Err(e) = data.open().read_to_string(&mut body) { data.open()
return Failure((Status::InternalServerError, format!("{:?}", e))); .read_to_string(&mut body)
} .map_err(|e| Err((Status::InternalServerError, format!("{:?}", e))))?;
match serde_json::from_str(&body) { Success(GraphQLRequest(if is_json {
Ok(value) => Success(GraphQLRequest(value)), serde_json::from_str(&body).map_err(|e| Err((Status::BadRequest, format!("{}", e))))?
Err(failure) => Failure((Status::BadRequest, format!("{}", failure))), } else {
} GraphQLBatchRequest::Single(http::GraphQLRequest::new(body, None, None))
}))
} }
} }
@ -456,16 +462,25 @@ mod tests {
client: Client, client: Client,
} }
impl http_tests::HTTPIntegration for TestRocketIntegration { impl http_tests::HttpIntegration for TestRocketIntegration {
fn get(&self, url: &str) -> http_tests::TestResponse { fn get(&self, url: &str) -> http_tests::TestResponse {
let req = &self.client.get(url); let req = &self.client.get(url);
make_test_response(req) 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 = &self.client.post(url).header(ContentType::JSON).body(body);
make_test_response(req) 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] #[test]

View file

@ -2,6 +2,7 @@
- Compatibility with the latest `juniper`. - Compatibility with the latest `juniper`.
- Rocket integration does not require default features. - 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) # [[0.5.1] 2019-10-24](https://github.com/graphql-rust/juniper/releases/tag/juniper_rocket-0.5.1)

View file

@ -294,12 +294,17 @@ where
{ {
type Error = String; 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 _; use tokio::io::AsyncReadExt as _;
if !request.content_type().map_or(false, |ct| ct.is_json()) { let content_type = req
return Box::pin(async move { Forward(data) }); .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 { Box::pin(async move {
let mut body = String::new(); let mut body = String::new();
@ -308,10 +313,14 @@ where
return Failure((Status::InternalServerError, format!("{:?}", e))); return Failure((Status::InternalServerError, format!("{:?}", e)));
} }
match serde_json::from_str(&body) { Success(GraphQLRequest(if is_json {
Ok(value) => Success(GraphQLRequest(value)), match serde_json::from_str(&body) {
Err(failure) => Failure((Status::BadRequest, format!("{}", failure))), 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, client: Client,
} }
impl http_tests::HTTPIntegration for TestRocketIntegration { impl http_tests::HttpIntegration for TestRocketIntegration {
fn get(&self, url: &str) -> http_tests::TestResponse { fn get(&self, url: &str) -> http_tests::TestResponse {
let req = self.client.get(url); let req = self.client.get(url);
let req = futures::executor::block_on(req.dispatch()); let req = futures::executor::block_on(req.dispatch());
futures::executor::block_on(make_test_response(req)) 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 = self.client.post(url).header(ContentType::JSON).body(body);
let req = futures::executor::block_on(req.dispatch()); let req = futures::executor::block_on(req.dispatch());
futures::executor::block_on(make_test_response(req)) 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] #[test]

View file

@ -6,10 +6,11 @@ to `juniper` to be reused in other http integrations, since this implementation
## Breaking Changes ## Breaking Changes
- Update `playground_filter` to support subscription endpoint URLs - Update `playground_filter` to support subscription endpoint URLs.
- Update `warp` to 0.2 - Update `warp` to 0.2.
- Rename synchronous `execute` to `execute_sync`, add asynchronous `execute` - 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) # [[0.5.2] 2019-12-16](https://github.com/graphql-rust/juniper/releases/tag/juniper_warp-0.5.2)

View file

@ -12,18 +12,19 @@ edition = "2018"
subscriptions = ["juniper_subscriptions"] subscriptions = ["juniper_subscriptions"]
[dependencies] [dependencies]
warp = "0.2" bytes = "0.5"
failure = "0.1.7"
futures = "0.3.1" futures = "0.3.1"
juniper = { version = "0.14.2", path = "../juniper", default-features = false } juniper = { version = "0.14.2", path = "../juniper", default-features = false }
juniper_subscriptions = { path = "../juniper_subscriptions", optional = true} juniper_subscriptions = { path = "../juniper_subscriptions", optional = true }
tokio = { version = "0.2", features = ["rt-core", "blocking"] }
serde = { version = "1.0.75", features = ["derive"] } serde = { version = "1.0.75", features = ["derive"] }
serde_json = "1.0.24" serde_json = "1.0.24"
failure = "0.1.7" tokio = { version = "0.2", features = ["blocking", "rt-core"] }
warp = "0.2"
[dev-dependencies] [dev-dependencies]
juniper = { version = "0.14.2", path = "../juniper", features = ["expose-test-schema", "serde_json"] }
env_logger = "0.5.11" env_logger = "0.5.11"
juniper = { version = "0.14.2", path = "../juniper", features = ["expose-test-schema", "serde_json"] }
log = "0.4.3" log = "0.4.3"
percent-encoding = "1.0" percent-encoding = "1.0"
tokio = { version = "0.2", features = ["rt-core", "macros", "blocking"] } tokio = { version = "0.2", features = ["blocking", "macros", "rt-core"] }

View file

@ -40,12 +40,16 @@ Check the LICENSE file for details.
#![deny(warnings)] #![deny(warnings)]
#![doc(html_root_url = "https://docs.rs/juniper_warp/0.2.0")] #![doc(html_root_url = "https://docs.rs/juniper_warp/0.2.0")]
use std::{pin::Pin, sync::Arc}; use std::{collections::HashMap, str, sync::Arc};
use futures::{Future, FutureExt as _, TryFutureExt}; use bytes::Bytes;
use juniper::{http::GraphQLBatchRequest, ScalarValue}; use futures::{FutureExt as _, TryFutureExt};
use juniper::{
http::{GraphQLBatchRequest, GraphQLRequest},
ScalarValue,
};
use tokio::task; use tokio::task;
use warp::{filters::BoxedFilter, Filter}; use warp::{body, filters::BoxedFilter, header, http, query, Filter};
/// Make a filter for graphql queries/mutations. /// 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>( pub fn make_graphql_filter<Query, Mutation, Subscription, Context, S>(
schema: juniper::RootNode<'static, Query, Mutation, Subscription, S>, schema: juniper::RootNode<'static, Query, Mutation, Subscription, S>,
context_extractor: BoxedFilter<(Context,)>, context_extractor: BoxedFilter<(Context,)>,
) -> BoxedFilter<(warp::http::Response<Vec<u8>>,)> ) -> BoxedFilter<(http::Response<Vec<u8>>,)>
where where
S: ScalarValue + Send + Sync + 'static, S: ScalarValue + Send + Sync + 'static,
Context: Send + Sync + 'static, Context: Send + Sync + 'static,
@ -122,65 +126,90 @@ where
Subscription::TypeInfo: Send + Sync, Subscription::TypeInfo: Send + Sync,
{ {
let schema = Arc::new(schema); let schema = Arc::new(schema);
let post_schema = schema.clone(); let post_json_schema = schema.clone();
let post_graphql_schema = schema.clone();
let handle_post_request = move |context: Context, request: GraphQLBatchRequest<S>| { let handle_post_json_request = move |context: Context, req: GraphQLBatchRequest<S>| {
let schema = post_schema.clone(); let schema = post_json_schema.clone();
async move {
Box::pin(async move { let resp = req.execute(&schema, &context).await;
let res = request.execute(&schema, &context).await;
Ok::<_, warp::Rejection>(build_response( Ok::<_, warp::Rejection>(build_response(
serde_json::to_vec(&res) serde_json::to_vec(&resp)
.map(|json| (json, res.is_ok())) .map(|json| (json, resp.is_ok()))
.map_err(Into::into), .map_err(Into::into),
)) ))
}) }
}; };
let post_json_filter = warp::post()
let post_filter = warp::post() .and(header::exact_ignore_case(
"content-type",
"application/json",
))
.and(context_extractor.clone()) .and(context_extractor.clone())
.and(warp::body::json()) .and(body::json())
.and_then(handle_post_request); .and_then(handle_post_json_request);
let handle_get_request = let handle_post_graphql_request = move |context: Context, body: Bytes| {
move |context: Context, mut request: std::collections::HashMap<String, String>| { let schema = post_graphql_schema.clone();
let schema = 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);
async move { let resp = req.execute(&schema, &context).await;
let variables = match request.remove("variables") {
None => None,
Some(vs) => serde_json::from_str(&vs)?,
};
let graphql_request = juniper::http::GraphQLRequest::new( Ok((serde_json::to_vec(&resp)?, resp.is_ok()))
request.remove("query").ok_or_else(|| { }
failure::format_err!("Missing GraphQL query string in query parameters") .then(|res| async { Ok::<_, warp::Rejection>(build_response(res)) })
})?, };
request.get("operation_name").map(|s| s.to_owned()), let post_graphql_filter = warp::post()
variables, .and(header::exact_ignore_case(
); "content-type",
"application/graphql",
))
.and(context_extractor.clone())
.and(body::bytes())
.and_then(handle_post_graphql_request);
let response = graphql_request.execute(&schema, &context).await; 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()?,
);
Ok((serde_json::to_vec(&response)?, response.is_ok())) let resp = req.execute(&schema, &context).await;
}
.then(|result| async move { Ok::<_, warp::Rejection>(build_response(result)) })
};
Ok((serde_json::to_vec(&resp)?, resp.is_ok()))
}
.then(|res| async move { Ok::<_, warp::Rejection>(build_response(res)) })
};
let get_filter = warp::get() let get_filter = warp::get()
.and(context_extractor) .and(context_extractor)
.and(warp::filters::query::query()) .and(query::query())
.and_then(handle_get_request); .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. /// Make a synchronous filter for graphql endpoint.
pub fn make_graphql_filter_sync<Query, Mutation, Subscription, Context, S>( pub fn make_graphql_filter_sync<Query, Mutation, Subscription, Context, S>(
schema: juniper::RootNode<'static, Query, Mutation, Subscription, S>, schema: juniper::RootNode<'static, Query, Mutation, Subscription, S>,
context_extractor: BoxedFilter<(Context,)>, context_extractor: BoxedFilter<(Context,)>,
) -> BoxedFilter<(warp::http::Response<Vec<u8>>,)> ) -> BoxedFilter<(http::Response<Vec<u8>>,)>
where where
S: ScalarValue + Send + Sync + 'static, S: ScalarValue + Send + Sync + 'static,
Context: Send + Sync + 'static, Context: Send + Sync + 'static,
@ -189,105 +218,116 @@ where
Subscription: juniper::GraphQLType<S, Context = Context, TypeInfo = ()> + Send + Sync + 'static, Subscription: juniper::GraphQLType<S, Context = Context, TypeInfo = ()> + Send + Sync + 'static,
{ {
let schema = Arc::new(schema); let schema = Arc::new(schema);
let post_schema = schema.clone(); let post_json_schema = schema.clone();
let post_graphql_schema = schema.clone();
let handle_post_request = let handle_post_json_request = move |context: Context, req: GraphQLBatchRequest<S>| {
move |context: Context, request: GraphQLBatchRequest<S>| -> Response { let schema = post_json_schema.clone();
let schema = post_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?;
Box::pin( Ok(build_response(res))
async move { }
let result = task::spawn_blocking(move || { .map_err(|e: task::JoinError| warp::reject::custom(JoinError(e)))
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()
.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>|
-> Response {
let schema = schema.clone();
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?;
Ok(build_response(result))
}
.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(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 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()?,
);
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 get_filter = warp::get() let get_filter = warp::get()
.and(context_extractor) .and(context_extractor)
.and(warp::filters::query::query()) .and(query::query())
.and_then(handle_get_request); .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 /// Error raised by `tokio_threadpool` if the thread pool has been shutdown.
/// 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); pub struct JoinError(task::JoinError);
impl warp::reject::Reject for JoinError {} impl warp::reject::Reject for JoinError {}
impl std::fmt::Debug for JoinError { fn build_response(response: Result<(Vec<u8>, bool), failure::Error>) -> http::Response<Vec<u8>> {
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>> {
match response { match response {
Ok((body, is_ok)) => warp::http::Response::builder() Ok((body, is_ok)) => http::Response::builder()
.status(if is_ok { 200 } else { 400 }) .status(if is_ok { 200 } else { 400 })
.header("content-type", "application/json") .header("content-type", "application/json")
.body(body) .body(body)
.expect("response is valid"), .expect("response is valid"),
Err(_) => warp::http::Response::builder() Err(_) => http::Response::builder()
.status(warp::http::StatusCode::INTERNAL_SERVER_ERROR) .status(http::StatusCode::INTERNAL_SERVER_ERROR)
.body(Vec::new()) .body(Vec::new())
.expect("status code is valid"), .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. /// 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: /// For example:
@ -318,7 +358,7 @@ type Response = Pin<
pub fn graphiql_filter( pub fn graphiql_filter(
graphql_endpoint_url: &'static str, graphql_endpoint_url: &'static str,
subscriptions_endpoint: Option<&'static str>, subscriptions_endpoint: Option<&'static str>,
) -> warp::filters::BoxedFilter<(warp::http::Response<Vec<u8>>,)> { ) -> warp::filters::BoxedFilter<(http::Response<Vec<u8>>,)> {
warp::any() warp::any()
.map(move || graphiql_response(graphql_endpoint_url, subscriptions_endpoint)) .map(move || graphiql_response(graphql_endpoint_url, subscriptions_endpoint))
.boxed() .boxed()
@ -327,8 +367,8 @@ pub fn graphiql_filter(
fn graphiql_response( fn graphiql_response(
graphql_endpoint_url: &'static str, graphql_endpoint_url: &'static str,
subscriptions_endpoint: Option<&'static str>, subscriptions_endpoint: Option<&'static str>,
) -> warp::http::Response<Vec<u8>> { ) -> http::Response<Vec<u8>> {
warp::http::Response::builder() http::Response::builder()
.header("content-type", "text/html;charset=utf-8") .header("content-type", "text/html;charset=utf-8")
.body( .body(
juniper::http::graphiql::graphiql_source(graphql_endpoint_url, subscriptions_endpoint) juniper::http::graphiql::graphiql_source(graphql_endpoint_url, subscriptions_endpoint)
@ -341,7 +381,7 @@ fn graphiql_response(
pub fn playground_filter( pub fn playground_filter(
graphql_endpoint_url: &'static str, graphql_endpoint_url: &'static str,
subscriptions_endpoint_url: Option<&'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() warp::any()
.map(move || playground_response(graphql_endpoint_url, subscriptions_endpoint_url)) .map(move || playground_response(graphql_endpoint_url, subscriptions_endpoint_url))
.boxed() .boxed()
@ -350,8 +390,8 @@ pub fn playground_filter(
fn playground_response( fn playground_response(
graphql_endpoint_url: &'static str, graphql_endpoint_url: &'static str,
subscriptions_endpoint_url: Option<&'static str>, subscriptions_endpoint_url: Option<&'static str>,
) -> warp::http::Response<Vec<u8>> { ) -> http::Response<Vec<u8>> {
warp::http::Response::builder() http::Response::builder()
.header("content-type", "text/html;charset=utf-8") .header("content-type", "text/html;charset=utf-8")
.body( .body(
juniper::http::playground::playground_source( juniper::http::playground::playground_source(
@ -780,100 +820,122 @@ mod tests {
} }
} }
//TODO: update warp tests #[cfg(test)]
//#[cfg(test)] mod tests_http_harness {
//mod tests_http_harness { use super::*;
// use super::*; use juniper::{
// use juniper::{ http::tests::{run_http_test_suite, HttpIntegration, TestResponse},
// http::tests::{run_http_test_suite, HTTPIntegration, TestResponse}, tests::{model::Database, schema::Query},
// tests::{model::Database, schema::Query}, EmptyMutation, EmptySubscription, RootNode,
// EmptyMutation, EmptySubscription, RootNode, };
// }; use warp::{
// use warp::{self, Filter}; self,
// filters::{path, BoxedFilter},
// type Schema = Filter,
// juniper::RootNode<'static, Query, EmptyMutation<Database>, EmptySubscription<Database>>; };
//
// fn warp_server() -> warp::filters::BoxedFilter<(warp::http::Response<Vec<u8>>,)> { struct TestWarpIntegration {
// let schema: Schema = RootNode::new( filter: BoxedFilter<(http::Response<Vec<u8>>,)>,
// Query, }
// EmptyMutation::<Database>::new(),
// EmptySubscription::<Database>::new(), impl TestWarpIntegration {
// ); fn new(is_sync: bool) -> Self {
// let schema = RootNode::new(
// let state = warp::any().map(move || Database::new()); Query,
// let filter = warp::filters::path::end().and(make_graphql_filter(schema, state.boxed())); EmptyMutation::<Database>::new(),
// EmptySubscription::<Database>::new(),
// filter.boxed() );
// } let state = warp::any().map(move || Database::new());
//
// struct TestWarpIntegration { let filter = path::end().and(if is_sync {
// filter: warp::filters::BoxedFilter<(warp::http::Response<Vec<u8>>,)>, make_graphql_filter_sync(schema, state.boxed())
// } } else {
// make_graphql_filter(schema, state.boxed())
// // 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 { Self {
// TestResponse { filter: filter.boxed(),
// status_code: response.status().as_u16() as i32, }
// body: Some(String::from_utf8(response.body().to_owned()).unwrap()), }
// content_type: response
// .headers() fn make_request(&self, req: warp::test::RequestBuilder) -> TestResponse {
// .get("content-type") let mut rt = tokio::runtime::Runtime::new().expect("Failed to create tokio::Runtime");
// .expect("missing content-type header in warp response") make_test_response(rt.block_on(async move {
// .to_str() req.filter(&self.filter).await.unwrap_or_else(|rejection| {
// .expect("invalid content-type string") let code = if rejection.is_not_found() {
// .to_owned(), http::StatusCode::NOT_FOUND
// } } else if let Some(body::BodyDeserializeError { .. }) = rejection.find() {
// } http::StatusCode::BAD_REQUEST
// } else {
// impl HTTPIntegration for TestWarpIntegration { http::StatusCode::INTERNAL_SERVER_ERROR
// fn get(&self, url: &str) -> TestResponse { };
// use percent_encoding::{percent_encode, DEFAULT_ENCODE_SET}; http::Response::builder()
// let url: String = percent_encode(url.replace("/?", "").as_bytes(), DEFAULT_ENCODE_SET) .status(code)
// .into_iter() .header("content-type", "application/json")
// .collect::<Vec<_>>() .body(Vec::new())
// .join(""); .unwrap()
// })
// let response = warp::test::request() }))
// .method("GET") }
// .path(&format!("/?{}", url)) }
// .filter(&self.filter)
// .await impl HttpIntegration for TestWarpIntegration {
// .unwrap_or_else(|rejection| { fn get(&self, url: &str) -> TestResponse {
// warp::http::Response::builder() use percent_encoding::{utf8_percent_encode, QUERY_ENCODE_SET};
// .status(rejection.status())
// .header("content-type", "application/json") let url: String = utf8_percent_encode(&url.replace("/?", ""), QUERY_ENCODE_SET)
// .body(Vec::new()) .into_iter()
// .unwrap() .collect::<Vec<_>>()
// }); .join("");
// test_response_from_http_response(response)
// } self.make_request(
// warp::test::request()
// fn post(&self, url: &str, body: &str) -> TestResponse { .method("GET")
// let response = warp::test::request() .path(&format!("/?{}", url)),
// .method("POST") )
// .header("content-type", "application/json") }
// .path(url)
// .body(body) fn post_json(&self, url: &str, body: &str) -> TestResponse {
// .filter(&self.filter) self.make_request(
// .await warp::test::request()
// .unwrap_or_else(|rejection| { .method("POST")
// warp::http::Response::builder() .header("content-type", "application/json")
// .status(rejection.status()) .path(url)
// .header("content-type", "application/json") .body(body),
// .body(Vec::new()) )
// .unwrap() }
// });
// test_response_from_http_response(response) fn post_graphql(&self, url: &str, body: &str) -> TestResponse {
// } self.make_request(
// } warp::test::request()
// .method("POST")
// #[test] .header("content-type", "application/graphql")
// fn test_warp_integration() { .path(url)
// let integration = TestWarpIntegration { .body(body),
// filter: warp_server(), )
// }; }
// }
// run_http_test_suite(&integration);
// } 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));
}
}