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 },
94
95 #[error("Query complexity limit exceeded")]
99 ComplexityLimitExceeded,
100
101 #[error("Query depth limit exceeded")]
105 DepthLimitExceeded,
106
107 #[error("Internal server error: {0}")]
111 InternalError(String),
112}
113
114impl GraphQLError {
115 #[must_use]
139 pub const fn status_code(&self) -> u16 {
140 match self {
141 Self::ParseError(_) | Self::JsonError(_) | Self::RequestHandlingError(_) => 400,
142 Self::ValidationError(_)
143 | Self::InvalidInput { .. }
144 | Self::ComplexityLimitExceeded
145 | Self::DepthLimitExceeded => 422,
146 Self::AuthenticationError(_) => 401,
147 Self::AuthorizationError(_) => 403,
148 Self::NotFound(_) => 404,
149 Self::RateLimitExceeded(_) => 429,
150 Self::ExecutionError(_) => 200, Self::SchemaBuildError(_) | Self::SerializationError(_) | Self::InternalError(_) => 500,
152 }
153 }
154
155 #[must_use]
187 pub fn to_graphql_response(&self) -> Value {
188 json!({
189 "errors": [{
190 "message": self.to_string(),
191 "extensions": {
192 "code": self.error_code(),
193 "status": self.status_code(),
194 "type": self.error_type_uri()
195 }
196 }]
197 })
198 }
199
200 #[must_use]
232 pub fn to_http_response(&self) -> Value {
233 let status = self.status_code();
234 let title = match self {
235 Self::ParseError(_) | Self::JsonError(_) | Self::RequestHandlingError(_) => "Bad Request",
236 Self::ValidationError(_)
237 | Self::InvalidInput { .. }
238 | Self::ComplexityLimitExceeded
239 | Self::DepthLimitExceeded => "Validation Failed",
240 Self::AuthenticationError(_) => "Unauthorized",
241 Self::AuthorizationError(_) => "Forbidden",
242 Self::NotFound(_) => "Not Found",
243 Self::RateLimitExceeded(_) => "Too Many Requests",
244 Self::ExecutionError(_) => "Execution Error",
245 Self::SchemaBuildError(_) | Self::SerializationError(_) | Self::InternalError(_) => "Internal Server Error",
246 };
247
248 json!({
249 "type": self.error_type_uri(),
250 "title": title,
251 "status": status,
252 "detail": self.to_string(),
253 "errors": [{
254 "type": self.error_code(),
255 "message": self.to_string()
256 }]
257 })
258 }
259
260 #[must_use]
266 pub const fn is_transient(&self) -> bool {
267 matches!(
268 self,
269 Self::RateLimitExceeded(_) | Self::InternalError(_) | Self::ExecutionError(_)
270 )
271 }
272
273 #[must_use]
279 pub const fn error_type(&self) -> &'static str {
280 self.error_code()
281 }
282
283 #[must_use]
287 pub const fn error_code(&self) -> &'static str {
288 match self {
289 Self::ParseError(_) => "GRAPHQL_PARSE_ERROR",
290 Self::JsonError(_) => "JSON_ERROR",
291 Self::ValidationError(_) => "GRAPHQL_VALIDATION_FAILED",
292 Self::ExecutionError(_) => "GRAPHQL_EXECUTION_ERROR",
293 Self::SchemaBuildError(_) => "GRAPHQL_SCHEMA_BUILD_ERROR",
294 Self::RequestHandlingError(_) => "REQUEST_HANDLING_ERROR",
295 Self::SerializationError(_) => "SERIALIZATION_ERROR",
296 Self::AuthenticationError(_) => "AUTHENTICATION_FAILED",
297 Self::AuthorizationError(_) => "AUTHORIZATION_FAILED",
298 Self::NotFound(_) => "NOT_FOUND",
299 Self::RateLimitExceeded(_) => "RATE_LIMIT_EXCEEDED",
300 Self::InvalidInput { .. } => "VALIDATION_ERROR",
301 Self::ComplexityLimitExceeded => "GRAPHQL_COMPLEXITY_LIMIT_EXCEEDED",
302 Self::DepthLimitExceeded => "GRAPHQL_DEPTH_LIMIT_EXCEEDED",
303 Self::InternalError(_) => "INTERNAL_SERVER_ERROR",
304 }
305 }
306
307 const fn error_type_uri(&self) -> &'static str {
311 match self {
312 Self::ParseError(_) => "https://spikard.dev/errors/graphql-parse-error",
313 Self::JsonError(_) => "https://spikard.dev/errors/json-error",
314 Self::ValidationError(_) => "https://spikard.dev/errors/graphql-validation-error",
315 Self::ExecutionError(_) => "https://spikard.dev/errors/graphql-execution-error",
316 Self::SchemaBuildError(_) => "https://spikard.dev/errors/schema-build-error",
317 Self::RequestHandlingError(_) => "https://spikard.dev/errors/request-handling-error",
318 Self::SerializationError(_) => "https://spikard.dev/errors/serialization-error",
319 Self::AuthenticationError(_) => "https://spikard.dev/errors/authentication-error",
320 Self::AuthorizationError(_) => "https://spikard.dev/errors/authorization-error",
321 Self::NotFound(_) => "https://spikard.dev/errors/not-found",
322 Self::RateLimitExceeded(_) => "https://spikard.dev/errors/rate-limit-exceeded",
323 Self::InvalidInput { .. } => "https://spikard.dev/errors/validation-error",
324 Self::ComplexityLimitExceeded => "https://spikard.dev/errors/complexity-limit-exceeded",
325 Self::DepthLimitExceeded => "https://spikard.dev/errors/depth-limit-exceeded",
326 Self::InternalError(_) => "https://spikard.dev/errors/internal-server-error",
327 }
328 }
329}
330
331#[cfg(test)]
332mod tests {
333 use super::*;
334
335 #[test]
336 fn test_status_code_parse_error() {
337 let error = GraphQLError::ParseError("Invalid syntax".to_string());
338 assert_eq!(error.status_code(), 400);
339 }
340
341 #[test]
342 fn test_status_code_validation_error() {
343 let error = GraphQLError::ValidationError("Invalid query".to_string());
344 assert_eq!(error.status_code(), 422);
345 }
346
347 #[test]
348 fn test_status_code_authentication_error() {
349 let error = GraphQLError::AuthenticationError("Invalid token".to_string());
350 assert_eq!(error.status_code(), 401);
351 }
352
353 #[test]
354 fn test_status_code_authorization_error() {
355 let error = GraphQLError::AuthorizationError("Forbidden".to_string());
356 assert_eq!(error.status_code(), 403);
357 }
358
359 #[test]
360 fn test_status_code_not_found() {
361 let error = GraphQLError::NotFound("User not found".to_string());
362 assert_eq!(error.status_code(), 404);
363 }
364
365 #[test]
366 fn test_status_code_rate_limit() {
367 let error = GraphQLError::RateLimitExceeded("Too many requests".to_string());
368 assert_eq!(error.status_code(), 429);
369 }
370
371 #[test]
372 fn test_status_code_execution_error() {
373 let error = GraphQLError::ExecutionError("Query execution failed".to_string());
374 assert_eq!(error.status_code(), 200); }
376
377 #[test]
378 fn test_to_graphql_response_structure() {
379 let error = GraphQLError::ValidationError("Invalid query".to_string());
380 let response = error.to_graphql_response();
381
382 assert!(response["errors"].is_array());
383 assert_eq!(response["errors"].as_array().unwrap().len(), 1);
384 assert!(response["errors"][0]["message"].is_string());
385 assert!(response["errors"][0]["extensions"]["code"].is_string());
386 assert_eq!(response["errors"][0]["extensions"]["code"], "GRAPHQL_VALIDATION_FAILED");
387 assert_eq!(response["errors"][0]["extensions"]["status"], 422);
388 }
389
390 #[test]
391 fn test_to_http_response_structure() {
392 let error = GraphQLError::AuthenticationError("Invalid token".to_string());
393 let response = error.to_http_response();
394
395 assert_eq!(response["status"], 401);
396 assert_eq!(response["title"], "Unauthorized");
397 assert!(response["type"].is_string());
398 assert!(response["errors"].is_array());
399 assert_eq!(response["errors"][0]["type"], "AUTHENTICATION_FAILED");
400 }
401
402 #[test]
403 fn test_error_code_serialization() {
404 let error = GraphQLError::InvalidInput {
405 message: "Field required".to_string(),
406 };
407 assert_eq!(error.error_code(), "VALIDATION_ERROR");
408 }
409
410 #[test]
411 fn test_error_type_uri_parse_error() {
412 let error = GraphQLError::ParseError("Invalid".to_string());
413 assert_eq!(error.error_type_uri(), "https://spikard.dev/errors/graphql-parse-error");
414 }
415
416 #[test]
417 fn test_json_error_creation() {
418 let json_error = GraphQLError::JsonError("Invalid JSON".to_string());
419 assert_eq!(json_error.error_code(), "JSON_ERROR");
420 assert_eq!(json_error.status_code(), 400);
421 }
422
423 #[test]
424 fn test_error_message_display() {
425 let error = GraphQLError::ExecutionError("Query failed".to_string());
426 assert_eq!(error.to_string(), "execution error: Query failed");
427 }
428
429 #[test]
430 fn test_invalid_input_error() {
431 let error = GraphQLError::InvalidInput {
432 message: "Invalid input provided".to_string(),
433 };
434
435 let response = error.to_http_response();
436 assert_eq!(response["status"], 422);
437 assert_eq!(response["title"], "Validation Failed");
438 }
439
440 #[test]
441 fn test_rate_limit_error_status() {
442 let error = GraphQLError::RateLimitExceeded("Limit: 100 requests/min".to_string());
443 let response = error.to_http_response();
444 assert_eq!(response["status"], 429);
445 assert_eq!(response["title"], "Too Many Requests");
446 }
447
448 #[test]
449 fn test_not_found_error_conversion() {
450 let error = GraphQLError::NotFound("Product ID 123 not found".to_string());
451 let response = error.to_http_response();
452 assert_eq!(response["status"], 404);
453 assert_eq!(response["title"], "Not Found");
454 assert_eq!(response["errors"][0]["type"], "NOT_FOUND");
455 }
456
457 #[test]
458 fn test_schema_build_error() {
459 let error = GraphQLError::SchemaBuildError("Duplicate type definition".to_string());
460 assert_eq!(error.status_code(), 500);
461 let response = error.to_graphql_response();
462 assert_eq!(
463 response["errors"][0]["extensions"]["code"],
464 "GRAPHQL_SCHEMA_BUILD_ERROR"
465 );
466 }
467
468 #[test]
469 fn test_complexity_limit_exceeded_status_code() {
470 let error = GraphQLError::ComplexityLimitExceeded;
471 assert_eq!(error.status_code(), 422);
472 assert_eq!(error.error_code(), "GRAPHQL_COMPLEXITY_LIMIT_EXCEEDED");
473 }
474
475 #[test]
476 fn test_depth_limit_exceeded_status_code() {
477 let error = GraphQLError::DepthLimitExceeded;
478 assert_eq!(error.status_code(), 422);
479 assert_eq!(error.error_code(), "GRAPHQL_DEPTH_LIMIT_EXCEEDED");
480 }
481
482 #[test]
483 fn test_complexity_limit_exceeded_response() {
484 let error = GraphQLError::ComplexityLimitExceeded;
485 let response = error.to_graphql_response();
486 assert_eq!(
487 response["errors"][0]["extensions"]["code"],
488 "GRAPHQL_COMPLEXITY_LIMIT_EXCEEDED"
489 );
490 assert_eq!(response["errors"][0]["extensions"]["status"], 422);
491 }
492
493 #[test]
494 fn test_depth_limit_exceeded_response() {
495 let error = GraphQLError::DepthLimitExceeded;
496 let response = error.to_graphql_response();
497 assert_eq!(
498 response["errors"][0]["extensions"]["code"],
499 "GRAPHQL_DEPTH_LIMIT_EXCEEDED"
500 );
501 assert_eq!(response["errors"][0]["extensions"]["status"], 422);
502 }
503
504 #[test]
505 fn test_complexity_limit_exceeded_error_type_uri() {
506 let error = GraphQLError::ComplexityLimitExceeded;
507 assert_eq!(
508 error.error_type_uri(),
509 "https://spikard.dev/errors/complexity-limit-exceeded"
510 );
511 }
512
513 #[test]
514 fn test_depth_limit_exceeded_error_type_uri() {
515 let error = GraphQLError::DepthLimitExceeded;
516 assert_eq!(
517 error.error_type_uri(),
518 "https://spikard.dev/errors/depth-limit-exceeded"
519 );
520 }
521
522 #[test]
523 fn test_all_error_codes_are_static() {
524 let errors = vec![
525 GraphQLError::ParseError(String::new()),
526 GraphQLError::JsonError(String::new()),
527 GraphQLError::ValidationError(String::new()),
528 GraphQLError::ExecutionError(String::new()),
529 GraphQLError::SchemaBuildError(String::new()),
530 GraphQLError::RequestHandlingError(String::new()),
531 GraphQLError::SerializationError(String::new()),
532 GraphQLError::AuthenticationError(String::new()),
533 GraphQLError::AuthorizationError(String::new()),
534 GraphQLError::NotFound(String::new()),
535 GraphQLError::RateLimitExceeded(String::new()),
536 GraphQLError::InvalidInput { message: String::new() },
537 GraphQLError::InternalError(String::new()),
538 ];
539
540 for error in errors {
541 let code = error.error_code();
542 let response = error.to_graphql_response();
543 assert_eq!(response["errors"][0]["extensions"]["code"].as_str(), Some(code));
544 }
545 }
546}