From 8587e4d11e11ce3902d8c2b7103dc6b1dde7a310 Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Sat, 25 May 2019 00:06:35 +0300 Subject: [PATCH] Add `didEncounterErrors` to request pipeline API. (#2719) (As I mentioned at the bottom of https://github.com/apollographql/apollo-server/pull/2714) This adds a new life-cycle event to the new request pipeline API called `didEncounterErrors`, which receives `requestContext`'s **unformatted** `errors`. (The `requestContext` represents an individual request within Apollo Server.) These `errors` give access to potentially different `errors` than those received within `response.errors`, which are sent to the client and available on the `willSendResponse` life-cycle hook, since they are **not** passed through the `formatError` transformation. This can allow plugin implementers to take advantage of the actual errors and properties which may be pruned from the error before transmission to the client. While most other request pipeline life-cycle events provide a guarantee of their arguments, this `didEncounterErrors` will have a little bit less certainty since the errors could have happened in parsing, validation, or execution, and thus different contracts would have been made (e.g. we may not have been able to negotiate a `requestContext.document` if we could not parse the operation). This still needs tests and I suspect I'll have some additional changes prior to this becoming official, but it can ship as-is for now and will live in the Apollo Server 2.6.0 alpha for the time-being. Use with minimal risk, but with the caveat that the final API may change. Also, I'll need to add docs to https://github.com/apollographql/apollo-server/pull/2008. --- .../apollo-server-core/src/requestPipeline.ts | 27 ++++++++++++++----- .../src/requestPipelineAPI.ts | 8 ++++++ .../apollo-server-plugin-base/src/index.ts | 6 +++++ 3 files changed, 34 insertions(+), 7 deletions(-) diff --git a/packages/apollo-server-core/src/requestPipeline.ts b/packages/apollo-server-core/src/requestPipeline.ts index 0249da325d8..1c806cda1dd 100644 --- a/packages/apollo-server-core/src/requestPipeline.ts +++ b/packages/apollo-server-core/src/requestPipeline.ts @@ -236,7 +236,7 @@ export async function processGraphQLRequest( parsingDidEnd(); } catch (syntaxError) { parsingDidEnd(syntaxError); - return sendErrorResponse(syntaxError, SyntaxError); + return await sendErrorResponse(syntaxError, SyntaxError); } const validationDidEnd = await dispatcher.invokeDidStartHook( @@ -253,7 +253,7 @@ export async function processGraphQLRequest( validationDidEnd(); } else { validationDidEnd(validationErrors); - return sendErrorResponse(validationErrors, ValidationError); + return await sendErrorResponse(validationErrors, ValidationError); } if (config.documentStore) { @@ -309,7 +309,7 @@ export async function processGraphQLRequest( if (err instanceof HttpQueryError) { throw err; } - return sendErrorResponse(err); + return await sendErrorResponse(err); } // Now that we've gone through the pre-execution phases of the request @@ -345,7 +345,7 @@ export async function processGraphQLRequest( >); if (result.errors) { - extensionStack.didEncounterErrors(result.errors); + await didEncounterErrors(result.errors); } response = { @@ -356,7 +356,7 @@ export async function processGraphQLRequest( executionDidEnd(); } catch (executionError) { executionDidEnd(executionError); - return sendErrorResponse(executionError); + return await sendErrorResponse(executionError); } } @@ -486,7 +486,20 @@ export async function processGraphQLRequest( return requestContext.response!; } - function sendErrorResponse( + async function didEncounterErrors(errors: ReadonlyArray) { + requestContext.errors = errors; + extensionStack.didEncounterErrors(errors); + + return await dispatcher.invokeHookAsync( + 'didEncounterErrors', + requestContext as WithRequired< + typeof requestContext, + 'metrics' | 'source' | 'errors' + >, + ); + } + + async function sendErrorResponse( errorOrErrors: ReadonlyArray | GraphQLError, errorClass?: typeof ApolloError, ) { @@ -495,7 +508,7 @@ export async function processGraphQLRequest( ? errorOrErrors : [errorOrErrors]; - extensionStack.didEncounterErrors(errors); + await didEncounterErrors(errors); return sendResponse({ errors: formatErrors( diff --git a/packages/apollo-server-core/src/requestPipelineAPI.ts b/packages/apollo-server-core/src/requestPipelineAPI.ts index ac8a2fd0a3f..06a422d302c 100644 --- a/packages/apollo-server-core/src/requestPipelineAPI.ts +++ b/packages/apollo-server-core/src/requestPipelineAPI.ts @@ -75,6 +75,14 @@ export interface GraphQLRequestContext> { readonly operationName?: string | null; readonly operation?: OperationDefinitionNode; + /** + * Unformatted errors which have occurred during the request. Note that these + * are present earlier in the request pipeline and differ from **formatted** + * errors which are the result of running the user-configurable `formatError` + * transformation function over specific errors. + */ + readonly errors?: ReadonlyArray; + readonly metrics?: GraphQLRequestMetrics; debug?: boolean; diff --git a/packages/apollo-server-plugin-base/src/index.ts b/packages/apollo-server-plugin-base/src/index.ts index f26e262eb8c..ecc7ca5ec0f 100644 --- a/packages/apollo-server-plugin-base/src/index.ts +++ b/packages/apollo-server-plugin-base/src/index.ts @@ -42,6 +42,12 @@ export interface GraphQLRequestListener> { 'metrics' | 'source' | 'document' | 'operationName' | 'operation' >, ): ValueOrPromise; + didEncounterErrors?( + requestContext: WithRequired< + GraphQLRequestContext, + 'metrics' | 'source' | 'errors' + >, + ): ValueOrPromise; // If this hook is defined, it is invoked immediately before GraphQL execution // would take place. If its return value resolves to a non-null // GraphQLResponse, that result is used instead of executing the query.