1use serde::{Deserialize, Serialize};
8use serde_json::{Value, json};
9use thiserror::Error;
10
11pub type Result<T> = std::result::Result<T, GraphQLError>;
13
14#[derive(Error, Debug, Clone, Serialize, Deserialize)]
19pub enum GraphQLError {
20 #[error("execution error: {0}")]
24 ExecutionError(String),
25
26 #[error("schema build error: {0}")]
30 SchemaBuildError(String),
31
32 #[error("request handling error: {0}")]
36 RequestHandlingError(String),
37
38 #[error("serialization error: {0}")]
42 SerializationError(String),
43
44 #[error("JSON error: {0}")]
48 JsonError(String),
49
50 #[error("GraphQL validation error: {0}")]
54 ValidationError(String),
55
56 #[error("GraphQL parse error: {0}")]
60 ParseError(String),
61
62 #[error("Authentication error: {0}")]
66 AuthenticationError(String),
67
68 #[error("Authorization error: {0}")]
72 AuthorizationError(String),
73
74 #[error("Not found: {0}")]
78 NotFound(String),
79
80 #[error("Rate limit exceeded: {0}")]
84 RateLimitExceeded(String),
85
86 #[error("Invalid input: {message}")]
90 InvalidInput {
91 message: String,
93 #[source]
95 details: Option<Box<Self>>,
96 },
97
98 #[error("Query complexity limit exceeded")]
102 ComplexityLimitExceeded,
103
104 #[error("Query depth limit exceeded")]
108 DepthLimitExceeded,
109
110 #[error("Internal server error: {0}")]
114 InternalError(String),
115}
116
117impl GraphQLError {
118 #[must_use]
142 pub const fn status_code(&self) -> u16 {
143 match self {
144 Self::ParseError(_) | Self::JsonError(_) | Self::RequestHandlingError(_) => 400,
145 Self::ValidationError(_)
146 | Self::InvalidInput { .. }
147 | Self::ComplexityLimitExceeded
148 | Self::DepthLimitExceeded => 422,
149 Self::AuthenticationError(_) => 401,
150 Self::AuthorizationError(_) => 403,
151 Self::NotFound(_) => 404,
152 Self::RateLimitExceeded(_) => 429,
153 Self::ExecutionError(_) => 200, Self::SchemaBuildError(_) | Self::SerializationError(_) | Self::InternalError(_) => 500,
155 }
156 }
157
158 #[must_use]
190 pub fn to_graphql_response(&self) -> Value {
191 json!({
192 "errors": [{
193 "message": self.to_string(),
194 "extensions": {
195 "code": self.error_code(),
196 "status": self.status_code(),
197 "type": self.error_type_uri()
198 }
199 }]
200 })
201 }
202
203 #[must_use]
235 pub fn to_http_response(&self) -> Value {
236 let status = self.status_code();
237 let title = match self {
238 Self::ParseError(_) | Self::JsonError(_) | Self::RequestHandlingError(_) => "Bad Request",
239 Self::ValidationError(_)
240 | Self::InvalidInput { .. }
241 | Self::ComplexityLimitExceeded
242 | Self::DepthLimitExceeded => "Validation Failed",
243 Self::AuthenticationError(_) => "Unauthorized",
244 Self::AuthorizationError(_) => "Forbidden",
245 Self::NotFound(_) => "Not Found",
246 Self::RateLimitExceeded(_) => "Too Many Requests",
247 Self::ExecutionError(_) => "Execution Error",
248 Self::SchemaBuildError(_) | Self::SerializationError(_) | Self::InternalError(_) => "Internal Server Error",
249 };
250
251 json!({
252 "type": self.error_type_uri(),
253 "title": title,
254 "status": status,
255 "detail": self.to_string(),
256 "errors": [{
257 "type": self.error_code(),
258 "message": self.to_string()
259 }]
260 })
261 }
262
263 #[must_use]
269 pub const fn is_transient(&self) -> bool {
270 matches!(
271 self,
272 Self::RateLimitExceeded(_) | Self::InternalError(_) | Self::ExecutionError(_)
273 )
274 }
275
276 #[must_use]
282 pub const fn error_type(&self) -> &'static str {
283 self.error_code()
284 }
285
286 #[must_use]
290 pub const fn error_code(&self) -> &'static str {
291 match self {
292 Self::ParseError(_) => "GRAPHQL_PARSE_ERROR",
293 Self::JsonError(_) => "JSON_ERROR",
294 Self::ValidationError(_) => "GRAPHQL_VALIDATION_FAILED",
295 Self::ExecutionError(_) => "GRAPHQL_EXECUTION_ERROR",
296 Self::SchemaBuildError(_) => "GRAPHQL_SCHEMA_BUILD_ERROR",
297 Self::RequestHandlingError(_) => "REQUEST_HANDLING_ERROR",
298 Self::SerializationError(_) => "SERIALIZATION_ERROR",
299 Self::AuthenticationError(_) => "AUTHENTICATION_FAILED",
300 Self::AuthorizationError(_) => "AUTHORIZATION_FAILED",
301 Self::NotFound(_) => "NOT_FOUND",
302 Self::RateLimitExceeded(_) => "RATE_LIMIT_EXCEEDED",
303 Self::InvalidInput { .. } => "VALIDATION_ERROR",
304 Self::ComplexityLimitExceeded => "GRAPHQL_COMPLEXITY_LIMIT_EXCEEDED",
305 Self::DepthLimitExceeded => "GRAPHQL_DEPTH_LIMIT_EXCEEDED",
306 Self::InternalError(_) => "INTERNAL_SERVER_ERROR",
307 }
308 }
309
310 const fn error_type_uri(&self) -> &'static str {
314 match self {
315 Self::ParseError(_) => "https://spikard.dev/errors/graphql-parse-error",
316 Self::JsonError(_) => "https://spikard.dev/errors/json-error",
317 Self::ValidationError(_) => "https://spikard.dev/errors/graphql-validation-error",
318 Self::ExecutionError(_) => "https://spikard.dev/errors/graphql-execution-error",
319 Self::SchemaBuildError(_) => "https://spikard.dev/errors/schema-build-error",
320 Self::RequestHandlingError(_) => "https://spikard.dev/errors/request-handling-error",
321 Self::SerializationError(_) => "https://spikard.dev/errors/serialization-error",
322 Self::AuthenticationError(_) => "https://spikard.dev/errors/authentication-error",
323 Self::AuthorizationError(_) => "https://spikard.dev/errors/authorization-error",
324 Self::NotFound(_) => "https://spikard.dev/errors/not-found",
325 Self::RateLimitExceeded(_) => "https://spikard.dev/errors/rate-limit-exceeded",
326 Self::InvalidInput { .. } => "https://spikard.dev/errors/validation-error",
327 Self::ComplexityLimitExceeded => "https://spikard.dev/errors/complexity-limit-exceeded",
328 Self::DepthLimitExceeded => "https://spikard.dev/errors/depth-limit-exceeded",
329 Self::InternalError(_) => "https://spikard.dev/errors/internal-server-error",
330 }
331 }
332}
333
334#[cfg(test)]
335mod tests {
336 use super::*;
337
338 #[test]
339 fn test_status_code_parse_error() {
340 let error = GraphQLError::ParseError("Invalid syntax".to_string());
341 assert_eq!(error.status_code(), 400);
342 }
343
344 #[test]
345 fn test_status_code_validation_error() {
346 let error = GraphQLError::ValidationError("Invalid query".to_string());
347 assert_eq!(error.status_code(), 422);
348 }
349
350 #[test]
351 fn test_status_code_authentication_error() {
352 let error = GraphQLError::AuthenticationError("Invalid token".to_string());
353 assert_eq!(error.status_code(), 401);
354 }
355
356 #[test]
357 fn test_status_code_authorization_error() {
358 let error = GraphQLError::AuthorizationError("Forbidden".to_string());
359 assert_eq!(error.status_code(), 403);
360 }
361
362 #[test]
363 fn test_status_code_not_found() {
364 let error = GraphQLError::NotFound("User not found".to_string());
365 assert_eq!(error.status_code(), 404);
366 }
367
368 #[test]
369 fn test_status_code_rate_limit() {
370 let error = GraphQLError::RateLimitExceeded("Too many requests".to_string());
371 assert_eq!(error.status_code(), 429);
372 }
373
374 #[test]
375 fn test_status_code_execution_error() {
376 let error = GraphQLError::ExecutionError("Query execution failed".to_string());
377 assert_eq!(error.status_code(), 200); }
379
380 #[test]
381 fn test_to_graphql_response_structure() {
382 let error = GraphQLError::ValidationError("Invalid query".to_string());
383 let response = error.to_graphql_response();
384
385 assert!(response["errors"].is_array());
386 assert_eq!(response["errors"].as_array().unwrap().len(), 1);
387 assert!(response["errors"][0]["message"].is_string());
388 assert!(response["errors"][0]["extensions"]["code"].is_string());
389 assert_eq!(response["errors"][0]["extensions"]["code"], "GRAPHQL_VALIDATION_FAILED");
390 assert_eq!(response["errors"][0]["extensions"]["status"], 422);
391 }
392
393 #[test]
394 fn test_to_http_response_structure() {
395 let error = GraphQLError::AuthenticationError("Invalid token".to_string());
396 let response = error.to_http_response();
397
398 assert_eq!(response["status"], 401);
399 assert_eq!(response["title"], "Unauthorized");
400 assert!(response["type"].is_string());
401 assert!(response["errors"].is_array());
402 assert_eq!(response["errors"][0]["type"], "AUTHENTICATION_FAILED");
403 }
404
405 #[test]
406 fn test_error_code_serialization() {
407 let error = GraphQLError::InvalidInput {
408 message: "Field required".to_string(),
409 details: None,
410 };
411 assert_eq!(error.error_code(), "VALIDATION_ERROR");
412 }
413
414 #[test]
415 fn test_error_type_uri_parse_error() {
416 let error = GraphQLError::ParseError("Invalid".to_string());
417 assert_eq!(error.error_type_uri(), "https://spikard.dev/errors/graphql-parse-error");
418 }
419
420 #[test]
421 fn test_json_error_creation() {
422 let json_error = GraphQLError::JsonError("Invalid JSON".to_string());
423 assert_eq!(json_error.error_code(), "JSON_ERROR");
424 assert_eq!(json_error.status_code(), 400);
425 }
426
427 #[test]
428 fn test_error_message_display() {
429 let error = GraphQLError::ExecutionError("Query failed".to_string());
430 assert_eq!(error.to_string(), "execution error: Query failed");
431 }
432
433 #[test]
434 fn test_invalid_input_error_with_details() {
435 let detail_error = Box::new(GraphQLError::ValidationError("Field required".to_string()));
436 let error = GraphQLError::InvalidInput {
437 message: "Invalid input provided".to_string(),
438 details: Some(detail_error),
439 };
440
441 let response = error.to_http_response();
442 assert_eq!(response["status"], 422);
443 assert_eq!(response["title"], "Validation Failed");
444 }
445
446 #[test]
447 fn test_rate_limit_error_status() {
448 let error = GraphQLError::RateLimitExceeded("Limit: 100 requests/min".to_string());
449 let response = error.to_http_response();
450 assert_eq!(response["status"], 429);
451 assert_eq!(response["title"], "Too Many Requests");
452 }
453
454 #[test]
455 fn test_not_found_error_conversion() {
456 let error = GraphQLError::NotFound("Product ID 123 not found".to_string());
457 let response = error.to_http_response();
458 assert_eq!(response["status"], 404);
459 assert_eq!(response["title"], "Not Found");
460 assert_eq!(response["errors"][0]["type"], "NOT_FOUND");
461 }
462
463 #[test]
464 fn test_schema_build_error() {
465 let error = GraphQLError::SchemaBuildError("Duplicate type definition".to_string());
466 assert_eq!(error.status_code(), 500);
467 let response = error.to_graphql_response();
468 assert_eq!(
469 response["errors"][0]["extensions"]["code"],
470 "GRAPHQL_SCHEMA_BUILD_ERROR"
471 );
472 }
473
474 #[test]
475 fn test_complexity_limit_exceeded_status_code() {
476 let error = GraphQLError::ComplexityLimitExceeded;
477 assert_eq!(error.status_code(), 422);
478 assert_eq!(error.error_code(), "GRAPHQL_COMPLEXITY_LIMIT_EXCEEDED");
479 }
480
481 #[test]
482 fn test_depth_limit_exceeded_status_code() {
483 let error = GraphQLError::DepthLimitExceeded;
484 assert_eq!(error.status_code(), 422);
485 assert_eq!(error.error_code(), "GRAPHQL_DEPTH_LIMIT_EXCEEDED");
486 }
487
488 #[test]
489 fn test_complexity_limit_exceeded_response() {
490 let error = GraphQLError::ComplexityLimitExceeded;
491 let response = error.to_graphql_response();
492 assert_eq!(
493 response["errors"][0]["extensions"]["code"],
494 "GRAPHQL_COMPLEXITY_LIMIT_EXCEEDED"
495 );
496 assert_eq!(response["errors"][0]["extensions"]["status"], 422);
497 }
498
499 #[test]
500 fn test_depth_limit_exceeded_response() {
501 let error = GraphQLError::DepthLimitExceeded;
502 let response = error.to_graphql_response();
503 assert_eq!(
504 response["errors"][0]["extensions"]["code"],
505 "GRAPHQL_DEPTH_LIMIT_EXCEEDED"
506 );
507 assert_eq!(response["errors"][0]["extensions"]["status"], 422);
508 }
509
510 #[test]
511 fn test_complexity_limit_exceeded_error_type_uri() {
512 let error = GraphQLError::ComplexityLimitExceeded;
513 assert_eq!(
514 error.error_type_uri(),
515 "https://spikard.dev/errors/complexity-limit-exceeded"
516 );
517 }
518
519 #[test]
520 fn test_depth_limit_exceeded_error_type_uri() {
521 let error = GraphQLError::DepthLimitExceeded;
522 assert_eq!(
523 error.error_type_uri(),
524 "https://spikard.dev/errors/depth-limit-exceeded"
525 );
526 }
527
528 #[test]
529 fn test_all_error_codes_are_static() {
530 let errors = vec![
531 GraphQLError::ParseError(String::new()),
532 GraphQLError::JsonError(String::new()),
533 GraphQLError::ValidationError(String::new()),
534 GraphQLError::ExecutionError(String::new()),
535 GraphQLError::SchemaBuildError(String::new()),
536 GraphQLError::RequestHandlingError(String::new()),
537 GraphQLError::SerializationError(String::new()),
538 GraphQLError::AuthenticationError(String::new()),
539 GraphQLError::AuthorizationError(String::new()),
540 GraphQLError::NotFound(String::new()),
541 GraphQLError::RateLimitExceeded(String::new()),
542 GraphQLError::InvalidInput {
543 message: String::new(),
544 details: None,
545 },
546 GraphQLError::InternalError(String::new()),
547 ];
548
549 for error in errors {
550 let code = error.error_code();
551 let response = error.to_graphql_response();
552 assert_eq!(response["errors"][0]["extensions"]["code"].as_str(), Some(code));
553 }
554 }
555}