Further explanation of `didEncounterErrors` on extending additional properties on errors
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.
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?
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
knownErrorswhich 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
graphqlErrorHandlerplugin usingdidEncounterErrorshook, and then finally go throughformatErrorfunction 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
reqandres.
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;
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...
@smblee's hack doesn't seem to work if the errors happen in request validation
I'm looking for a way to emit internationalized error messages during validation.
It looks like Apollo isn't up to the task
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 ) );
}
}
})
}]
The hack doesn't work here, the errors object seems to not persist the changes between didEncounterErrors and formatError.
@Zikoat using formatError triggers some different strange logic https://github.com/apollographql/apollo-server/issues/6345
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.