Skip to content

Commit 1790162

Browse files
authored
refactor(error-handling): add context to PlanExecutionError (#513)
This commit adds context to the `PlanExecutionError` enum. It includes the subgraph name and the affected path in case of a projection or header propagation failure. It also adds a `ResultExt` trait to map `ProjectionError` and `HeaderRuleRuntimeError` to `PlanExecutionError`. I will improve names soon, it's a draft anyway, I just wanted to showcase how we could do errors, instead of manually creating GraphQL Error with code and stuff in extensions.
1 parent c064cc5 commit 1790162

File tree

4 files changed

+208
-80
lines changed

4 files changed

+208
-80
lines changed

Cargo.lock

Lines changed: 24 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/executor/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ dashmap = { workspace = true }
3232
ahash = "0.8.12"
3333
regex-automata = "0.4.10"
3434
vrl = { version = "0.27.0", features = ["compiler", "parser", "value", "diagnostic", "stdlib", "core"] }
35+
strum = { version = "0.27.2", features = ["derive"] }
3536

3637
ntex-http = "0.1.15"
3738
hyper-tls = { version = "0.6.0", features = ["vendored"] }
Lines changed: 130 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,136 @@
1-
use crate::{headers::errors::HeaderRuleRuntimeError, projection::error::ProjectionError};
1+
use strum::IntoStaticStr;
22

3-
#[derive(thiserror::Error, Debug, Clone)]
4-
pub enum PlanExecutionError {
3+
use crate::{
4+
headers::errors::HeaderRuleRuntimeError,
5+
projection::error::ProjectionError,
6+
response::graphql_error::{GraphQLError, GraphQLErrorExtensions},
7+
};
8+
9+
#[derive(thiserror::Error, Debug, Clone, IntoStaticStr)]
10+
pub enum PlanExecutionErrorKind {
511
#[error("Projection faiure: {0}")]
12+
#[strum(serialize = "PROJECTION_FAILURE")]
613
ProjectionFailure(#[from] ProjectionError),
14+
715
#[error(transparent)]
16+
#[strum(serialize = "HEADER_PROPAGATION_FAILURE")]
817
HeaderPropagation(#[from] HeaderRuleRuntimeError),
918
}
19+
20+
/// The central error type for all query plan execution failures.
21+
///
22+
/// This struct combines a specific `PlanExecutionErrorKind` with a
23+
/// `PlanExecutionErrorContext` that holds shared, dynamic information
24+
/// like the subgraph name or affected GraphQL path.
25+
#[derive(thiserror::Error, Debug, Clone)]
26+
#[error("{kind}")]
27+
pub struct PlanExecutionError {
28+
#[source]
29+
kind: PlanExecutionErrorKind,
30+
context: PlanExecutionErrorContext,
31+
}
32+
33+
#[derive(Debug, Clone)]
34+
pub struct PlanExecutionErrorContext {
35+
subgraph_name: Option<String>,
36+
affected_path: Option<String>,
37+
}
38+
39+
pub struct LazyPlanContext<SN, AP> {
40+
pub subgraph_name: SN,
41+
pub affected_path: AP,
42+
}
43+
44+
impl PlanExecutionError {
45+
pub(crate) fn new<SN, AP>(
46+
kind: PlanExecutionErrorKind,
47+
lazy_context: LazyPlanContext<SN, AP>,
48+
) -> Self
49+
where
50+
SN: FnOnce() -> Option<String>,
51+
AP: FnOnce() -> Option<String>,
52+
{
53+
Self {
54+
kind,
55+
context: PlanExecutionErrorContext {
56+
subgraph_name: (lazy_context.subgraph_name)(),
57+
affected_path: (lazy_context.affected_path)(),
58+
},
59+
}
60+
}
61+
62+
pub fn error_code(&self) -> &'static str {
63+
(&self.kind).into()
64+
}
65+
66+
pub fn subgraph_name(&self) -> &Option<String> {
67+
&self.context.subgraph_name
68+
}
69+
70+
pub fn affected_path(&self) -> &Option<String> {
71+
&self.context.affected_path
72+
}
73+
}
74+
75+
impl From<PlanExecutionError> for GraphQLError {
76+
fn from(val: PlanExecutionError) -> Self {
77+
let message = val.to_string();
78+
GraphQLError {
79+
extensions: GraphQLErrorExtensions {
80+
code: Some(val.error_code().into()),
81+
service_name: val.context.subgraph_name,
82+
affected_path: val.context.affected_path,
83+
extensions: Default::default(),
84+
},
85+
message,
86+
locations: None,
87+
path: None,
88+
}
89+
}
90+
}
91+
92+
/// An extension trait for `Result` types that can be converted into a `PlanExecutionError`.
93+
///
94+
/// This trait provides a lazy, performant way to add contextual information to
95+
/// an error, only performing work (like cloning strings) if the `Result` is an `Err`.
96+
pub trait IntoPlanExecutionError<T> {
97+
fn with_plan_context<SN, AP>(
98+
self,
99+
context: LazyPlanContext<SN, AP>,
100+
) -> Result<T, PlanExecutionError>
101+
where
102+
SN: FnOnce() -> Option<String>,
103+
AP: FnOnce() -> Option<String>;
104+
}
105+
106+
impl<T> IntoPlanExecutionError<T> for Result<T, ProjectionError> {
107+
fn with_plan_context<SN, AP>(
108+
self,
109+
context: LazyPlanContext<SN, AP>,
110+
) -> Result<T, PlanExecutionError>
111+
where
112+
SN: FnOnce() -> Option<String>,
113+
AP: FnOnce() -> Option<String>,
114+
{
115+
self.map_err(|source| {
116+
let kind = PlanExecutionErrorKind::ProjectionFailure(source);
117+
PlanExecutionError::new(kind, context)
118+
})
119+
}
120+
}
121+
122+
impl<T> IntoPlanExecutionError<T> for Result<T, HeaderRuleRuntimeError> {
123+
fn with_plan_context<SN, AP>(
124+
self,
125+
context: LazyPlanContext<SN, AP>,
126+
) -> Result<T, PlanExecutionError>
127+
where
128+
SN: FnOnce() -> Option<String>,
129+
AP: FnOnce() -> Option<String>,
130+
{
131+
self.map_err(|source| {
132+
let kind = PlanExecutionErrorKind::HeaderPropagation(source);
133+
PlanExecutionError::new(kind, context)
134+
})
135+
}
136+
}

0 commit comments

Comments
 (0)