apollo-server/packages/apollo-server-core/src/errors.ts
David Glasser 5c65742a39
Factor out runQuery's use of logFunction into an extension (#1128)
This requires a slightly newer graphql-extensions beta which has more arguments
to requestDidStart.

Also make it ok to not pass logFunction to formatApolloErrors, and make sure
custom fieldResolvers continue to work with extensions (by upgrading the
dependency and fixing a logic bug).

The custom fieldResolvers fix means that we now unconditionally put the
extension stack on the GraphQL context, which means that the context can no
longer be (say) a string.  I changed a test that expected string contexts to
work. You couldn't use a string for a context when using extensions before, so
this isn't that big of a change.
2018-06-01 21:16:25 -07:00

267 lines
7.5 KiB
TypeScript

import { GraphQLError } from 'graphql';
import { LogStep, LogAction, LogFunction } from './logging';
export class ApolloError extends Error implements GraphQLError {
public extensions: Record<string, any>;
readonly name;
readonly locations;
readonly path;
readonly source;
readonly positions;
readonly nodes;
public originalError;
[key: string]: any;
constructor(
message: string,
code?: string,
properties?: Record<string, any>,
) {
super(message);
// Set the prototype explicitly.
// https://stackoverflow.com/a/41102306
Object.setPrototypeOf(this, ApolloError.prototype);
if (properties) {
Object.keys(properties).forEach(key => {
this[key] = properties[key];
});
}
//if no name provided, use the default. defineProperty ensures that it stays non-enumerable
if (!this.name) {
Object.defineProperty(this, 'name', { value: 'ApolloError' });
}
//extensions are flattened to be included in the root of GraphQLError's, so
//don't add properties to extensions
this.extensions = { code };
}
}
function enrichError(error: Partial<GraphQLError>, debug: boolean = false) {
const expanded = {} as any;
// follows similar structure to https://github.com/graphql/graphql-js/blob/master/src/error/GraphQLError.js#L145-L193
// with the addition of name
Object.defineProperties(expanded, {
name: {
value: error.name,
},
message: {
value: error.message,
enumerable: true,
writable: true,
},
locations: {
value: error.locations || undefined,
enumerable: true,
},
path: {
value: error.path || undefined,
enumerable: true,
},
nodes: {
value: error.nodes || undefined,
},
source: {
value: error.source || undefined,
},
positions: {
value: error.positions || undefined,
},
originalError: {
value: error.originalError,
},
});
expanded.extensions = {
...error.extensions,
code:
(error.extensions && error.extensions.code) || 'INTERNAL_SERVER_ERROR',
exception: {
...(error.extensions && error.extensions.exception),
...(error.originalError as any),
},
};
//ensure that extensions is not taken from the originalError
//graphql-js ensures that the originalError's extensions are hoisted
//https://github.com/graphql/graphql-js/blob/0bb47b2/src/error/GraphQLError.js#L138
delete expanded.extensions.exception.extensions;
if (debug && !expanded.extensions.exception.stacktrace) {
expanded.extensions.exception.stacktrace =
(error.originalError &&
error.originalError.stack &&
error.originalError.stack.split('\n')) ||
(error.stack && error.stack.split('\n'));
}
if (Object.keys(expanded.extensions.exception).length === 0) {
//remove from printing an empty object
delete expanded.extensions.exception;
}
return expanded as ApolloError;
}
export function toApolloError(
error: Error & { extensions?: Record<string, any> },
code: string = 'INTERNAL_SERVER_ERROR',
): Error & { extensions: Record<string, any> } {
let err = error;
if (err.extensions) {
err.extensions.code = code;
} else {
err.extensions = { code };
}
return err as Error & { extensions: Record<string, any> };
}
export interface ErrorOptions {
code?: string;
errorClass?: typeof ApolloError;
}
export function fromGraphQLError(error: GraphQLError, options?: ErrorOptions) {
const copy: ApolloError =
options && options.errorClass
? new options.errorClass(error.message)
: new ApolloError(error.message);
//copy enumerable keys
Object.keys(error).forEach(key => {
copy[key] = error[key];
});
//extensions are non enumerable, so copy them directly
copy.extensions = {
...copy.extensions,
...error.extensions,
};
//Fallback on default for code
if (!copy.extensions.code) {
copy.extensions.code = (options && options.code) || 'INTERNAL_SERVER_ERROR';
}
//copy the original error, while keeping all values non-enumerable, so they
//are not printed unless directly referenced
Object.defineProperty(copy, 'originalError', { value: {} });
Object.getOwnPropertyNames(error).forEach(key => {
Object.defineProperty(copy.originalError, key, { value: error[key] });
});
return copy;
}
export class SyntaxError extends ApolloError {
constructor(message: string) {
super(message, 'GRAPHQL_PARSE_FAILED');
// Set the prototype explicitly.
// https://stackoverflow.com/a/41102306
Object.setPrototypeOf(this, SyntaxError.prototype);
Object.defineProperty(this, 'name', { value: 'SyntaxError' });
}
}
export class ValidationError extends ApolloError {
constructor(message: string) {
super(message, 'GRAPHQL_VALIDATION_FAILED');
// Set the prototype explicitly.
// https://stackoverflow.com/a/41102306
Object.setPrototypeOf(this, ValidationError.prototype);
Object.defineProperty(this, 'name', { value: 'ValidationError' });
}
}
export class AuthenticationError extends ApolloError {
constructor(message: string) {
super(message, 'UNAUTHENTICATED');
// Set the prototype explicitly.
// https://stackoverflow.com/a/41102306
Object.setPrototypeOf(this, AuthenticationError.prototype);
Object.defineProperty(this, 'name', { value: 'AuthenticationError' });
}
}
export class ForbiddenError extends ApolloError {
constructor(message: string) {
super(message, 'FORBIDDEN');
// Set the prototype explicitly.
// https://stackoverflow.com/a/41102306
Object.setPrototypeOf(this, ForbiddenError.prototype);
Object.defineProperty(this, 'name', { value: 'ForbiddenError' });
}
}
export function formatApolloErrors(
errors: Array<Error>,
options?: {
formatter?: Function;
logFunction?: LogFunction;
debug?: boolean;
},
): Array<ApolloError> {
if (!options) {
return errors.map(error => enrichError(error));
}
const { formatter, debug, logFunction } = options;
const flattenedErrors = [];
errors.forEach(error => {
// Errors that occur in graphql-tools can contain an errors array that contains the errors thrown in a merged schema
// https://github.com/apollographql/graphql-tools/blob/3d53986ca/src/stitching/errors.ts#L104-L107
//
// They are are wrapped in an extra GraphQL error
// https://github.com/apollographql/graphql-tools/blob/3d53986ca/src/stitching/errors.ts#L109-L113
// which calls:
// https://github.com/graphql/graphql-js/blob/0a30b62964/src/error/locatedError.js#L18-L37
if (Array.isArray((error as any).errors)) {
(error as any).errors.forEach(e => flattenedErrors.push(e));
} else if (
(error as any).originalError &&
Array.isArray((error as any).originalError.errors)
) {
(error as any).originalError.errors.forEach(e => flattenedErrors.push(e));
} else {
flattenedErrors.push(error);
}
});
const enrichedErrors = flattenedErrors.map(error =>
enrichError(error, debug),
);
if (!formatter) {
return enrichedErrors;
}
return enrichedErrors.map(error => {
try {
return formatter(error);
} catch (err) {
logFunction &&
logFunction({
action: LogAction.cleanup,
step: LogStep.status,
data: err,
key: 'error',
});
if (debug) {
return enrichError(err, debug);
} else {
//obscure error
const newError = fromGraphQLError(
new GraphQLError('Internal server error'),
);
return enrichError(newError, debug);
}
}
}) as Array<ApolloError>;
}