diff --git a/federation-2/router-bridge/js-src/plan.ts b/federation-2/router-bridge/js-src/plan.ts index 6fb69b8a5..3a042d206 100644 --- a/federation-2/router-bridge/js-src/plan.ts +++ b/federation-2/router-bridge/js-src/plan.ts @@ -7,6 +7,7 @@ import { import { DocumentNode, ExecutionResult, + GraphQLError, GraphQLSchema, parse, validate, @@ -25,7 +26,9 @@ import { import { ReferencedFieldsForType } from "apollo-reporting-protobuf"; const PARSE_FAILURE: string = "## GraphQLParseFailure\n"; +const PARSE_FAILURE_EXT_CODE: string = "GRAPHQL_PARSE_FAILED"; const VALIDATION_FAILURE: string = "## GraphQLValidationFailure\n"; +const VALIDATION_FAILURE_EXT_CODE: string = "GRAPHQL_VALIDATION_FAILED"; const UNKNOWN_OPERATION: string = "## GraphQLUnknownOperationName\n"; export type ReferencedFieldsByType = Record; @@ -77,7 +80,14 @@ export class BridgeQueryPlanner { statsReportKey: PARSE_FAILURE, referencedFieldsByType: {}, }, - errors: [parseError], + errors: [ + { + ...parseError, + extensions: { + code: PARSE_FAILURE_EXT_CODE, + }, + }, + ], }; } @@ -90,7 +100,25 @@ export class BridgeQueryPlanner { statsReportKey: VALIDATION_FAILURE, referencedFieldsByType: {}, }, - errors: validationErrors, + errors: validationErrors.map((error): GraphQLError => { + if ( + error.extensions == null || + Object.keys(error.extensions).length === 0 + ) { + return new GraphQLError(error.message, { + extensions: { + code: VALIDATION_FAILURE_EXT_CODE, + }, + path: error.path, + nodes: error.nodes, + originalError: error.originalError, + positions: error.positions, + source: error.source, + }); + } + + return error; + }), }; } @@ -116,7 +144,14 @@ export class BridgeQueryPlanner { statsReportKey, referencedFieldsByType: {}, }, - errors: [e], + errors: [ + { + ...e, + extensions: { + code: VALIDATION_FAILURE_EXT_CODE, + }, + }, + ], }; } diff --git a/federation-2/router-bridge/js-src/plan_worker.ts b/federation-2/router-bridge/js-src/plan_worker.ts index 9427779b6..951a0d53e 100644 --- a/federation-2/router-bridge/js-src/plan_worker.ts +++ b/federation-2/router-bridge/js-src/plan_worker.ts @@ -1,6 +1,6 @@ import { GraphQLErrorExt } from "@apollo/core-schema/dist/error"; import { QueryPlannerConfig } from "@apollo/query-planner"; -import { ASTNode, Source, SourceLocation } from "graphql"; +import { ASTNode, GraphQLError, Source, SourceLocation } from "graphql"; import { BridgeQueryPlanner, ExecutionResultWithUsageReporting, @@ -191,10 +191,21 @@ async function run() { } } catch (e) { logger.warn(`an error happened in the worker runtime ${e}\n`); + + const unexpectedError = new GraphQLError(e.message, { + extensions: { + code: "QUERY_PLANNING_FAILED", + exception: { + stacktrace: e.toString().split(/\n/), + }, + }, + }); + unexpectedError.name = e.name || "unknown"; + await send({ id, payload: { - errors: [e], + errors: [unexpectedError], usageReporting: { statsReportKey: "", referencedFieldsByType: {}, @@ -203,10 +214,21 @@ async function run() { }); } } catch (e) { - logger.warn(`plan_worker: an unknown error occured ${e}\n`); + logger.warn(`plan_worker: an unknown error occurred ${e}\n`); + + const unexpectedError = new GraphQLError(e.message, { + extensions: { + code: "QUERY_PLANNING_FAILED", + exception: { + stacktrace: e.toString().split(/\n/), + }, + }, + }); + unexpectedError.name = e.name || "unknown"; + await send({ payload: { - errors: [e], + errors: [unexpectedError], usageReporting: { statsReportKey: "", referencedFieldsByType: {}, diff --git a/federation-2/router-bridge/package.json b/federation-2/router-bridge/package.json index 74839a2b2..8fd311a85 100644 --- a/federation-2/router-bridge/package.json +++ b/federation-2/router-bridge/package.json @@ -53,4 +53,4 @@ "node": "16.13.2", "npm": "8.3.1" } -} +} \ No newline at end of file diff --git a/federation-2/router-bridge/src/planner.rs b/federation-2/router-bridge/src/planner.rs index d4eb89f4b..b19123d4f 100644 --- a/federation-2/router-bridge/src/planner.rs +++ b/federation-2/router-bridge/src/planner.rs @@ -2,14 +2,20 @@ * Instantiate a QueryPlanner from a schema, and perform query planning */ -use crate::worker::JsWorker; -use serde::{de::DeserializeOwned, Deserialize, Serialize}; use std::collections::HashMap; -use std::fmt::{Debug, Display, Formatter}; +use std::fmt::Debug; +use std::fmt::Display; +use std::fmt::Formatter; use std::marker::PhantomData; use std::sync::Arc; + +use serde::de::DeserializeOwned; +use serde::Deserialize; +use serde::Serialize; use thiserror::Error; +use crate::worker::JsWorker; + // ------------------------------------ #[derive(Debug, Serialize)] @@ -50,7 +56,7 @@ pub struct OperationalContext { /// /// [`graphql-js`]: https://npm.im/graphql /// [`GraphQLError`]: https://github.com/graphql/graphql-js/blob/3869211/src/error/GraphQLError.js#L18-L75 -#[derive(Debug, Error, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Error, Serialize, Deserialize, PartialEq, Eq, Clone)] pub struct PlanError { /// A human-readable description of the error that prevented planning. pub message: Option, @@ -108,11 +114,21 @@ impl Display for PlanError { } } -#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] /// Error codes pub struct PlanErrorExtensions { /// The error code pub code: String, + #[serde(skip_serializing_if = "Option::is_none")] + /// The stacktrace if we have one + pub exception: Option, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +/// stacktrace in error extensions +pub struct ExtensionsException { + /// The stacktrace generated in JavaScript + pub stacktrace: String, } /// An error that was received during planning within JavaScript. @@ -137,7 +153,7 @@ pub struct BridgeSetupResult { pub errors: Option>, } -#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] /// The error location pub struct Location { /// The line number @@ -146,7 +162,7 @@ pub struct Location { pub column: u32, } -#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] #[serde(untagged)] /// This contains the set of all errors that can be thrown from deno pub enum PlannerError { @@ -183,7 +199,7 @@ impl std::fmt::Display for PlannerError { /// WorkerError represents the non GraphQLErrors the deno worker can throw. /// We try to get as much data out of them. -#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] pub struct WorkerError { /// The error message pub message: Option, @@ -215,7 +231,7 @@ impl std::fmt::Display for WorkerError { /// We try to get as much data out of them. /// While they mostly represent GraphQLErrors, they sometimes don't. /// See [`WorkerError`] -#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] #[serde(rename_all = "camelCase")] pub struct WorkerGraphQLError { /// The error kind @@ -516,8 +532,10 @@ pub struct IncrementalDeliverySupport { #[cfg(test)] mod tests { + use futures::stream::StreamExt; + use futures::stream::{self}; + use super::*; - use futures::stream::{self, StreamExt}; const QUERY: &str = include_str!("testdata/query.graphql"); const QUERY2: &str = include_str!("testdata/query2.graphql"); @@ -633,7 +651,7 @@ mod tests { assert_eq!( "Syntax Error: Unexpected Name \"this\".", - payload.errors[0].message.as_ref().clone().unwrap() + payload.errors[0].message.as_ref().unwrap() ); assert_eq!( "## GraphQLParseFailure\n", @@ -671,7 +689,7 @@ mod tests { assert_eq!( "Cannot spread fragment \"thatUserFragment1\" within itself via \"thatUserFragment2\".", - payload.errors[0].message.as_ref().clone().unwrap() + payload.errors[0].message.as_ref().unwrap() ); assert_eq!( "## GraphQLValidationFailure\n", @@ -698,7 +716,7 @@ mod tests { assert_eq!( "Unknown operation named \"ThisOperationNameDoesntExist\"", - payload.errors[0].message.as_ref().clone().unwrap() + payload.errors[0].message.as_ref().unwrap() ); assert_eq!( "## GraphQLUnknownOperationName\n", @@ -722,7 +740,7 @@ mod tests { assert_eq!( "Must provide operation name if query contains multiple operations.", - payload.errors[0].message.as_ref().clone().unwrap() + payload.errors[0].message.as_ref().unwrap() ); assert_eq!( "## GraphQLUnknownOperationName\n", @@ -746,7 +764,7 @@ mod tests { assert_eq!( "This anonymous operation must be the only defined operation.", - payload.errors[0].message.as_ref().clone().unwrap() + payload.errors[0].message.as_ref().unwrap() ); assert_eq!( "## GraphQLValidationFailure\n", @@ -770,7 +788,7 @@ mod tests { assert_eq!( "Fragment \"thatUserFragment1\" is never used.", - payload.errors[0].message.as_ref().clone().unwrap() + payload.errors[0].message.as_ref().unwrap() ); assert_eq!( "## GraphQLValidationFailure\n", @@ -868,6 +886,7 @@ mod tests { ), extensions: Some(PlanErrorExtensions { code: "INVALID_GRAPHQL".to_string(), + exception: None, }), }]; @@ -1039,6 +1058,7 @@ GraphQL request:4:1 locations: Default::default(), extensions: Some(PlanErrorExtensions { code: "CheckFailed".to_string(), + exception: None }), original_error: None, causes: vec![ @@ -1046,14 +1066,14 @@ GraphQL request:4:1 message: Some("the `for:` argument is unsupported by version v0.1 of the core spec. Please upgrade to at least @core v0.2 (https://specs.apollo.dev/core/v0.2).".to_string()), name: None, stack: None, - extensions: Some(PlanErrorExtensions { code: "UNSUPPORTED_LINKED_FEATURE".to_string() }), + extensions: Some(PlanErrorExtensions { code: "UNSUPPORTED_LINKED_FEATURE".to_string(), exception: None }), locations: vec![Location { line: 2, column: 1 }, Location { line: 3, column: 1 }, Location { line: 4, column: 1 }] }), Box::new(WorkerError { message: Some("feature https://specs.apollo.dev/something-unsupported/v0.1 is for: SECURITY but is unsupported".to_string()), name: None, stack: None, - extensions: Some(PlanErrorExtensions { code: "UNSUPPORTED_LINKED_FEATURE".to_string() }), + extensions: Some(PlanErrorExtensions { code: "UNSUPPORTED_LINKED_FEATURE".to_string(), exception: None }), locations: vec![Location { line: 4, column: 1 }] }) ], @@ -1097,6 +1117,7 @@ GraphQL request:4:9 locations: Default::default(), extensions: Some(PlanErrorExtensions { code: "CheckFailed".to_string(), + exception: None }), original_error: None, causes: vec![ @@ -1104,7 +1125,7 @@ GraphQL request:4:9 message: Some("feature https://specs.apollo.dev/unsupported-feature/v0.1 is for: EXECUTION but is unsupported".to_string()), name: None, stack: None, - extensions: Some(PlanErrorExtensions { code: "UNSUPPORTED_LINKED_FEATURE".to_string() }), + extensions: Some(PlanErrorExtensions { code: "UNSUPPORTED_LINKED_FEATURE".to_string(), exception: None }), locations: vec![Location { line: 4, column: 9 }] }), ], @@ -1135,12 +1156,14 @@ GraphQL request:4:9 locations: vec![], extensions: Some(PlanErrorExtensions { code: "CheckFailed".to_string(), + exception: None }), original_error: None, causes: vec![Box::new(WorkerError { message: Some("feature https://specs.apollo.dev/unsupported-feature/v0.1 is for: SECURITY but is unsupported".to_string()), extensions: Some(PlanErrorExtensions { code: "UNSUPPORTED_LINKED_FEATURE".to_string(), + exception: None }), name: None, stack: None, @@ -1163,7 +1186,10 @@ GraphQL request:4:9 mod planning_error { use std::collections::HashMap; - use crate::planner::{PlanError, PlanErrorExtensions, ReferencedFieldsForType, UsageReporting}; + use crate::planner::PlanError; + use crate::planner::PlanErrorExtensions; + use crate::planner::ReferencedFieldsForType; + use crate::planner::UsageReporting; #[test] #[should_panic( @@ -1196,6 +1222,7 @@ mod planning_error { message: Some("something terrible happened".to_string()), extensions: Some(PlanErrorExtensions { code: "E_TEST_CASE".to_string(), + exception: None, }), }; @@ -1270,6 +1297,7 @@ feature https://specs.apollo.dev/something-unsupported/v0.1 is for: SECURITY but locations: Default::default(), extensions: Some(PlanErrorExtensions { code: "CheckFailed".to_string(), + exception: None }), original_error: None, causes: vec![ @@ -1277,14 +1305,14 @@ feature https://specs.apollo.dev/something-unsupported/v0.1 is for: SECURITY but message: Some("the `for:` argument is unsupported by version v0.1 of the core spec. Please upgrade to at least @core v0.2 (https://specs.apollo.dev/core/v0.2).".to_string()), name: None, stack: None, - extensions: Some(PlanErrorExtensions { code: "ForUnsupported".to_string() }), + extensions: Some(PlanErrorExtensions { code: "ForUnsupported".to_string(), exception: None }), locations: vec![Location { line: 2, column: 1 }, Location { line: 3, column: 1 }, Location { line: 4, column: 1 }] }), Box::new(WorkerError { message: Some("feature https://specs.apollo.dev/something-unsupported/v0.1 is for: SECURITY but is unsupported".to_string()), name: None, stack: None, - extensions: Some(PlanErrorExtensions { code: "UnsupportedFeature".to_string() }), + extensions: Some(PlanErrorExtensions { code: "UnsupportedFeature".to_string(), exception: None }), locations: vec![Location { line: 4, column: 1 }] }) ], @@ -1305,6 +1333,7 @@ feature https://specs.apollo.dev/unsupported-feature/v0.1 is for: EXECUTION but locations: Default::default(), extensions: Some(PlanErrorExtensions { code: "CheckFailed".to_string(), + exception: None }), original_error: None, causes: vec![ @@ -1312,7 +1341,7 @@ feature https://specs.apollo.dev/unsupported-feature/v0.1 is for: EXECUTION but message: Some("feature https://specs.apollo.dev/unsupported-feature/v0.1 is for: EXECUTION but is unsupported".to_string()), name: None, stack: None, - extensions: Some(PlanErrorExtensions { code: "UnsupportedFeature".to_string() }), + extensions: Some(PlanErrorExtensions { code: "UnsupportedFeature".to_string(), exception: None }), locations: vec![Location { line: 4, column: 9 }] }), ], @@ -1333,12 +1362,14 @@ feature https://specs.apollo.dev/unsupported-feature/v0.1 is for: SECURITY but i locations: vec![], extensions: Some(PlanErrorExtensions { code: "CheckFailed".to_string(), + exception: None }), original_error: None, causes: vec![Box::new(WorkerError { message: Some("feature https://specs.apollo.dev/unsupported-feature/v0.1 is for: SECURITY but is unsupported".to_string()), extensions: Some(PlanErrorExtensions { code: "UnsupportedFeature".to_string(), + exception: None }), name: None, stack: None,