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

Further explanation of `didEncounterErrors` on extending additional properties on errors

Open smblee opened this issue 4 years ago • 8 comments

In the documentation (https://www.apollographql.com/docs/apollo-server/data/errors/), if I need additional properties, it suggests using didEncounterErrors lifecycle hook to add additional properties.

consider using the didEncounterErrors lifecycle hook to attach additional properties to the error

I am having a hard time figuring out how to actually extend the info because the function signature for formatError prop is (error: GraphQLError) => GraphQLFormattedError. I would like the formatError prop function to have things like request and context as well.

smblee avatar Apr 19 '20 20:04 smblee

I second this. We used to use a custom formatError function that had access to the request context and moved to an ErrorFormatExtension once that wasn't possible anymore.

export class ErrorFormatExtension extends GraphQLExtension {
    willSendResponse(o) {
        const { context, graphqlResponse } = o;
        if (graphqlResponse.errors) {
            graphqlResponse.errors = graphqlResponse.errors.map(e => customFormatError(e, context));
        }
        return o;
    }
}

Now that's deprecated so I took a look at Plugins, but I can't figure out how I can mutate the response/errors using plugins all the typescript typings appear to be readonly

Might I add, that it's relatively hard to keep up with all these changes?

simhnna avatar Jun 02 '20 14:06 simhnna

For those of you wondering, this is how I am currently handling the errors:

Before I go into error handling logic with apollo, just wanted to preface with how I handle error in my app in general.

  • There are 2 different categories of errors: known/unknown. Known errors are errors that I throw purposefully, and would like to get returned back to the user (to show in the UI, such as form errors like invalid password). Unknown errors are errors that I don't want to expose or expect (such as database query errors), and I want to convert to generic message like "something went wrong" and also want reported back to me via reporting system (I use Sentry).

  • I currently filter out the "expected" from "unexpected" using an array of knownErrors which consist of errors I know I will throw from the app.


And this is how I handle them with apollo server.

tl;dr: didEncounterErrors hook => formatError prop

Longer version:

  • When an error occurs (any type of "thrown" error whether intentional or not), it will be first caught by graphqlErrorHandler plugin using didEncounterErrors hook, and then finally go through formatError function which gets returned as a response back to the user.

  • In order to access request/response bodies, I put them inside my graphql context object as req and res.


Code example:

apolloServer.ts

  const apolloServer = new ApolloServer({
    schema,
    plugins: [graphqlErrorHandler],
    formatError: error => {
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      const sentryId = error && error.originalError && (error.originalError as any).sentryId;
      // if we didn't report this to sentry, we know this error is something we expected, so just return the error.
      if (sentryId === undefined) {
        return error;
      }

      let errorResponse: { message: string; debug?: object } = {
        message: `Something unexpected happened. Sentry ID: ${sentryId}`,
      };

      // attach the whole error object for non-production environment.
      if (!config.isProduction) {
        errorResponse = {
          ...errorResponse,
          debug: error,
        };
      }

      return errorResponse;
    },
    context: ({ req, res }) => {
      const context = { req, res };

      // add user to context if exists
      try {
        const authorization = req.headers['authorization']!;
        const token = authorization.split(' ')[1];
        const user = verifyToken(token) as ContextUser;
        Object.assign(context, { user });
      } catch (err) {
        // do nothing since requests don't need auth.
      }

      return context;
    },
  });

graphqlErrorHandler.ts

const knownErrors = [ArgumentValidationError, UserInputError, EntityNotFoundError];

const graphqlErrorHandler: ApolloServerPlugin = {
  requestDidStart() {
    return {
      didEncounterErrors(requestContext) {
        const context = requestContext.context;

        for (const error of requestContext.errors) {
          const err = error.originalError || error;

          // don't do anything with errors we expect.
          if (knownErrors.some(expectedError => err instanceof expectedError)) {
            continue;
          }

          let sentryId = 'ID only generated in production';
          if (config.isProduction) {
            Sentry.withScope((scope: Scope) => {
              if (context.user) {
                scope.setUser({
                  id: context.user.id,
                  email: context.user.email,
                  username: context.user.handle,
                  // eslint-disable-next-line @typescript-eslint/camelcase
                  ip_address: context.req.ip,
                });
              }
              scope.setExtra('body', context.req.body);
              scope.setExtra('origin', context.req.headers.origin);
              scope.setExtra('user-agent', context.req.headers['user-agent']);
              sentryId = Sentry.captureException(err);
            });
          }

          // HACK; set sentry id to indicate this is an error that we did not expect. `formatError` handler will check for this.
          // eslint-disable-next-line @typescript-eslint/no-explicit-any
          (err as any).sentryId = sentryId;
        }

        return;
      },
    };
  },
};

export default graphqlErrorHandler;

smblee avatar Jun 02 '20 23:06 smblee

I also have the same problem and I also decided to use the small hack proposed above. It is weird that it is not a native support for it...

Sytten avatar Jul 01 '20 17:07 Sytten

@smblee's hack doesn't seem to work if the errors happen in request validation

thekarel avatar Jan 15 '21 23:01 thekarel

I'm looking for a way to emit internationalized error messages during validation.

It looks like Apollo isn't up to the task

iambumblehead avatar Apr 21 '22 18:04 iambumblehead

Defining a 'plugin' as mentioned by @simhnna can work. Attaching numeric codes or other custom details to the final response is clunky. It would be better if context were given directly to the input-validating function,

plugins: [{
  requestDidStart: () => ({
    willSendResponse: ctx => {
      if ( ctx.response && ctx.response.errors ) {
        ctx.response.errors
          .forEach( e => rootScalarErrMessageToI18N( ctx, e ) );
      }
    }
  })
}]

iambumblehead avatar Apr 21 '22 20:04 iambumblehead

The hack doesn't work here, the errors object seems to not persist the changes between didEncounterErrors and formatError.

Zikoat avatar Apr 26 '22 09:04 Zikoat

@Zikoat using formatError triggers some different strange logic https://github.com/apollographql/apollo-server/issues/6345

iambumblehead avatar Apr 26 '22 16:04 iambumblehead

We can't add a full context to formatError because it is also used for very early errors parsing your request. See some discussion at https://github.com/apollographql/apollo-server/pull/5689#issuecomment-1283148966

You should be able to mutate errors in didEncounterErrors in Apollo Server 4. If this doesn't work for you, please open a new issue with a self-contained reproduction.

glasser avatar Oct 20 '22 20:10 glasser