Skip to main content

apollo_router/
error.rs

1//! Router errors.
2use std::ops::Deref;
3use std::sync::Arc;
4
5use apollo_compiler::validation::DiagnosticList;
6use apollo_compiler::validation::WithErrors;
7use apollo_federation::error::FederationError;
8use displaydoc::Display;
9use serde::Deserialize;
10use serde::Serialize;
11use thiserror::Error;
12use tokio::task::JoinError;
13use tower::BoxError;
14
15use crate::apollo_studio_interop::UsageReporting;
16pub(crate) use crate::configuration::ConfigurationError;
17pub(crate) use crate::graphql::Error;
18use crate::graphql::ErrorExtension;
19use crate::graphql::IntoGraphQLErrors;
20use crate::graphql::Location as ErrorLocation;
21use crate::graphql::Response;
22use crate::json_ext::Path;
23use crate::json_ext::Value;
24use crate::spec::SpecError;
25use crate::spec::operation_limits::OperationLimits;
26
27/// Return up to this many GraphQL parsing or validation errors.
28///
29/// Any remaining errors get silently dropped.
30const MAX_VALIDATION_ERRORS: usize = 100;
31
32/// Error types for execution.
33///
34/// Note that these are not actually returned to the client, but are instead converted to JSON for
35/// [`struct@Error`].
36#[derive(Error, Display, Debug, Clone, Serialize, Eq, PartialEq)]
37#[serde(untagged)]
38#[ignore_extra_doc_attributes]
39#[non_exhaustive]
40#[allow(missing_docs)] // FIXME
41pub(crate) enum FetchError {
42    /// {message}
43    ValidationInvalidTypeVariable {
44        name: serde_json_bytes::ByteString,
45        message: crate::spec::InvalidInputValue,
46    },
47
48    /// query could not be planned: {reason}
49    ValidationPlanningError {
50        /// The failure reason.
51        reason: String,
52    },
53
54    /// request was malformed: {reason}
55    MalformedRequest {
56        /// The reason the serialization failed.
57        reason: String,
58    },
59
60    /// response was malformed: {reason}
61    MalformedResponse {
62        /// The reason the serialization failed.
63        reason: String,
64    },
65
66    /// service '{service}' response was malformed: {reason}
67    SubrequestMalformedResponse {
68        /// The service that responded with the malformed response.
69        service: String,
70
71        /// The reason the serialization failed.
72        reason: String,
73    },
74
75    /// service '{service}' returned a PATCH response which was not expected
76    SubrequestUnexpectedPatchResponse {
77        /// The service that returned the PATCH response.
78        service: String,
79    },
80
81    /// HTTP fetch failed from '{service}': {reason}
82    ///
83    /// note that this relates to a transport error and not a GraphQL error
84    SubrequestHttpError {
85        status_code: Option<u16>,
86
87        /// The service failed.
88        service: String,
89
90        /// The reason the fetch failed.
91        reason: String,
92    },
93    /// Websocket fetch failed from '{service}': {reason}
94    ///
95    /// note that this relates to a transport error and not a GraphQL error
96    SubrequestWsError {
97        /// The service failed.
98        service: String,
99
100        /// The reason the fetch failed.
101        reason: String,
102    },
103
104    /// could not find path: {reason}
105    ExecutionPathNotFound { reason: String },
106
107    /// Batching error for '{service}': {reason}
108    SubrequestBatchingError {
109        /// The service for which batch processing failed.
110        service: String,
111
112        /// The reason batch processing failed.
113        reason: String,
114    },
115}
116
117impl FetchError {
118    /// Convert the fetch error to a GraphQL error.
119    pub(crate) fn to_graphql_error(&self, path: Option<Path>) -> Error {
120        // FIXME(SimonSapin): this causes every Rust field to be included in `extensions`,
121        // do we really want that?
122        let mut value: Value = serde_json_bytes::to_value(self).unwrap_or_default();
123        if let Some(extensions) = value.as_object_mut() {
124            extensions
125                .entry("code")
126                .or_insert_with(|| self.extension_code().into());
127            // Following these specs https://www.apollographql.com/docs/apollo-server/data/errors/#including-custom-error-details
128            match self {
129                FetchError::SubrequestHttpError {
130                    service,
131                    status_code,
132                    ..
133                } => {
134                    extensions
135                        .entry("service")
136                        .or_insert_with(|| service.clone().into());
137                    extensions.remove("status_code");
138                    if let Some(status_code) = status_code {
139                        extensions
140                            .insert("http", serde_json_bytes::json!({ "status": status_code }));
141                    }
142                }
143                FetchError::SubrequestMalformedResponse { service, .. }
144                | FetchError::SubrequestUnexpectedPatchResponse { service }
145                | FetchError::SubrequestWsError { service, .. } => {
146                    extensions
147                        .entry("service")
148                        .or_insert_with(|| service.clone().into());
149                }
150                FetchError::ValidationInvalidTypeVariable { name, .. } => {
151                    extensions.remove("message");
152                    extensions
153                        .entry("name")
154                        .or_insert_with(|| Value::String(name.clone()));
155                }
156                _ => (),
157            }
158        }
159
160        Error::builder()
161            .message(self.to_string())
162            .locations(Vec::default())
163            .and_path(path)
164            .extensions(value.as_object().unwrap().to_owned())
165            .build()
166    }
167
168    /// Convert the error to an appropriate response.
169    pub(crate) fn to_response(&self) -> Response {
170        Response {
171            errors: vec![self.to_graphql_error(None)],
172            ..Response::default()
173        }
174    }
175}
176
177impl ErrorExtension for FetchError {
178    fn extension_code(&self) -> String {
179        match self {
180            FetchError::ValidationInvalidTypeVariable { .. } => "VALIDATION_INVALID_TYPE_VARIABLE",
181            FetchError::ValidationPlanningError { .. } => "VALIDATION_PLANNING_ERROR",
182            FetchError::SubrequestMalformedResponse { .. } => "SUBREQUEST_MALFORMED_RESPONSE",
183            FetchError::SubrequestUnexpectedPatchResponse { .. } => {
184                "SUBREQUEST_UNEXPECTED_PATCH_RESPONSE"
185            }
186            FetchError::SubrequestHttpError { .. } => "SUBREQUEST_HTTP_ERROR",
187            FetchError::SubrequestWsError { .. } => "SUBREQUEST_WEBSOCKET_ERROR",
188            FetchError::ExecutionPathNotFound { .. } => "EXECUTION_PATH_NOT_FOUND",
189            FetchError::MalformedRequest { .. } => "MALFORMED_REQUEST",
190            FetchError::MalformedResponse { .. } => "MALFORMED_RESPONSE",
191            FetchError::SubrequestBatchingError { .. } => "SUBREQUEST_BATCHING_ERROR",
192        }
193        .to_string()
194    }
195}
196
197impl From<QueryPlannerError> for FetchError {
198    fn from(err: QueryPlannerError) -> Self {
199        FetchError::ValidationPlanningError {
200            reason: err.to_string(),
201        }
202    }
203}
204
205/// Error types for CacheResolver
206#[derive(Error, Debug, Display, Clone)]
207pub(crate) enum CacheResolverError {
208    /// value retrieval failed: {0}
209    RetrievalError(Arc<QueryPlannerError>),
210    /// {0}
211    Backpressure(crate::compute_job::ComputeBackPressureError),
212    /// batch processing failed: {0}
213    BatchingError(String),
214}
215
216impl IntoGraphQLErrors for CacheResolverError {
217    fn into_graphql_errors(self) -> Result<Vec<Error>, Self> {
218        match self {
219            CacheResolverError::RetrievalError(retrieval_error) => retrieval_error
220                .deref()
221                .clone()
222                .into_graphql_errors()
223                .map_err(|_err| CacheResolverError::RetrievalError(retrieval_error)),
224            CacheResolverError::Backpressure(e) => Ok(vec![e.to_graphql_error()]),
225            CacheResolverError::BatchingError(msg) => Ok(vec![
226                Error::builder()
227                    .message(msg)
228                    .extension_code("BATCH_PROCESSING_FAILED")
229                    .build(),
230            ]),
231        }
232    }
233}
234
235impl From<QueryPlannerError> for CacheResolverError {
236    fn from(qp_err: QueryPlannerError) -> Self {
237        Self::RetrievalError(Arc::new(qp_err))
238    }
239}
240
241/// Error types for service building.
242#[derive(Error, Debug, Display)]
243pub(crate) enum ServiceBuildError {
244    /// failed to initialize the query planner: {0}
245    QpInitError(FederationError),
246
247    /// schema error: {0}
248    Schema(SchemaError),
249
250    /// couldn't build Router service: {0}
251    ServiceError(BoxError),
252}
253
254impl From<SchemaError> for ServiceBuildError {
255    fn from(err: SchemaError) -> Self {
256        ServiceBuildError::Schema(err)
257    }
258}
259
260impl From<BoxError> for ServiceBuildError {
261    fn from(err: BoxError) -> Self {
262        ServiceBuildError::ServiceError(err)
263    }
264}
265
266/// Error types for QueryPlanner
267///
268/// This error may be cached so no temporary errors may be defined here.
269#[derive(Error, Debug, Display, Clone, Serialize, Deserialize)]
270pub(crate) enum QueryPlannerError {
271    /// invalid query: {0}
272    OperationValidationErrors(ValidationErrors),
273
274    /// query planning panicked: {0}
275    JoinError(String),
276
277    /// empty query plan. This behavior is unexpected and we suggest opening an issue to apollographql/router with a reproduction.
278    EmptyPlan(String), // usage_reporting stats_report_key
279
280    /// unhandled planner result
281    UnhandledPlannerResult,
282
283    /// spec error: {0}
284    SpecError(SpecError),
285
286    /// complexity limit exceeded
287    LimitExceeded(OperationLimits<bool>),
288
289    // Safe to cache because user scopes and policies are included in the cache key.
290    /// Unauthorized field or type
291    Unauthorized(Vec<Path>),
292
293    /// Federation error: {0}
294    FederationError(FederationErrorBridge),
295
296    /// Query planning timed out: {0}
297    Timeout(String),
298
299    /// Query planning memory limit exceeded: {0}
300    MemoryLimitExceeded(String),
301}
302
303impl From<FederationErrorBridge> for QueryPlannerError {
304    fn from(value: FederationErrorBridge) -> Self {
305        Self::FederationError(value)
306    }
307}
308
309/// A temporary error type used to extract a few variants from `apollo-federation`'s
310/// `FederationError`. For backwards compatibility, these other variant need to be extracted so
311/// that the correct status code (GRAPHQL_VALIDATION_ERROR) can be added to the response. For
312/// router 2.0, apollo-federation should split its error type into internal and external types.
313/// When this happens, this temp type should be replaced with that type.
314// TODO(@TylerBloom): See the comment above
315#[derive(Error, Debug, Display, Clone, Serialize, Deserialize)]
316pub(crate) enum FederationErrorBridge {
317    /// {0}
318    UnknownOperation(String),
319    /// {0}
320    OperationNameNotProvided(String),
321    /// {0}
322    Other(String),
323    /// {0}
324    Cancellation(String),
325}
326
327impl From<FederationError> for FederationErrorBridge {
328    fn from(value: FederationError) -> Self {
329        match &value {
330            err @ FederationError::SingleFederationError(
331                apollo_federation::error::SingleFederationError::UnknownOperation,
332            ) => Self::UnknownOperation(err.to_string()),
333            err @ FederationError::SingleFederationError(
334                apollo_federation::error::SingleFederationError::OperationNameNotProvided,
335            ) => Self::OperationNameNotProvided(err.to_string()),
336            err @ FederationError::SingleFederationError(
337                apollo_federation::error::SingleFederationError::PlanningCancelled,
338            ) => Self::Cancellation(err.to_string()),
339            err => Self::Other(err.to_string()),
340        }
341    }
342}
343
344impl IntoGraphQLErrors for FederationErrorBridge {
345    fn into_graphql_errors(self) -> Result<Vec<Error>, Self> {
346        match self {
347            FederationErrorBridge::UnknownOperation(msg) => Ok(vec![
348                Error::builder()
349                    .message(msg)
350                    .extension_code("GRAPHQL_VALIDATION_FAILED")
351                    .build(),
352            ]),
353            FederationErrorBridge::OperationNameNotProvided(msg) => Ok(vec![
354                Error::builder()
355                    .message(msg)
356                    .extension_code("GRAPHQL_VALIDATION_FAILED")
357                    .build(),
358            ]),
359            // All other errors will be pushed on and be treated as internal server errors
360            err => Err(err),
361        }
362    }
363}
364
365impl IntoGraphQLErrors for Vec<apollo_compiler::response::GraphQLError> {
366    fn into_graphql_errors(self) -> Result<Vec<Error>, Self> {
367        Ok(self
368            .into_iter()
369            .map(|err| {
370                Error::builder()
371                    .message(err.message)
372                    .locations(
373                        err.locations
374                            .into_iter()
375                            .map(|location| ErrorLocation {
376                                line: location.line as u32,
377                                column: location.column as u32,
378                            })
379                            .collect::<Vec<_>>(),
380                    )
381                    .extension_code("GRAPHQL_VALIDATION_FAILED")
382                    .build()
383            })
384            .take(MAX_VALIDATION_ERRORS)
385            .collect())
386    }
387}
388
389impl IntoGraphQLErrors for QueryPlannerError {
390    fn into_graphql_errors(self) -> Result<Vec<Error>, Self> {
391        match self {
392            QueryPlannerError::SpecError(err) => err
393                .into_graphql_errors()
394                .map_err(QueryPlannerError::SpecError),
395
396            QueryPlannerError::OperationValidationErrors(errs) => errs
397                .into_graphql_errors()
398                .map_err(QueryPlannerError::OperationValidationErrors),
399
400            QueryPlannerError::LimitExceeded(OperationLimits {
401                depth,
402                height,
403                root_fields,
404                aliases,
405            }) => {
406                let mut errors = Vec::new();
407                let mut build = |exceeded, code, message| {
408                    if exceeded {
409                        errors.push(
410                            Error::builder()
411                                .message(message)
412                                .extension_code(code)
413                                .build(),
414                        )
415                    }
416                };
417                build(
418                    depth,
419                    "MAX_DEPTH_LIMIT",
420                    "Maximum depth limit exceeded in this operation",
421                );
422                build(
423                    height,
424                    "MAX_HEIGHT_LIMIT",
425                    "Maximum height (field count) limit exceeded in this operation",
426                );
427                build(
428                    root_fields,
429                    "MAX_ROOT_FIELDS_LIMIT",
430                    "Maximum root fields limit exceeded in this operation",
431                );
432                build(
433                    aliases,
434                    "MAX_ALIASES_LIMIT",
435                    "Maximum aliases limit exceeded in this operation",
436                );
437                Ok(errors)
438            }
439            QueryPlannerError::FederationError(err) => err
440                .into_graphql_errors()
441                .map_err(QueryPlannerError::FederationError),
442            err => Err(err),
443        }
444    }
445}
446
447impl QueryPlannerError {
448    pub(crate) fn usage_reporting(&self) -> Option<UsageReporting> {
449        match self {
450            QueryPlannerError::SpecError(e) => {
451                Some(UsageReporting::Error(e.get_error_key().to_string()))
452            }
453            _ => None,
454        }
455    }
456}
457
458impl From<JoinError> for QueryPlannerError {
459    fn from(err: JoinError) -> Self {
460        QueryPlannerError::JoinError(err.to_string())
461    }
462}
463
464impl From<SpecError> for QueryPlannerError {
465    fn from(err: SpecError) -> Self {
466        match err {
467            SpecError::ValidationError(errors) => {
468                QueryPlannerError::OperationValidationErrors(errors)
469            }
470            _ => QueryPlannerError::SpecError(err),
471        }
472    }
473}
474
475impl From<ValidationErrors> for QueryPlannerError {
476    fn from(err: ValidationErrors) -> Self {
477        QueryPlannerError::OperationValidationErrors(ValidationErrors { errors: err.errors })
478    }
479}
480impl From<OperationLimits<bool>> for QueryPlannerError {
481    fn from(error: OperationLimits<bool>) -> Self {
482        QueryPlannerError::LimitExceeded(error)
483    }
484}
485
486impl From<QueryPlannerError> for Response {
487    fn from(err: QueryPlannerError) -> Self {
488        FetchError::from(err).to_response()
489    }
490}
491
492/// Error in the schema.
493#[derive(Debug, Error, Display, derive_more::From)]
494#[non_exhaustive]
495pub(crate) enum SchemaError {
496    /// URL parse error for subgraph {0}: {1}
497    UrlParse(String, http::uri::InvalidUri),
498    /// Could not find an URL for subgraph {0}
499    #[from(ignore)]
500    MissingSubgraphUrl(String),
501    /// GraphQL parser error: {0}
502    Parse(ParseErrors),
503    /// GraphQL validation error: {0}
504    Validate(ValidationErrors),
505    /// Federation error: {0}
506    FederationError(FederationError),
507    /// Api error(s): {0}
508    #[from(ignore)]
509    Api(String),
510
511    /// Connector error(s): {0}
512    #[from(ignore)]
513    Connector(FederationError),
514}
515
516/// Collection of schema validation errors.
517#[derive(Debug)]
518pub(crate) struct ParseErrors {
519    pub(crate) errors: DiagnosticList,
520}
521
522impl std::fmt::Display for ParseErrors {
523    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
524        let mut errors = self.errors.iter();
525        for (i, error) in errors.by_ref().take(5).enumerate() {
526            if i > 0 {
527                f.write_str("\n")?;
528            }
529            write!(f, "{error}")?;
530        }
531        let remaining = errors.count();
532        if remaining > 0 {
533            write!(f, "\n...and {remaining} other errors")?;
534        }
535        Ok(())
536    }
537}
538
539impl IntoGraphQLErrors for ParseErrors {
540    fn into_graphql_errors(self) -> Result<Vec<Error>, Self> {
541        Ok(self
542            .errors
543            .iter()
544            .map(|diagnostic| {
545                Error::builder()
546                    .message(diagnostic.error.to_string())
547                    .locations(
548                        diagnostic
549                            .line_column_range()
550                            .map(|location| {
551                                vec![ErrorLocation {
552                                    line: location.start.line as u32,
553                                    column: location.start.column as u32,
554                                }]
555                            })
556                            .unwrap_or_default(),
557                    )
558                    .extension_code("GRAPHQL_PARSING_FAILED")
559                    .build()
560            })
561            .take(MAX_VALIDATION_ERRORS)
562            .collect())
563    }
564}
565
566/// Collection of schema validation errors.
567#[derive(Debug, Clone, Serialize, Deserialize)]
568pub(crate) struct ValidationErrors {
569    pub(crate) errors: Vec<apollo_compiler::response::GraphQLError>,
570}
571
572impl ValidationErrors {
573    pub(crate) fn into_graphql_errors_infallible(self) -> Vec<Error> {
574        self.errors
575            .iter()
576            .map(|diagnostic| {
577                Error::builder()
578                    .message(diagnostic.message.to_string())
579                    .locations(
580                        diagnostic
581                            .locations
582                            .iter()
583                            .map(|loc| ErrorLocation {
584                                line: loc.line as u32,
585                                column: loc.column as u32,
586                            })
587                            .collect(),
588                    )
589                    .extension_code("GRAPHQL_VALIDATION_FAILED")
590                    .build()
591            })
592            .take(MAX_VALIDATION_ERRORS)
593            .collect()
594    }
595}
596impl IntoGraphQLErrors for ValidationErrors {
597    fn into_graphql_errors(self) -> Result<Vec<Error>, Self> {
598        Ok(self.into_graphql_errors_infallible())
599    }
600}
601
602impl From<DiagnosticList> for ValidationErrors {
603    fn from(errors: DiagnosticList) -> Self {
604        Self {
605            errors: errors
606                .iter()
607                .map(|e| e.unstable_to_json_compat())
608                .take(MAX_VALIDATION_ERRORS)
609                .collect(),
610        }
611    }
612}
613
614impl<T> From<WithErrors<T>> for ValidationErrors {
615    fn from(WithErrors { errors, .. }: WithErrors<T>) -> Self {
616        errors.into()
617    }
618}
619
620impl std::fmt::Display for ValidationErrors {
621    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
622        for (index, error) in self.errors.iter().enumerate() {
623            if index > 0 {
624                f.write_str("\n")?;
625            }
626            if let Some(location) = error.locations.first() {
627                write!(
628                    f,
629                    "[{}:{}] {}",
630                    location.line, location.column, error.message
631                )?;
632            } else {
633                write!(f, "{}", error.message)?;
634            }
635        }
636        Ok(())
637    }
638}
639
640/// Error during subgraph batch processing
641#[derive(Debug, Error, Display)]
642pub(crate) enum SubgraphBatchingError {
643    /// Sender unavailable
644    SenderUnavailable,
645    /// Requests is empty
646    RequestsIsEmpty,
647    /// Batch processing failed: {0}
648    ProcessingFailed(String),
649}
650
651#[cfg(test)]
652mod tests {
653    use super::*;
654    use crate::assert_error_eq_ignoring_id;
655    use crate::graphql;
656
657    #[test]
658    fn test_into_graphql_error() {
659        let error = FetchError::SubrequestHttpError {
660            status_code: Some(400),
661            service: String::from("my_service"),
662            reason: String::from("invalid request"),
663        };
664        let expected_gql_error = graphql::Error::builder()
665            .message("HTTP fetch failed from 'my_service': invalid request")
666            .extension_code("SUBREQUEST_HTTP_ERROR")
667            .extension("reason", Value::String("invalid request".into()))
668            .extension("service", Value::String("my_service".into()))
669            .extension(
670                "http",
671                serde_json_bytes::json!({"status": Value::Number(400.into())}),
672            )
673            .build();
674
675        assert_error_eq_ignoring_id!(expected_gql_error, error.to_graphql_error(None));
676    }
677}