diff --git a/.gitignore b/.gitignore index bf084b6c226..a0d8e5475b4 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,7 @@ lcov.info # Built solidity contracts. /tests/**/bin /tests/**/truffle_output + +# Docker volumes and debug logs +.postgres +logfile \ No newline at end of file diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 742de12649d..d92f6d17fb8 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -28,7 +28,7 @@ services: volumes: - ./data/ipfs:/data/ipfs postgres: - image: postgres + image: postgres:14 ports: - '5432:5432' command: diff --git a/graph/src/data/query/result.rs b/graph/src/data/query/result.rs index d2d06e65679..4f0f0e26842 100644 --- a/graph/src/data/query/result.rs +++ b/graph/src/data/query/result.rs @@ -190,8 +190,10 @@ impl QueryResults { pub fn as_http_response>(&self) -> http::Response { let status_code = http::StatusCode::OK; + let json = serde_json::to_string(self).expect("Failed to serialize GraphQL response to JSON"); + http::Response::builder() .status(status_code) .header(ACCESS_CONTROL_ALLOW_ORIGIN, "*") diff --git a/server/http/src/server.rs b/server/http/src/server.rs index a99e8bafe05..39c1bd22e66 100644 --- a/server/http/src/server.rs +++ b/server/http/src/server.rs @@ -1,11 +1,12 @@ use std::net::{Ipv4Addr, SocketAddrV4}; +use futures::future::Future; use hyper::service::make_service_fn; use hyper::Server; +use thiserror::Error; use crate::service::GraphQLService; -use graph::prelude::{GraphQLServer as GraphQLServerTrait, *}; -use thiserror::Error; +use graph::prelude::{GraphQLServer as GraphQLServerTrait, GraphQlRunner, *}; /// Errors that may occur when starting the server. #[derive(Debug, Error)] @@ -66,12 +67,14 @@ where let graphql_runner = self.graphql_runner.clone(); let node_id = self.node_id.clone(); let new_service = make_service_fn(move |_| { - futures03::future::ok::<_, Error>(GraphQLService::new( + let graphql_service = GraphQLService::new( logger_for_service.clone(), graphql_runner.clone(), ws_port, node_id.clone(), - )) + ); + + futures03::future::ok::<_, Error>(graphql_service) }); // Create a task to run the server and handle HTTP requests diff --git a/server/http/src/service.rs b/server/http/src/service.rs index 64cc36afdcc..a1880d46a0f 100644 --- a/server/http/src/service.rs +++ b/server/http/src/service.rs @@ -1,11 +1,15 @@ use std::convert::TryFrom; +use std::env; use std::pin::Pin; use std::task::Context; use std::task::Poll; use std::time::Instant; +use graph::prelude::serde_json; +use graph::prelude::serde_json::json; use graph::prelude::*; use graph::semver::VersionReq; +use graph::url::form_urlencoded; use graph::{components::server::query::GraphQLServerError, data::query::QueryTarget}; use http::header; use http::header::{ @@ -62,14 +66,17 @@ where } async fn index(self) -> GraphQLServiceResult { + let response_obj = json!({ + "message": "Access deployed subgraphs by deployment ID at \ + /subgraphs/id/ or by name at /subgraphs/name/" + }); + let response_str = serde_json::to_string(&response_obj).unwrap(); + Ok(Response::builder() .status(200) .header(ACCESS_CONTROL_ALLOW_ORIGIN, "*") - .header(CONTENT_TYPE, "text/plain") - .body(Body::from(String::from( - "Access deployed subgraphs by deployment ID at \ - /subgraphs/id/ or by name at /subgraphs/name/", - ))) + .header(CONTENT_TYPE, "application/json") + .body(Body::from(response_str)) .unwrap()) } @@ -79,7 +86,7 @@ where Ok(Response::builder() .status(200) .header(ACCESS_CONTROL_ALLOW_ORIGIN, "*") - .header(CONTENT_TYPE, "text/html") + .header(CONTENT_TYPE, "text/html; charset=utf-8") .body(Body::from(contents)) .unwrap()) } @@ -202,7 +209,7 @@ where .header(ACCESS_CONTROL_ALLOW_ORIGIN, "*") .header(ACCESS_CONTROL_ALLOW_HEADERS, "Content-Type, User-Agent") .header(ACCESS_CONTROL_ALLOW_METHODS, "GET, OPTIONS, POST") - .header(CONTENT_TYPE, "text/html") + .header(CONTENT_TYPE, "text/html; charset=utf-8") .body(Body::from("")) .unwrap()) } @@ -220,24 +227,89 @@ where .status(StatusCode::FOUND) .header(ACCESS_CONTROL_ALLOW_ORIGIN, "*") .header(LOCATION, loc_header_val) - .header(CONTENT_TYPE, "text/plain") + .header(CONTENT_TYPE, "text/plain; charset=utf-8") .body(Body::from("Redirecting...")) .unwrap() }) } - /// Handles 404s. fn handle_not_found(&self) -> GraphQLServiceResponse { async { + let response_obj = json!({ + "message": "Not found" + }); + let response_str = serde_json::to_string(&response_obj).unwrap(); + + Ok(Response::builder() + .status(200) + .header(CONTENT_TYPE, "application/json") + .header(ACCESS_CONTROL_ALLOW_ORIGIN, "*") + .body(Body::from(response_str)) + .unwrap()) + } + .boxed() + } + + fn handle_mutations(&self) -> GraphQLServiceResponse { + async { + let response_obj = json!({ + "error": "Can't use mutations with GET method" + }); + let response_str = serde_json::to_string(&response_obj).unwrap(); + + Ok(Response::builder() + .status(400) + .header(CONTENT_TYPE, "application/json") + .header(ACCESS_CONTROL_ALLOW_ORIGIN, "*") + .body(Body::from(response_str)) + .unwrap()) + } + .boxed() + } + /// Handles requests without content type. + fn handle_requests_without_content_type(&self) -> GraphQLServiceResponse { + async { + let response_obj = json!({ + "message": "Content-Type header is required" + }); + let response_str = serde_json::to_string(&response_obj).unwrap(); + Ok(Response::builder() - .status(StatusCode::NOT_FOUND) - .header(CONTENT_TYPE, "text/plain") + .status(400) + .header(CONTENT_TYPE, "application/json") .header(ACCESS_CONTROL_ALLOW_ORIGIN, "*") - .body(Body::from("Not found")) + .body(Body::from(response_str)) .unwrap()) } .boxed() } + /// Handles requests without body. + fn handle_requests_without_body(&self) -> GraphQLServiceResponse { + async { + let response_obj = json!({ + "message": "Body is required" + }); + let response_str = serde_json::to_string(&response_obj).unwrap(); + + Ok(Response::builder() + .status(400) + .header(CONTENT_TYPE, "application/json") + .header(ACCESS_CONTROL_ALLOW_ORIGIN, "*") + .body(Body::from(response_str)) + .unwrap()) + } + .boxed() + } + fn has_request_body(&self, req: &Request) -> bool { + if let Some(length) = req.headers().get(hyper::header::CONTENT_LENGTH) { + if let Ok(length) = length.to_str() { + if let Ok(length) = length.parse::() { + return length > 0; + } + } + } + false + } fn handle_call(self, req: Request) -> GraphQLServiceResponse { let method = req.method().clone(); @@ -252,6 +324,34 @@ where segments.collect::>() }; + let headers = req.headers(); + let content_type = headers.get("content-type"); + + let less_strict_graphql_compliance = env::var("LESS_STRICT_GRAPHQL_COMPLIANCE").is_ok(); + + if !less_strict_graphql_compliance { + if method == Method::POST && (content_type.is_none()) { + return self.handle_requests_without_content_type().boxed(); + } + + if method == Method::POST && !self.has_request_body(&req) { + return self.handle_requests_without_body().boxed(); + } + } + + let is_mutation = req + .uri() + .query() + .and_then(|query_str| { + form_urlencoded::parse(query_str.as_bytes()) + .find(|(key, _)| key == "query") + .map(|(_, value)| value.into_owned()) + }) + .unwrap_or_else(|| String::new()) + .trim() + .to_lowercase() + .starts_with("mutation"); + match (method, path_segments.as_slice()) { (Method::GET, [""]) => self.index().boxed(), (Method::GET, &["subgraphs", "id", _, "graphql"]) @@ -260,6 +360,9 @@ where | (Method::GET, &["subgraphs", "network", _, _, "graphql"]) | (Method::GET, &["subgraphs", "graphql"]) => self.handle_graphiql(), + (Method::GET, _path @ ["subgraphs", "name", _, _]) if is_mutation => { + self.handle_mutations() + } (Method::GET, path @ ["subgraphs", "id", _]) | (Method::GET, path @ ["subgraphs", "name", _]) | (Method::GET, path @ ["subgraphs", "name", _, _]) @@ -316,22 +419,35 @@ where // Instead, we generate a Response with an error code and return Ok Box::pin(async move { let result = service.handle_call(req).await; + match result { Ok(response) => Ok(response), - Err(err @ GraphQLServerError::ClientError(_)) => Ok(Response::builder() - .status(400) - .header(CONTENT_TYPE, "text/plain") - .header(ACCESS_CONTROL_ALLOW_ORIGIN, "*") - .body(Body::from(err.to_string())) - .unwrap()), + Err(err @ GraphQLServerError::ClientError(_)) => { + let response_obj = json!({ + "error": err.to_string() + }); + let response_str = serde_json::to_string(&response_obj).unwrap(); + + Ok(Response::builder() + .status(400) + .header(CONTENT_TYPE, "application/json") + .header(ACCESS_CONTROL_ALLOW_ORIGIN, "*") + .body(Body::from(response_str)) + .unwrap()) + } Err(err @ GraphQLServerError::QueryError(_)) => { error!(logger, "GraphQLService call failed: {}", err); + let response_obj = json!({ + "QueryError": err.to_string() + }); + let response_str = serde_json::to_string(&response_obj).unwrap(); + Ok(Response::builder() .status(400) - .header(CONTENT_TYPE, "text/plain") + .header(CONTENT_TYPE, "application/json") .header(ACCESS_CONTROL_ALLOW_ORIGIN, "*") - .body(Body::from(format!("Query error: {}", err))) + .body(Body::from(response_str)) .unwrap()) } Err(err @ GraphQLServerError::InternalError(_)) => { @@ -339,7 +455,7 @@ where Ok(Response::builder() .status(500) - .header(CONTENT_TYPE, "text/plain") + .header(CONTENT_TYPE, "text/plain; charset=utf-8") .header(ACCESS_CONTROL_ALLOW_ORIGIN, "*") .body(Body::from(format!("Internal server error: {}", err))) .unwrap()) @@ -352,6 +468,8 @@ where #[cfg(test)] mod tests { use graph::data::value::{Object, Word}; + use graph::prelude::serde_json::json; + use http::header::{CONTENT_LENGTH, CONTENT_TYPE}; use http::status::StatusCode; use hyper::service::Service; use hyper::{Body, Method, Request}; @@ -419,6 +537,39 @@ mod tests { } } + #[tokio::test] + async fn querying_not_found_routes_responds_correctly() { + let logger = Logger::root(slog::Discard, o!()); + let graphql_runner = Arc::new(TestGraphQlRunner); + + let node_id = NodeId::new("test").unwrap(); + let mut service = GraphQLService::new(logger, graphql_runner, 8001, node_id); + + let request = Request::builder() + .method(Method::GET) + .header(CONTENT_TYPE, "text/plain; charset=utf-8") + .uri("http://localhost:8000/not_found_route".to_string()) + .body(Body::from("{}")) + .unwrap(); + + let response = + futures03::executor::block_on(service.call(request)).expect("Should return a response"); + + let content_type_header = response.status(); + assert_eq!(content_type_header, StatusCode::OK); + + let content_type_header = response.headers().get(CONTENT_TYPE).unwrap(); + assert_eq!(content_type_header, "application/json"); + + let body_bytes = hyper::body::to_bytes(response.into_body()).await.unwrap(); + let json: serde_json::Result = + serde_json::from_str(String::from_utf8(body_bytes.to_vec()).unwrap().as_str()); + + assert!(json.is_ok(), "Response body is not valid JSON"); + + assert_eq!(json.unwrap(), serde_json::json!({"message": "Not found"})); + } + #[test] fn posting_invalid_query_yields_error_response() { let logger = Logger::root(slog::Discard, o!()); @@ -430,6 +581,8 @@ mod tests { let request = Request::builder() .method(Method::POST) + .header(CONTENT_TYPE, "text/plain; charset=utf-8") + .header(CONTENT_LENGTH, 100) .uri(format!( "http://localhost:8000/subgraphs/id/{}", subgraph_id @@ -443,10 +596,11 @@ mod tests { let message = errors[0].as_str().expect("Error message is not a string"); - assert_eq!( - message, - "GraphQL server error (client error): The \"query\" field is missing in request data" - ); + let response = json!({ + "error": "GraphQL server error (client error): The \"query\" field is missing in request data".to_string() + }); + + assert_eq!(message, response.to_string()); } #[tokio::test(flavor = "multi_thread")] @@ -460,6 +614,8 @@ mod tests { let request = Request::builder() .method(Method::POST) + .header(CONTENT_TYPE, "text/plain; charset=utf-8") + .header(CONTENT_LENGTH, 100) .uri(format!( "http://localhost:8000/subgraphs/id/{}", subgraph_id @@ -472,6 +628,7 @@ mod tests { .await .unwrap() .expect("Should return a response"); + let data = test_utils::assert_successful_response(response); // The body should match the simulated query result diff --git a/server/http/src/test_utils.rs b/server/http/src/test_utils.rs index 22935598ee7..6f99f5b1d83 100644 --- a/server/http/src/test_utils.rs +++ b/server/http/src/test_utils.rs @@ -7,7 +7,6 @@ use hyper::{header::ACCESS_CONTROL_ALLOW_ORIGIN, Body, Response}; pub fn assert_successful_response( response: Response, ) -> serde_json::Map { - assert_eq!(response.status(), StatusCode::OK); assert_expected_headers(&response); futures03::executor::block_on( hyper::body::to_bytes(response.into_body()) diff --git a/server/http/tests/server.rs b/server/http/tests/server.rs index e9046e20020..a8b53c3c70d 100644 --- a/server/http/tests/server.rs +++ b/server/http/tests/server.rs @@ -85,6 +85,8 @@ impl GraphQlRunner for TestGraphQlRunner { #[cfg(test)] mod test { + use http::header::CONTENT_TYPE; + use super::*; lazy_static! { @@ -114,7 +116,7 @@ mod test { // Send an empty JSON POST request let client = Client::new(); let request = - Request::post(format!("http://localhost:8007/subgraphs/id/{}", id)) + Request::post(format!("http://localhost:8007/subgraphs/id/{}", id)).header(CONTENT_TYPE, "text/plain") .body(Body::from("{}")) .unwrap(); @@ -128,7 +130,7 @@ mod test { let message = errors[0] .as_str() .expect("Error message is not a string"); - assert_eq!(message, "GraphQL server error (client error): The \"query\" field is missing in request data"); + assert_eq!(message, "{\"error\":\"GraphQL server error (client error): The \\\"query\\\" field is missing in request data\"}"); }).await.unwrap() }) } @@ -157,6 +159,7 @@ mod test { let client = Client::new(); let request = Request::post(format!("http://localhost:8002/subgraphs/id/{}", id)) + .header(CONTENT_TYPE, "text/plain") .body(Body::from("{\"query\": \"M>\"}")) .unwrap(); @@ -238,6 +241,7 @@ mod test { let client = Client::new(); let request = Request::post(format!("http://localhost:8003/subgraphs/id/{}", id)) + .header(CONTENT_TYPE, "plain/text") .body(Body::from("{\"query\": \"{ name }\"}")) .unwrap();