apollo-server
apollo-server copied to clipboard
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
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 usingdidEncounterErrors
hook, and then finally go throughformatError
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
andres
.
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.