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