Apollo-server: How to correctly propagate errors from plugins?

Created on 20 Dec 2019  路  7Comments  路  Source: apollographql/apollo-server

I'm writing an Apollo Server plugin that validates a presence of a certain HTTP header. It looks like this:

import { ApolloServerPlugin } from 'apollo-server-plugin-base';
import { OperationDefinitionNode } from 'graphql';

export const myPlugin: ApolloServerPlugin = {
  requestDidStart() {
    return {
      didResolveOperation(context) {
        if (!headerIsValid(context.request.http!.headers.get('x-special-header'))) {
          throw new Error('header not valid');
        }
      },
    };
  },
};

I'm doing that in a plugin because I need access to a parsed query so that I can distinguish introspection queries and normal queries, and I didn't find a better place to do it, see here.

Previously, I had this logic in the context: () => {...} function inside new ApolloServer constructor and when I threw the error there, it was returned to the client and not logged to console.

When I throw an error in a plugin, it is sent to the client but also logged to a console, as if it was an uncaught error.

Am I doing it correctly? Is there a way to avoid having a full stack trace in the server console / logs? My code does not have any error, I just want to indicate a problematic query to the user.

馃攲 plugins

Most helpful comment

To give a concrete followup to anyone stumbling on this issue, here's what I've learned.

Throwing an error from inside the plugin methods will cause didEncounterErrors to fire. The only plugin method which lets you actually send a response is responseForOperation. There are two ways, therefore, to modify the response:

  1. in responseForOperation
  2. By modifying context.response.

If you follow the code, you'll find that before issuing the HTTP response, the Apollo code checks to see if erorrs.length is non-zero and that data is null. If this is the case, it responds with a 400 status code and an object called "error" which contains the errors. Essentially, it doesn't know what to do with it at that point.

In fact, returning from responseForOperation with errors, but no data object, will cause the above to happen as well.

To address my problem, I am returning the a properly spec'd GQl response, including errors and data with null values for each of context.operation.selectionSet.selections. This makes the response mimic what would be expected when throwing any error inside of resolvers. This special case exists because we want to stop execution from inside of a plugin.

All 7 comments

Depending on what exact behavior you want you may want to look at https://www.apollographql.com/docs/apollo-server/integrations/plugins/#didresolveoperation

Seems like you could forgo the execution of the requested operation if you found an invalid header and return a response to the requester without throwing an error via didResolveOperation returning a non-null GraphQLResponse (according to the docs linked).

Thanks. It also sounds like I cannot utilize the standard pipeline with formatError etc., right?

You could use formatError but it sounds like you didn't want to throw an error or log it to a console. For formatError to execute, an uncaught error in parse, validate or execute phases would need to be thrown.

@borekb I am struggling with a similar problem. In my case, when I throw an error from a plugin lifecycle method, the value returned to the client is

{
  "error": {
    "errors": [...]
  }
}

Which is not up to spec AFAIK and should instead be

{
  "errors": [...],
  "data": {... null}
}

Have you seen this behavior too as it pertains to error handling in plugins?

@aaronleesmith Sorry, it's been a while, I don't remember whether I've seen your output or not.

To give a concrete followup to anyone stumbling on this issue, here's what I've learned.

Throwing an error from inside the plugin methods will cause didEncounterErrors to fire. The only plugin method which lets you actually send a response is responseForOperation. There are two ways, therefore, to modify the response:

  1. in responseForOperation
  2. By modifying context.response.

If you follow the code, you'll find that before issuing the HTTP response, the Apollo code checks to see if erorrs.length is non-zero and that data is null. If this is the case, it responds with a 400 status code and an object called "error" which contains the errors. Essentially, it doesn't know what to do with it at that point.

In fact, returning from responseForOperation with errors, but no data object, will cause the above to happen as well.

To address my problem, I am returning the a properly spec'd GQl response, including errors and data with null values for each of context.operation.selectionSet.selections. This makes the response mimic what would be expected when throwing any error inside of resolvers. This special case exists because we want to stop execution from inside of a plugin.

Hi aaronleesmith! Thank you for the information. I'm trying to do the same...basically in a federated service I defined a custom plugin to check the query cost. When the cost reach the limit, I throw an error, but unfortunately when it happens, the Gateway shows a Bad request because the "formatError" is not called. Can I ask you how did you resolve this? Do you have a piece of code to share? Thanks

Was this page helpful?
0 / 5 - 0 ratings

Related issues

nevyn-lookback picture nevyn-lookback  路  3Comments

dbrrt picture dbrrt  路  3Comments

leinue picture leinue  路  3Comments

dobesv picture dobesv  路  3Comments

stevezau picture stevezau  路  3Comments