Skip to main content

hive_router_plan_executor/execution/
error.rs

1use strum::IntoStaticStr;
2
3use crate::{
4    executors::error::SubgraphExecutorError, headers::errors::HeaderRuleRuntimeError,
5    projection::error::ProjectionError, response::graphql_error::GraphQLError,
6};
7
8#[derive(thiserror::Error, Debug, Clone, IntoStaticStr)]
9pub enum PlanExecutionErrorKind {
10    #[error("Projection failure: {0}")]
11    #[strum(serialize = "PROJECTION_FAILURE")]
12    ProjectionFailure(#[from] ProjectionError),
13
14    #[error(transparent)]
15    #[strum(serialize = "HEADER_PROPAGATION_FAILURE")]
16    HeaderPropagation(#[from] HeaderRuleRuntimeError),
17
18    #[error(transparent)]
19    #[strum(serialize = "SUBGRAPH_EXECUTION_FAILURE")]
20    SubgraphExecutor(#[from] SubgraphExecutorError),
21}
22
23/// The central error type for all query plan execution failures.
24///
25/// This struct combines a specific `PlanExecutionErrorKind` with a
26/// `PlanExecutionErrorContext` that holds shared, dynamic information
27/// like the subgraph name or affected GraphQL path.
28#[derive(thiserror::Error, Debug, Clone)]
29#[error("{kind}")]
30pub struct PlanExecutionError {
31    #[source]
32    kind: PlanExecutionErrorKind,
33    context: PlanExecutionErrorContext,
34}
35
36#[derive(Debug, Clone)]
37pub struct PlanExecutionErrorContext {
38    subgraph_name: Option<String>,
39    affected_path: Option<String>,
40}
41
42pub struct LazyPlanContext<SN, AP> {
43    pub subgraph_name: SN,
44    pub affected_path: AP,
45}
46
47impl PlanExecutionError {
48    pub(crate) fn new<SN, AP>(
49        kind: PlanExecutionErrorKind,
50        lazy_context: LazyPlanContext<SN, AP>,
51    ) -> Self
52    where
53        SN: FnOnce() -> Option<String>,
54        AP: FnOnce() -> Option<String>,
55    {
56        Self {
57            kind,
58            context: PlanExecutionErrorContext {
59                subgraph_name: (lazy_context.subgraph_name)(),
60                affected_path: (lazy_context.affected_path)(),
61            },
62        }
63    }
64
65    pub fn error_code(&self) -> &'static str {
66        if let PlanExecutionErrorKind::SubgraphExecutor(subgraph_error) = &self.kind {
67            return subgraph_error.error_code();
68        }
69        (&self.kind).into()
70    }
71
72    pub fn subgraph_name(&self) -> &Option<String> {
73        &self.context.subgraph_name
74    }
75
76    pub fn affected_path(&self) -> &Option<String> {
77        &self.context.affected_path
78    }
79}
80
81// This is needed for individual fetch node error handling
82// Individual fetch node errors are not propagated as PipelineError
83// but converted directly to GraphQLError
84// and added to `errors` field in GraphQL response
85// So failing plan nodes do not fail the whole operation
86// See `error_handling_e2e_tests` for reproduction
87impl From<PlanExecutionError> for GraphQLError {
88    fn from(val: PlanExecutionError) -> Self {
89        let mut error = GraphQLError::from_message_and_code(val.to_string(), val.error_code());
90
91        // We destructure the context to take ownership of the Option<String> values.
92        // Then we move owned Strings directly into builder methods.
93        // This way we avoid cloning through Into<String> in those methods.
94
95        if let Some(subgraph_name) = val.context.subgraph_name {
96            error = error.add_subgraph_name(subgraph_name);
97        }
98        if let Some(affected_path) = val.context.affected_path {
99            error = error.add_affected_path(affected_path);
100        }
101        error
102    }
103}
104
105/// An extension trait for `Result` types that can be converted into a `PlanExecutionError`.
106///
107/// This trait provides a lazy, performant way to add contextual information to
108/// an error, only performing work (like cloning strings) if the `Result` is an `Err`.
109pub trait IntoPlanExecutionError<T> {
110    fn with_plan_context<SN, AP>(
111        self,
112        context: LazyPlanContext<SN, AP>,
113    ) -> Result<T, PlanExecutionError>
114    where
115        SN: FnOnce() -> Option<String>,
116        AP: FnOnce() -> Option<String>;
117}
118
119impl<T> IntoPlanExecutionError<T> for Result<T, ProjectionError> {
120    fn with_plan_context<SN, AP>(
121        self,
122        context: LazyPlanContext<SN, AP>,
123    ) -> Result<T, PlanExecutionError>
124    where
125        SN: FnOnce() -> Option<String>,
126        AP: FnOnce() -> Option<String>,
127    {
128        self.map_err(|source| {
129            let kind = PlanExecutionErrorKind::ProjectionFailure(source);
130            PlanExecutionError::new(kind, context)
131        })
132    }
133}
134
135impl<T> IntoPlanExecutionError<T> for Result<T, HeaderRuleRuntimeError> {
136    fn with_plan_context<SN, AP>(
137        self,
138        context: LazyPlanContext<SN, AP>,
139    ) -> Result<T, PlanExecutionError>
140    where
141        SN: FnOnce() -> Option<String>,
142        AP: FnOnce() -> Option<String>,
143    {
144        self.map_err(|source| {
145            let kind = PlanExecutionErrorKind::HeaderPropagation(source);
146            PlanExecutionError::new(kind, context)
147        })
148    }
149}
150
151impl<T> IntoPlanExecutionError<T> for Result<T, SubgraphExecutorError> {
152    fn with_plan_context<SN, AP>(
153        self,
154        context: LazyPlanContext<SN, AP>,
155    ) -> Result<T, PlanExecutionError>
156    where
157        SN: FnOnce() -> Option<String>,
158        AP: FnOnce() -> Option<String>,
159    {
160        self.map_err(|source| {
161            let kind = PlanExecutionErrorKind::SubgraphExecutor(source);
162            PlanExecutionError::new(kind, context)
163        })
164    }
165}