Skip to main content

fraiseql_error/
graphql_error.rs

1//! Canonical GraphQL protocol error types.
2//!
3//! Implements the [GraphQL specification](https://spec.graphql.org/October2021/#sec-Errors)
4//! error format used across the FraiseQL workspace — `WebSocket` subscription protocol
5//! (graphql-ws v5+), HTTP response bodies, and federation subgraph communication.
6//!
7//! # Structure
8//!
9//! ```json
10//! {
11//!   "message": "Cannot query field 'id' on type 'User'.",
12//!   "locations": [{ "line": 3, "column": 5 }],
13//!   "path": ["user", "friends", 0, "name"],
14//!   "extensions": { "code": "FIELD_NOT_FOUND" }
15//! }
16//! ```
17
18use std::collections::HashMap;
19
20use serde::{Deserialize, Serialize};
21
22/// Location in a GraphQL document (source text) where an error occurred.
23///
24/// Line and column are 1-indexed as required by the GraphQL specification.
25#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
26pub struct GraphQLErrorLocation {
27    /// Line number in the query document (1-indexed).
28    pub line:   u32,
29    /// Column number in the query document (1-indexed).
30    pub column: u32,
31}
32
33/// A GraphQL protocol-level error as defined in the
34/// [GraphQL specification §7.1.2](https://spec.graphql.org/October2021/#sec-Errors).
35///
36/// This is the canonical wire format used by:
37/// - `WebSocket` subscription protocol (`graphql-ws` v5+) `error` messages
38/// - HTTP GraphQL response `errors` array
39/// - Federation subgraph HTTP responses
40///
41/// Crates that need HTTP-layer concerns (error codes, status codes, sanitization)
42/// build on top of this type rather than replacing it.
43#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct GraphQLError {
45    /// Human-readable error message.
46    pub message: String,
47
48    /// Locations in the query document where the error occurred.
49    #[serde(skip_serializing_if = "Option::is_none")]
50    pub locations: Option<Vec<GraphQLErrorLocation>>,
51
52    /// Path into the response data where the error occurred.
53    ///
54    /// Elements are either string field names or integer list indices.
55    #[serde(skip_serializing_if = "Option::is_none")]
56    pub path: Option<Vec<serde_json::Value>>,
57
58    /// Arbitrary extension data (error codes, categories, etc.).
59    #[serde(skip_serializing_if = "Option::is_none")]
60    pub extensions: Option<HashMap<String, serde_json::Value>>,
61}
62
63impl GraphQLError {
64    /// Create a simple error with only a message.
65    #[must_use]
66    pub fn new(message: impl Into<String>) -> Self {
67        Self {
68            message:    message.into(),
69            locations:  None,
70            path:       None,
71            extensions: None,
72        }
73    }
74
75    /// Create an error with a string extension code.
76    ///
77    /// This is the conventional way to attach a machine-readable error code:
78    ///
79    /// ```rust
80    /// use fraiseql_error::GraphQLError;
81    ///
82    /// let err = GraphQLError::with_code("Subscription not found", "SUBSCRIPTION_NOT_FOUND");
83    /// ```
84    #[must_use]
85    pub fn with_code(message: impl Into<String>, code: impl Into<String>) -> Self {
86        let mut extensions = HashMap::new();
87        extensions.insert("code".to_string(), serde_json::json!(code.into()));
88        Self {
89            message:    message.into(),
90            locations:  None,
91            path:       None,
92            extensions: Some(extensions),
93        }
94    }
95
96    /// Add a source location to this error.
97    #[must_use]
98    pub fn with_location(mut self, line: u32, column: u32) -> Self {
99        let loc = GraphQLErrorLocation { line, column };
100        self.locations.get_or_insert_with(Vec::new).push(loc);
101        self
102    }
103
104    /// Add a response path to this error.
105    #[must_use]
106    pub fn with_path(mut self, path: Vec<serde_json::Value>) -> Self {
107        self.path = Some(path);
108        self
109    }
110}