diff --git a/CHANGELOG.md b/CHANGELOG.md index 06d4b740..43c84473 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +# [4.0.14] 2022-09-25 + +# Add support for parse request from query string. [#1085](https://github.com/async-graphql/async-graphql/issues/1085) + # [4.0.13] 2022-09-09 - Compare to expected schema [#1048](https://github.com/async-graphql/async-graphql/pull/1048) diff --git a/Cargo.toml b/Cargo.toml index 2a38c62e..0c0f3ec5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -54,6 +54,7 @@ static_assertions = "1.1.0" tempfile = "3.2.0" thiserror = "1.0.24" base64 = "0.13.0" +serde_urlencoded = "0.7.0" # Feature optional dependencies bson = { version = "2.4.0", optional = true, features = [ diff --git a/integrations/actix-web/Cargo.toml b/integrations/actix-web/Cargo.toml index a3f5474a..913bfb5d 100644 --- a/integrations/actix-web/Cargo.toml +++ b/integrations/actix-web/Cargo.toml @@ -23,7 +23,6 @@ futures-channel = "0.3.13" futures-util = { version = "0.3.0", default-features = false } serde_cbor = { version = "0.11.2", optional = true } serde_json = "1.0.64" -serde_urlencoded = "0.7.0" thiserror = "1.0.30" [features] diff --git a/integrations/actix-web/src/request.rs b/integrations/actix-web/src/request.rs index fbc480ee..f5dedd3f 100644 --- a/integrations/actix-web/src/request.rs +++ b/integrations/actix-web/src/request.rs @@ -75,7 +75,8 @@ impl FromRequest for GraphQLBatchRequest { .unwrap_or_default(); if req.method() == Method::GET { - let res = serde_urlencoded::from_str(req.query_string()); + let res = async_graphql::http::parse_query_string(req.query_string()) + .map_err(|err| io::Error::new(ErrorKind::Other, err)); Box::pin(async move { Ok(Self(async_graphql::BatchRequest::Single(res?))) }) } else if req.method() == Method::POST { let content_type = req diff --git a/integrations/axum/Cargo.toml b/integrations/axum/Cargo.toml index 98cbf9f3..9806b1bc 100644 --- a/integrations/axum/Cargo.toml +++ b/integrations/axum/Cargo.toml @@ -20,6 +20,5 @@ bytes = "1.0.1" futures-util = "0.3.0" http-body = "0.4.2" serde_json = "1.0.66" -serde_urlencoded = "0.7.0" tokio-util = { version = "0.7.1", features = ["io", "compat"] } tower-service = "0.3" diff --git a/integrations/axum/src/extract.rs b/integrations/axum/src/extract.rs index 2ec55254..51dd9528 100644 --- a/integrations/axum/src/extract.rs +++ b/integrations/axum/src/extract.rs @@ -107,7 +107,7 @@ where async fn from_request(req: &mut RequestParts) -> Result { if let (&Method::GET, uri) = (req.method(), req.uri()) { - let res = serde_urlencoded::from_str(uri.query().unwrap_or_default()).map_err(|err| { + let res = async_graphql::http::parse_query_string(uri.query().unwrap_or_default()).map_err(|err| { ParseRequestError::Io(std::io::Error::new( ErrorKind::Other, format!("failed to parse graphql request from uri query: {}", err), diff --git a/integrations/poem/src/extractor.rs b/integrations/poem/src/extractor.rs index 4d33a989..6285e28a 100644 --- a/integrations/poem/src/extractor.rs +++ b/integrations/poem/src/extractor.rs @@ -3,7 +3,6 @@ use poem::{ async_trait, error::BadRequest, http::{header, Method}, - web::Query, FromRequest, Request, RequestBody, Result, }; use tokio_util::compat::TokioAsyncReadCompatExt; @@ -68,7 +67,9 @@ pub struct GraphQLBatchRequest(pub async_graphql::BatchRequest); impl<'a> FromRequest<'a> for GraphQLBatchRequest { async fn from_request(req: &'a Request, body: &mut RequestBody) -> Result { if req.method() == Method::GET { - let req = Query::from_request(req, body).await?.0; + let req = + async_graphql::http::parse_query_string(req.uri().query().unwrap_or_default()) + .map_err(BadRequest)?; Ok(Self(async_graphql::BatchRequest::Single(req))) } else { let content_type = req diff --git a/integrations/tide/src/lib.rs b/integrations/tide/src/lib.rs index a180374e..791a19dc 100644 --- a/integrations/tide/src/lib.rs +++ b/integrations/tide/src/lib.rs @@ -132,7 +132,9 @@ pub async fn receive_batch_request_opts( opts: MultipartOptions, ) -> tide::Result { if request.method() == Method::Get { - request.query::().map(Into::into) + async_graphql::http::parse_query_string(request.url().query().unwrap_or_default()) + .map(Into::into) + .map_err(|err| tide::Error::new(StatusCode::BadRequest, err)) } else if request.method() == Method::Post { let body = request.take_body(); let content_type = request diff --git a/integrations/warp/src/batch_request.rs b/integrations/warp/src/batch_request.rs index a619bd1f..07ce1b05 100644 --- a/integrations/warp/src/batch_request.rs +++ b/integrations/warp/src/batch_request.rs @@ -35,7 +35,13 @@ where Subscription: SubscriptionType + 'static, { warp::any() - .and(warp::get().and(warp::query()).map(BatchRequest::Single)) + .and(warp::get().and(warp::filters::query::raw()).and_then( + |query_string: String| async move { + async_graphql::http::parse_query_string(&query_string) + .map(Into::into) + .map_err(|e| warp::reject::custom(GraphQLBadRequest(e))) + }, + )) .or(warp::post() .and(warp::header::optional::("content-type")) .and(warp::body::stream()) diff --git a/src/http/mod.rs b/src/http/mod.rs index c4ddfce0..fa339d14 100644 --- a/src/http/mod.rs +++ b/src/http/mod.rs @@ -6,18 +6,59 @@ mod multipart; mod playground_source; mod websocket; +use std::io::ErrorKind; + use futures_util::io::{AsyncRead, AsyncReadExt}; pub use graphiql_source::graphiql_source; pub use graphiql_v2_source::GraphiQLSource; use mime; pub use multipart::MultipartOptions; pub use playground_source::{playground_source, GraphQLPlaygroundConfig}; +use serde::Deserialize; pub use websocket::{ ClientMessage, Protocols as WebSocketProtocols, WebSocket, WsMessage, ALL_WEBSOCKET_PROTOCOLS, }; use crate::{BatchRequest, ParseRequestError, Request}; +/// Parse a GraphQL request from a query string. +pub fn parse_query_string(input: &str) -> Result { + #[derive(Deserialize)] + struct RequestSerde { + #[serde(default)] + pub query: String, + pub operation_name: Option, + pub variables: Option, + pub extensions: Option, + } + + let request: RequestSerde = serde_urlencoded::from_str(input) + .map_err(|err| std::io::Error::new(ErrorKind::Other, err))?; + let variables = request + .variables + .map(|data| serde_json::from_str(&data)) + .transpose() + .map_err(|err| { + std::io::Error::new(ErrorKind::Other, format!("invalid variables: {}", err)) + })? + .unwrap_or_default(); + let extensions = request + .extensions + .map(|data| serde_json::from_str(&data)) + .transpose() + .map_err(|err| { + std::io::Error::new(ErrorKind::Other, format!("invalid extensions: {}", err)) + })? + .unwrap_or_default(); + + Ok(Request { + operation_name: request.operation_name, + variables, + extensions, + ..Request::new(request.query) + }) +} + /// Receive a GraphQL request from a content type and body. pub async fn receive_body( content_type: Option>, @@ -116,3 +157,33 @@ pub async fn receive_batch_cbor(body: impl AsyncRead) -> Result(&data) .map_err(|e| ParseRequestError::InvalidRequest(Box::new(e))) } + +#[cfg(test)] +mod tests { + use std::collections::HashMap; + + use super::*; + use crate::{value, Variables}; + + #[test] + fn test_parse_query_string() { + let request = parse_query_string("variables=%7B%7D&extensions=%7B%22persistedQuery%22%3A%7B%22sha256Hash%22%3A%22cde5de0a350a19c59f8ddcd9646e5f260b2a7d5649ff6be8e63e9462934542c3%22%2C%22version%22%3A1%7D%7D").unwrap(); + assert_eq!(request.query.as_str(), ""); + assert_eq!(request.variables, Variables::default()); + assert_eq!(request.extensions, { + let mut extensions = HashMap::new(); + extensions.insert("persistedQuery".to_string(), value!({ + "sha256Hash": "cde5de0a350a19c59f8ddcd9646e5f260b2a7d5649ff6be8e63e9462934542c3", + "version": 1, + })); + extensions + }); + + let request = parse_query_string("query={a}&variables=%7B%22a%22%3A10%7D").unwrap(); + assert_eq!(request.query.as_str(), "{a}"); + assert_eq!( + request.variables, + Variables::from_value(value!({ "a" : 10 })) + ); + } +} diff --git a/value/src/variables.rs b/value/src/variables.rs index 9b3acd29..bfb84698 100644 --- a/value/src/variables.rs +++ b/value/src/variables.rs @@ -9,7 +9,7 @@ use serde::{Deserialize, Deserializer, Serialize}; use crate::{ConstValue, Name}; /// Variables of a query. -#[derive(Debug, Clone, Default, Serialize)] +#[derive(Debug, Clone, Default, Serialize, Eq, PartialEq)] #[serde(transparent)] pub struct Variables(BTreeMap);