apollo-server icon indicating copy to clipboard operation
apollo-server copied to clipboard

How to correctly propagate errors from plugins?

Open borekb opened this issue 5 years ago • 11 comments

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.

borekb avatar Dec 20 '19 15:12 borekb

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).

brianjquinn avatar Jan 23 '20 17:01 brianjquinn

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

borekb avatar Jan 24 '20 10:01 borekb

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.

brianjquinn avatar Jan 24 '20 17:01 brianjquinn

@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 avatar Jun 11 '20 19:06 aaronleesmith

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

borekb avatar Jun 12 '20 08:06 borekb

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.

aaronleesmith avatar Jun 12 '20 15:06 aaronleesmith

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

besasch88 avatar Dec 09 '20 00:12 besasch88

i also have this question...

According to @aaronleesmith 's answer, i use this code to solve this question.

const error = new HttpError({
        status: 403,
        data: {
            c: 403003,
            m: 'auth error'
        }
    })
requestContext.errors = [error]
requestContext.response = {
    ...requestContext.response,
    data: null,
    errors: requestContext.errors
}

i try it in willSendResponse, also i think it can work in responseForOperation.

i hope this helps you.

Terness avatar Nov 09 '21 02:11 Terness

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:

Is this still true? I'm working on a plugin for validate the document, I perform this validation on the validationDidStart method and when I throw an error the listener didEncounterErrors doesn't execute. I've tested forcing other types of errors (GRAPHQL_VALIDATION_FAILED) the listener it's called.

Trying what @Terness suggested doesn't work for me, since the errors/response properties are readonly in typescript.

Any suggestion?

Edit: I've solved all my issues moving the depth limit validation from the validationDidStart to the didResolveOperation

ispirals avatar Nov 19 '21 14:11 ispirals

bump

LockedThread avatar Apr 11 '22 09:04 LockedThread

Just ran into this as well, we need to validate an incoming query and abort an execution before resolving any fields, but this bug limits our ability to change HTTP response code from 500 to something else and return { data: null } according to specs

jaroslav-kubicek avatar Jul 20 '22 16:07 jaroslav-kubicek

In Apollo Server 4 (currently a Release Candidate) we are explicitly documenting and testing support for throwing GraphQLErrors from didResolveOperation (https://github.com/apollographql/apollo-server/pull/7001), including setting HTTP status and headers. (Note that these error responses will not have a data field instead of having data:null, which is what you're supposed to do if you have an error before execution begins; I hope that's what you mean, @jaroslav-kubicek .)

We don't necessarily consider every plugin hook to provide specified behavior upon throwing, but this one does now.

glasser avatar Oct 07 '22 23:10 glasser