async_graphql/http/
mod.rs

1//! A helper module that supports HTTP
2
3#[cfg(feature = "altair")]
4mod altair_source;
5#[cfg(feature = "graphiql")]
6mod graphiql_plugin;
7#[cfg(feature = "graphiql")]
8mod graphiql_source;
9#[cfg(feature = "graphiql")]
10mod graphiql_v2_source;
11mod multipart;
12mod multipart_subscribe;
13#[cfg(feature = "playground")]
14mod playground_source;
15mod websocket;
16
17#[cfg(feature = "altair")]
18pub use altair_source::*;
19use futures_util::io::{AsyncRead, AsyncReadExt};
20#[cfg(feature = "graphiql")]
21pub use graphiql_plugin::{GraphiQLPlugin, graphiql_plugin_explorer};
22#[cfg(feature = "graphiql")]
23pub use graphiql_source::graphiql_source;
24#[cfg(feature = "graphiql")]
25pub use graphiql_v2_source::{Credentials, GraphiQLSource};
26pub use multipart::MultipartOptions;
27pub use multipart_subscribe::{create_multipart_mixed_stream, is_accept_multipart_mixed};
28#[cfg(feature = "playground")]
29pub use playground_source::{GraphQLPlaygroundConfig, playground_source};
30use serde::Deserialize;
31pub use websocket::{
32    ALL_WEBSOCKET_PROTOCOLS, ClientMessage, DefaultOnConnInitType, DefaultOnPingType,
33    Protocols as WebSocketProtocols, WebSocket, WsMessage, default_on_connection_init,
34    default_on_ping,
35};
36
37use crate::{BatchRequest, ParseRequestError, Request};
38
39/// Parse a GraphQL request from a query string.
40pub fn parse_query_string(input: &str) -> Result<Request, ParseRequestError> {
41    #[derive(Deserialize)]
42    struct RequestSerde {
43        #[serde(default)]
44        pub query: String,
45        pub operation_name: Option<String>,
46        pub variables: Option<String>,
47        pub extensions: Option<String>,
48    }
49
50    let request: RequestSerde = serde_urlencoded::from_str(input).map_err(std::io::Error::other)?;
51    let variables = request
52        .variables
53        .map(|data| serde_json::from_str(&data))
54        .transpose()
55        .map_err(|err| std::io::Error::other(format!("invalid variables: {}", err)))?
56        .unwrap_or_default();
57    let extensions = request
58        .extensions
59        .map(|data| serde_json::from_str(&data))
60        .transpose()
61        .map_err(|err| std::io::Error::other(format!("invalid extensions: {}", err)))?
62        .unwrap_or_default();
63
64    Ok(Request {
65        operation_name: request.operation_name,
66        variables,
67        extensions,
68        ..Request::new(request.query)
69    })
70}
71
72/// Receive a GraphQL request from a content type and body.
73pub async fn receive_body(
74    content_type: Option<impl AsRef<str>>,
75    body: impl AsyncRead + Send,
76    opts: MultipartOptions,
77) -> Result<Request, ParseRequestError> {
78    receive_batch_body(content_type, body, opts)
79        .await?
80        .into_single()
81}
82
83/// Receive a GraphQL request from a content type and body.
84pub async fn receive_batch_body(
85    content_type: Option<impl AsRef<str>>,
86    body: impl AsyncRead + Send,
87    opts: MultipartOptions,
88) -> Result<BatchRequest, ParseRequestError> {
89    // if no content-type header is set, we default to json
90    let content_type = content_type
91        .as_ref()
92        .map(AsRef::as_ref)
93        .unwrap_or("application/graphql-response+json");
94
95    let content_type: mime::Mime = content_type.parse()?;
96
97    match (content_type.type_(), content_type.subtype()) {
98        // try to use multipart
99        (mime::MULTIPART, _) => {
100            if let Some(boundary) = content_type.get_param("boundary") {
101                multipart::receive_batch_multipart(body, boundary.to_string(), opts).await
102            } else {
103                Err(ParseRequestError::InvalidMultipart(
104                    multer::Error::NoBoundary,
105                ))
106            }
107        }
108        // application/json or cbor (currently)
109        // cbor is in application/octet-stream.
110        // Note: cbor will only match if feature ``cbor`` is active
111        // TODO: wait for mime to add application/cbor and match against that too
112        _ => receive_batch_body_no_multipart(&content_type, body).await,
113    }
114}
115
116/// Receives a GraphQL query which is either cbor or json but NOT multipart
117/// This method is only to avoid recursive calls with [``receive_batch_body``]
118/// and [``multipart::receive_batch_multipart``]
119pub(super) async fn receive_batch_body_no_multipart(
120    content_type: &mime::Mime,
121    body: impl AsyncRead + Send,
122) -> Result<BatchRequest, ParseRequestError> {
123    assert_ne!(content_type.type_(), mime::MULTIPART, "received multipart");
124    match (content_type.type_(), content_type.subtype()) {
125        #[cfg(feature = "cbor")]
126        // cbor is in application/octet-stream.
127        // TODO: wait for mime to add application/cbor and match against that too
128        (mime::OCTET_STREAM, _) | (mime::APPLICATION, mime::OCTET_STREAM) => {
129            receive_batch_cbor(body).await
130        }
131        // default to json
132        _ => receive_batch_json(body).await,
133    }
134}
135/// Receive a GraphQL request from a body as JSON.
136pub async fn receive_json(body: impl AsyncRead) -> Result<Request, ParseRequestError> {
137    receive_batch_json(body).await?.into_single()
138}
139
140/// Receive a GraphQL batch request from a body as JSON.
141pub async fn receive_batch_json(body: impl AsyncRead) -> Result<BatchRequest, ParseRequestError> {
142    let mut data = Vec::new();
143    futures_util::pin_mut!(body);
144    body.read_to_end(&mut data)
145        .await
146        .map_err(ParseRequestError::Io)?;
147    serde_json::from_slice::<BatchRequest>(&data)
148        .map_err(|e| ParseRequestError::InvalidRequest(Box::new(e)))
149}
150
151/// Receive a GraphQL request from a body as CBOR.
152#[cfg(feature = "cbor")]
153#[cfg_attr(docsrs, doc(cfg(feature = "cbor")))]
154pub async fn receive_cbor(body: impl AsyncRead) -> Result<Request, ParseRequestError> {
155    receive_batch_cbor(body).await?.into_single()
156}
157
158/// Receive a GraphQL batch request from a body as CBOR
159#[cfg(feature = "cbor")]
160#[cfg_attr(docsrs, doc(cfg(feature = "cbor")))]
161pub async fn receive_batch_cbor(body: impl AsyncRead) -> Result<BatchRequest, ParseRequestError> {
162    let mut data = Vec::new();
163    futures_util::pin_mut!(body);
164    body.read_to_end(&mut data)
165        .await
166        .map_err(ParseRequestError::Io)?;
167    serde_cbor::from_slice::<BatchRequest>(&data)
168        .map_err(|e| ParseRequestError::InvalidRequest(Box::new(e)))
169}
170
171#[cfg(test)]
172mod tests {
173    use std::collections::HashMap;
174
175    use async_graphql_value::Extensions;
176
177    use super::*;
178    use crate::{Variables, value};
179
180    #[test]
181    fn test_parse_query_string() {
182        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();
183        assert_eq!(request.query.as_str(), "");
184        assert_eq!(request.variables, Variables::default());
185        assert_eq!(request.extensions, {
186            let mut extensions = HashMap::new();
187            extensions.insert("persistedQuery".to_string(), value!({
188                "sha256Hash": "cde5de0a350a19c59f8ddcd9646e5f260b2a7d5649ff6be8e63e9462934542c3",
189                "version": 1,
190            }));
191            Extensions(extensions)
192        });
193
194        let request = parse_query_string("query={a}&variables=%7B%22a%22%3A10%7D").unwrap();
195        assert_eq!(request.query.as_str(), "{a}");
196        assert_eq!(
197            request.variables,
198            Variables::from_value(value!({ "a" : 10 }))
199        );
200    }
201}