Skip to main content

spikard_graphql/
handler.rs

1//! GraphQL HTTP handler integration with Spikard
2//!
3//! This module provides HTTP request handling for GraphQL queries and mutations,
4//! implementing the `Handler` trait for integration with Spikard's HTTP server
5//! and tower-http middleware stack.
6
7use crate::GraphQLExecutor;
8use crate::error::GraphQLError;
9use axum::{
10    body::Body,
11    http::{Request, Response, StatusCode},
12};
13use serde::{Deserialize, Serialize};
14use serde_json::{Value, json};
15use spikard_http::{Handler, HandlerResult, RequestData};
16use std::future::Future;
17use std::pin::Pin;
18use std::sync::Arc;
19
20/// Maximum allowed GraphQL query size (1MB)
21const MAX_QUERY_SIZE: usize = 1_048_576;
22
23/// GraphQL request payload
24///
25/// Represents a standard GraphQL HTTP request body as defined by the GraphQL
26/// specification for application/json requests.
27#[derive(Debug, Deserialize, Serialize, Clone)]
28pub struct GraphQLRequestPayload {
29    /// The GraphQL query string
30    pub query: String,
31
32    /// Optional query variables as a JSON object
33    #[serde(default)]
34    pub variables: Option<Value>,
35
36    /// Optional operation name for selecting which operation to execute
37    /// when the query contains multiple operations
38    #[serde(rename = "operationName")]
39    pub operation_name: Option<String>,
40}
41
42/// GraphQL response payload
43///
44/// Represents a standard GraphQL HTTP response body containing data
45/// and optional errors.
46#[derive(Debug, Serialize, Deserialize, Clone)]
47pub struct GraphQLResponsePayload {
48    /// Execution result data
49    #[serde(skip_serializing_if = "Option::is_none")]
50    pub data: Option<Value>,
51
52    /// Execution errors (null if successful)
53    #[serde(skip_serializing_if = "Option::is_none")]
54    pub errors: Option<Vec<GraphQLErrorResponse>>,
55
56    /// Extensions (optional, for tracing, etc.)
57    #[serde(skip_serializing_if = "Option::is_none")]
58    pub extensions: Option<Value>,
59}
60
61/// GraphQL error response format
62#[derive(Debug, Serialize, Deserialize, Clone)]
63pub struct GraphQLErrorResponse {
64    /// Error message
65    pub message: String,
66
67    /// Optional locations in the query where the error occurred
68    #[serde(skip_serializing_if = "Option::is_none")]
69    pub locations: Option<Vec<Value>>,
70
71    /// Optional path to the field that caused the error
72    #[serde(skip_serializing_if = "Option::is_none")]
73    pub path: Option<Vec<Value>>,
74
75    /// Optional additional error details
76    #[serde(skip_serializing_if = "Option::is_none")]
77    pub extensions: Option<Value>,
78}
79
80/// Parse GraphQL request from HTTP request body
81///
82/// # Arguments
83///
84/// * `raw_body` - Raw request body bytes
85///
86/// # Returns
87///
88/// Parsed GraphQL request payload or error
89fn parse_graphql_request(raw_body: &[u8]) -> Result<GraphQLRequestPayload, GraphQLError> {
90    if raw_body.len() > MAX_QUERY_SIZE {
91        return Err(GraphQLError::RequestHandlingError(format!(
92            "Request body exceeds maximum size of {MAX_QUERY_SIZE} bytes"
93        )));
94    }
95    serde_json::from_slice(raw_body)
96        .map_err(|e| GraphQLError::RequestHandlingError(format!("Failed to parse GraphQL request: {e}")))
97}
98
99/// GraphQL HTTP handler
100///
101/// Integrates GraphQL execution with Spikard's HTTP server and tower-http
102/// middleware stack. Handles parsing GraphQL requests, executing them via
103/// the GraphQL executor, and returning properly formatted responses.
104///
105/// # Type Parameters
106///
107/// * `Query` - The root GraphQL Query type
108/// * `Mutation` - The root GraphQL Mutation type
109/// * `Subscription` - The root GraphQL Subscription type
110///
111/// # Example
112///
113/// ```ignore
114/// use spikard_graphql::{GraphQLHandler, GraphQLExecutor};
115///
116/// // With concrete types:
117/// // let executor = GraphQLExecutor::<Query, Mutation, Subscription>::new(schema);
118/// // let handler = GraphQLHandler::new(Arc::new(executor));
119/// ```
120#[derive(Debug)]
121pub struct GraphQLHandler<Query, Mutation, Subscription> {
122    executor: Arc<GraphQLExecutor<Query, Mutation, Subscription>>,
123}
124
125impl<Query, Mutation, Subscription> GraphQLHandler<Query, Mutation, Subscription>
126where
127    Query: async_graphql::ObjectType + Send + Sync + 'static,
128    Mutation: async_graphql::ObjectType + Send + Sync + 'static,
129    Subscription: async_graphql::SubscriptionType + Send + Sync + 'static,
130{
131    /// Create a new GraphQL handler
132    ///
133    /// # Arguments
134    ///
135    /// * `executor` - Arc-wrapped GraphQL executor instance
136    ///
137    /// # Returns
138    ///
139    /// A new `GraphQLHandler` instance
140    #[must_use]
141    pub const fn new(executor: Arc<GraphQLExecutor<Query, Mutation, Subscription>>) -> Self {
142        Self { executor }
143    }
144
145    /// Handle a GraphQL request
146    ///
147    /// # Arguments
148    ///
149    /// * `request_data` - The incoming request data
150    ///
151    /// # Returns
152    ///
153    /// Handler result with GraphQL response
154    ///
155    /// # Errors
156    ///
157    /// Returns `GraphQLError` if the request body cannot be parsed or execution fails.
158    pub async fn handle_graphql(&self, request_data: &RequestData) -> Result<Value, GraphQLError> {
159        // Extract raw body
160        let body_bytes = request_data.raw_body.as_ref().map_or_else(
161            || serde_json::to_vec(&request_data.body).unwrap_or_default(),
162            |raw_body| raw_body.to_vec(),
163        );
164
165        // Parse the request payload
166        let payload = parse_graphql_request(&body_bytes)?;
167
168        // Execute the GraphQL query
169        self.executor
170            .execute(
171                &payload.query,
172                payload.variables.as_ref(),
173                payload.operation_name.as_deref(),
174            )
175            .await
176    }
177
178    /// Create an HTTP response from a GraphQL result
179    fn response_from_result(result: Result<Value, GraphQLError>) -> Response<Body> {
180        match result {
181            Ok(graphql_response) => {
182                let body = serde_json::to_vec(&graphql_response).unwrap_or_else(|_| {
183                    serde_json::to_vec(&json!({"errors": [{"message": "Failed to serialize GraphQL response"}]}))
184                        .unwrap_or_else(|_| b"{\"errors\":[{\"message\":\"Internal server error\"}]}".to_vec())
185                });
186                Response::builder()
187                    .status(StatusCode::OK)
188                    .header("content-type", "application/json")
189                    .body(Body::from(body))
190                    .unwrap_or_else(|_| {
191                        Response::builder()
192                            .status(StatusCode::INTERNAL_SERVER_ERROR)
193                            .body(Body::from("Internal server error"))
194                            .unwrap()
195                    })
196            }
197            Err(e) => {
198                let status = StatusCode::from_u16(e.status_code()).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
199                let error_response = e.to_graphql_response();
200                let body = serde_json::to_vec(&error_response).unwrap_or_else(|_| {
201                    serde_json::to_vec(&json!({"errors": [{"message": "Internal server error"}]}))
202                        .unwrap_or_else(|_| b"{\"errors\":[{\"message\":\"Internal server error\"}]}".to_vec())
203                });
204
205                Response::builder()
206                    .status(status)
207                    .header("content-type", "application/json")
208                    .body(Body::from(body))
209                    .unwrap_or_else(|_| {
210                        Response::builder()
211                            .status(StatusCode::INTERNAL_SERVER_ERROR)
212                            .body(Body::from("Internal server error"))
213                            .unwrap()
214                    })
215            }
216        }
217    }
218}
219
220impl<Query, Mutation, Subscription> Clone for GraphQLHandler<Query, Mutation, Subscription> {
221    fn clone(&self) -> Self {
222        Self {
223            executor: Arc::clone(&self.executor),
224        }
225    }
226}
227
228impl<Query, Mutation, Subscription> Handler for GraphQLHandler<Query, Mutation, Subscription>
229where
230    Query: async_graphql::ObjectType + Send + Sync + 'static,
231    Mutation: async_graphql::ObjectType + Send + Sync + 'static,
232    Subscription: async_graphql::SubscriptionType + Send + Sync + 'static,
233{
234    fn call(
235        &self,
236        _request: Request<Body>,
237        request_data: RequestData,
238    ) -> Pin<Box<dyn Future<Output = HandlerResult> + Send + '_>> {
239        Box::pin(async move {
240            let result = self.handle_graphql(&request_data).await;
241            Ok(Self::response_from_result(result))
242        })
243    }
244
245    fn prefers_raw_json_body(&self) -> bool {
246        true
247    }
248
249    fn wants_headers(&self) -> bool {
250        false
251    }
252
253    fn wants_cookies(&self) -> bool {
254        false
255    }
256}
257
258#[cfg(test)]
259mod tests {
260    use super::*;
261    use async_graphql::{EmptyMutation, EmptySubscription, Object, Schema};
262
263    #[derive(Default)]
264    struct TestQuery;
265
266    #[Object]
267    impl TestQuery {
268        async fn hello(&self) -> &'static str {
269            "world"
270        }
271    }
272
273    fn make_test_handler() -> GraphQLHandler<TestQuery, EmptyMutation, EmptySubscription> {
274        let schema = Schema::build(TestQuery, EmptyMutation, EmptySubscription).finish();
275        let executor = Arc::new(GraphQLExecutor::new(schema));
276        GraphQLHandler::new(executor)
277    }
278
279    #[test]
280    fn test_graphql_request_payload_parsing_minimal() {
281        let json = r#"{"query":"{ hello }"}"#;
282        let payload: GraphQLRequestPayload = serde_json::from_str(json).unwrap();
283
284        assert_eq!(payload.query, "{ hello }");
285        assert!(payload.variables.is_none());
286        assert!(payload.operation_name.is_none());
287    }
288
289    #[test]
290    fn test_graphql_request_payload_parsing_with_variables() {
291        let json = r#"{"query":"query GetUser($id: ID!) { user(id: $id) { name } }","variables":{"id":"123"}}"#;
292        let payload: GraphQLRequestPayload = serde_json::from_str(json).unwrap();
293
294        assert_eq!(payload.query, "query GetUser($id: ID!) { user(id: $id) { name } }");
295        assert!(payload.variables.is_some());
296        assert_eq!(payload.variables.as_ref().unwrap()["id"], "123");
297    }
298
299    #[test]
300    fn test_graphql_request_payload_parsing_with_operation_name() {
301        let json =
302            r#"{"query":"query GetUser { user { name } } query ListUsers { users { id } }","operationName":"GetUser"}"#;
303        let payload: GraphQLRequestPayload = serde_json::from_str(json).unwrap();
304
305        assert_eq!(payload.operation_name, Some("GetUser".to_string()));
306    }
307
308    #[test]
309    fn test_graphql_request_payload_serialization() {
310        let payload = GraphQLRequestPayload {
311            query: "{ hello }".to_string(),
312            variables: Some(json!({"test": "value"})),
313            operation_name: Some("TestOp".to_string()),
314        };
315
316        let json = serde_json::to_string(&payload).unwrap();
317        let parsed: GraphQLRequestPayload = serde_json::from_str(&json).unwrap();
318
319        assert_eq!(parsed.query, payload.query);
320        assert_eq!(parsed.variables, payload.variables);
321        assert_eq!(parsed.operation_name, payload.operation_name);
322    }
323
324    #[test]
325    fn test_graphql_response_payload_success() {
326        let payload = GraphQLResponsePayload {
327            data: Some(json!({"hello": "world"})),
328            errors: None,
329            extensions: None,
330        };
331
332        let json = serde_json::to_string(&payload).unwrap();
333        assert!(json.contains("\"data\""));
334        assert!(!json.contains("\"errors\""));
335    }
336
337    #[test]
338    fn test_graphql_response_payload_with_errors() {
339        let error = GraphQLErrorResponse {
340            message: "Test error".to_string(),
341            locations: None,
342            path: None,
343            extensions: None,
344        };
345
346        let payload = GraphQLResponsePayload {
347            data: None,
348            errors: Some(vec![error]),
349            extensions: None,
350        };
351
352        let json = serde_json::to_string(&payload).unwrap();
353        assert!(json.contains("\"errors\""));
354        assert!(json.contains("Test error"));
355    }
356
357    #[test]
358    fn test_parse_request_invalid_json() {
359        let result = parse_graphql_request(b"invalid json");
360        assert!(result.is_err());
361    }
362
363    #[test]
364    fn test_parse_request_missing_query() {
365        let result = parse_graphql_request(b"{}");
366        assert!(result.is_err());
367    }
368
369    #[test]
370    fn test_parse_request_exceeds_size_limit() {
371        let huge_request = vec![b'x'; MAX_QUERY_SIZE + 1];
372        let result = parse_graphql_request(&huge_request);
373        assert!(result.is_err());
374        match result.unwrap_err() {
375            GraphQLError::RequestHandlingError(msg) => {
376                assert!(msg.contains("exceeds maximum size"));
377                assert!(msg.contains("1048576"));
378            }
379            _ => panic!("Expected RequestHandlingError"),
380        }
381    }
382
383    #[test]
384    fn test_parse_request_at_size_limit() {
385        let request_at_limit = vec![b'x'; MAX_QUERY_SIZE];
386        let result = parse_graphql_request(&request_at_limit);
387        assert!(result.is_err());
388        match result.unwrap_err() {
389            GraphQLError::RequestHandlingError(msg) => {
390                assert!(!msg.contains("exceeds maximum size"));
391                assert!(msg.contains("Failed to parse GraphQL request"));
392            }
393            _ => panic!("Expected RequestHandlingError"),
394        }
395    }
396
397    #[test]
398    fn test_parse_request_normal_size_succeeds() {
399        let json = r#"{"query":"{ hello }"}"#;
400        let result = parse_graphql_request(json.as_bytes());
401        assert!(result.is_ok());
402        let payload = result.unwrap();
403        assert_eq!(payload.query, "{ hello }");
404    }
405
406    #[test]
407    fn test_handler_prefers_raw_json_body() {
408        let handler = make_test_handler();
409        assert!(handler.prefers_raw_json_body());
410    }
411
412    #[test]
413    fn test_handler_does_not_want_headers() {
414        let handler = make_test_handler();
415        assert!(!handler.wants_headers());
416    }
417
418    #[test]
419    fn test_handler_does_not_want_cookies() {
420        let handler = make_test_handler();
421        assert!(!handler.wants_cookies());
422    }
423
424    #[test]
425    fn test_handler_clone() {
426        let handler1 = make_test_handler();
427        let handler2 = handler1.clone();
428
429        // Verify Arc is shared
430        assert!(Arc::ptr_eq(&handler1.executor, &handler2.executor));
431    }
432}