import { GraphQLError } from 'graphql'; import { LogStep, LogAction, LogFunction } from './logging'; export class ApolloError extends Error implements GraphQLError { public extensions: Record; readonly name; readonly locations; readonly path; readonly source; readonly positions; readonly nodes; public originalError; [key: string]: any; constructor( message: string, code?: string, properties?: Record, ) { super(message); 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, 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 }, code: string = 'INTERNAL_SERVER_ERROR', ): Error & { extensions: Record } { let err = error; if (err.extensions) { err.extensions.code = code; } else { err.extensions = { code }; } return err as Error & { extensions: Record }; } 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'); Object.defineProperty(this, 'name', { value: 'SyntaxError' }); } } export class ValidationError extends ApolloError { constructor(message: string) { super(message, 'GRAPHQL_VALIDATION_FAILED'); Object.defineProperty(this, 'name', { value: 'ValidationError' }); } } export class AuthenticationError extends ApolloError { constructor(message: string) { super(message, 'UNAUTHENTICATED'); Object.defineProperty(this, 'name', { value: 'AuthenticationError' }); } } export class ForbiddenError extends ApolloError { constructor(message: string) { super(message, 'FORBIDDEN'); Object.defineProperty(this, 'name', { value: 'ForbiddenError' }); } } export class BadUserInputError extends ApolloError { constructor(message: string, properties?: Record) { super(message, 'BAD_USER_INPUT', properties); Object.defineProperty(this, 'name', { value: 'BadUserInputError' }); } } export function formatApolloErrors( errors: Array, options?: { formatter?: Function; logFunction?: LogFunction; debug?: boolean; }, ): Array { 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; }