apollo-server-errors
Version:
326 lines (282 loc) • 9.62 kB
text/typescript
import {
ASTNode,
GraphQLError,
GraphQLFormattedError,
Source,
SourceLocation,
printError,
formatError,
} from 'graphql';
declare module 'graphql' {
export interface GraphQLErrorExtensions {
exception?: {
code?: string;
stacktrace?: ReadonlyArray<string>;
};
}
}
// Note: We'd like to switch to `extends GraphQLError` and look forward to doing so
// as soon as we drop support for `graphql` bellow `v15.7.0`.
export class ApolloError extends Error implements GraphQLError {
public extensions: Record<string, any>;
override readonly name!: string;
readonly locations: ReadonlyArray<SourceLocation> | undefined;
readonly path: ReadonlyArray<string | number> | undefined;
readonly source: Source | undefined;
readonly positions: ReadonlyArray<number> | undefined;
readonly nodes: ReadonlyArray<ASTNode> | undefined;
public originalError: Error | undefined;
[key: string]: any;
constructor(
message: string,
code?: string,
extensions?: Record<string, any>,
) {
super(message);
// if no name provided, use the default. defineProperty ensures that it stays non-enumerable
if (!this.name) {
Object.defineProperty(this, 'name', { value: 'ApolloError' });
}
if (extensions?.extensions) {
throw Error(
'Pass extensions directly as the third argument of the ApolloError constructor: `new ' +
'ApolloError(message, code, {myExt: value})`, not `new ApolloError(message, code, ' +
'{extensions: {myExt: value}})`',
);
}
this.extensions = { ...extensions, code };
}
toJSON(): GraphQLFormattedError {
return formatError(toGraphQLError(this));
}
override toString(): string {
return printError(toGraphQLError(this));
}
get [Symbol.toStringTag](): string {
return this.name;
}
}
function toGraphQLError(error: ApolloError): GraphQLError {
return new GraphQLError(
error.message,
error.nodes,
error.source,
error.positions,
error.path,
error.originalError,
error.extensions,
);
}
function enrichError(error: Partial<GraphQLError>, debug: boolean = false) {
// follows similar structure to https://github.com/graphql/graphql-js/blob/main/src/error/GraphQLError.ts#L127-L176
// with the addition of name
const expanded = Object.create(Object.getPrototypeOf(error), {
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?.code || 'INTERNAL_SERVER_ERROR',
exception: {
...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) {
const stack = error.originalError?.stack || error.stack;
expanded.extensions.exception.stacktrace = 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;
// This declaration means it takes any "class" that has a constructor that
// takes a single string, and should be invoked via the `new` operator.
errorClass?: new (message: string) => ApolloError;
}
export function fromGraphQLError(error: GraphQLError, options?: ErrorOptions) {
const copy: ApolloError = options?.errorClass
? new options.errorClass(error.message)
: new ApolloError(error.message);
// copy enumerable keys
Object.entries(error).forEach(([key, value]) => {
if (key === 'extensions') {
return; // extensions are handled bellow
}
copy[key] = value;
});
// merge extensions instead of just copying them
copy.extensions = {
...copy.extensions,
...error.extensions,
};
// Fallback on default for code
if (!copy.extensions.code) {
copy.extensions.code = 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 as any)[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, extensions?: Record<string, any>) {
super(message, 'UNAUTHENTICATED', extensions);
Object.defineProperty(this, 'name', { value: 'AuthenticationError' });
}
}
export class ForbiddenError extends ApolloError {
constructor(message: string, extensions?: Record<string, any>) {
super(message, 'FORBIDDEN', extensions);
Object.defineProperty(this, 'name', { value: 'ForbiddenError' });
}
}
export class PersistedQueryNotFoundError extends ApolloError {
constructor() {
super('PersistedQueryNotFound', 'PERSISTED_QUERY_NOT_FOUND');
Object.defineProperty(this, 'name', {
value: 'PersistedQueryNotFoundError',
});
}
}
export class PersistedQueryNotSupportedError extends ApolloError {
constructor() {
super('PersistedQueryNotSupported', 'PERSISTED_QUERY_NOT_SUPPORTED');
Object.defineProperty(this, 'name', {
value: 'PersistedQueryNotSupportedError',
});
}
}
export class UserInputError extends ApolloError {
constructor(message: string, extensions?: Record<string, any>) {
super(message, 'BAD_USER_INPUT', extensions);
Object.defineProperty(this, 'name', { value: 'UserInputError' });
}
}
export function formatApolloErrors(
errors: ReadonlyArray<Error>,
options?: {
formatter?: (error: GraphQLError) => GraphQLFormattedError;
debug?: boolean;
},
): Array<ApolloError> {
if (!options) {
return errors.map((error) => enrichError(error));
}
const { formatter, debug } = options;
// 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
// Some processing for these nested errors could be done here:
//
// 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 = errors.map((error) => enrichError(error, debug));
const makePrintable = (error: GraphQLFormattedError) => {
if (error instanceof Error) {
// Error defines its `message` and other fields as non-enumerable, meaning JSON.stringify does not print them.
const graphQLError = error as GraphQLFormattedError;
return {
message: graphQLError.message,
...(graphQLError.locations && { locations: graphQLError.locations }),
...(graphQLError.path && { path: graphQLError.path }),
...(graphQLError.extensions && { extensions: graphQLError.extensions }),
};
}
return error;
};
if (!formatter) {
return enrichedErrors;
}
return enrichedErrors.map((error) => {
try {
return makePrintable(formatter(error));
} catch (err) {
if (debug) {
// XXX: This cast is pretty sketchy, as other error types can be thrown!
return enrichError(err as Partial<GraphQLError>, debug);
} else {
// obscure error
const newError = fromGraphQLError(
new GraphQLError('Internal server error'),
);
return enrichError(newError, debug);
}
}
}) as Array<ApolloError>;
}