errors: send stack in debug, codes, make ApolloErrors

This commit is contained in:
Evans Hauser 2018-04-17 21:19:44 -07:00
parent e1cfd83124
commit 4230220e2b
No known key found for this signature in database
GPG key ID: 88AF586817F52EEC
5 changed files with 162 additions and 19 deletions

View file

@ -0,0 +1,98 @@
import { GraphQLError } from 'graphql';
export interface IApolloError {}
export class ApolloError extends Error {
public extensions;
constructor(message: string, code: string, properties?: Record<string, any>) {
super(message);
this.extensions = { ...properties, code };
}
}
export function formatError(error: GraphQLError, debug: boolean = false) {
const expanded: GraphQLError = {
...error,
extensions: {
...error.extensions,
code: (error.extensions && error.extensions.code) || 'INTERNAL_ERROR',
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 =
(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
expanded.extensions.exception = undefined;
}
return expanded;
}
export function toApolloError(
error: Error,
code: string = 'INTERNAL_ERROR',
): Error & { extensions: Record<string, any> } {
let err: GraphQLError = error;
if (err.extensions) {
err.extensions.code = code;
} else {
err.extensions = { code };
}
return err as Error & { extensions: Record<string, any> };
}
export function fromGraphQLError(
error: GraphQLError,
code: string = 'INTERNAL_ERROR',
) {
const copy: GraphQLError = {
...error,
};
copy.extensions = {
...copy.extensions,
code,
};
//copy the original error, while keeping all values non-enumerable, so they
//are not printed unless directly referenced
Object.defineProperty(copy, 'originalError', { value: {} });
Reflect.ownKeys(error).forEach(key => {
Object.defineProperty(copy.originalError, key, { value: error[key] });
});
return copy;
}
export class ParseError extends ApolloError {
name = 'MalformedQueryError';
constructor(message: string) {
super(message, 'MALFORMED_QUERY');
}
}
export class ValidationError extends ApolloError {
name = 'ValidationError';
constructor(message: string) {
super(message, 'QUERY_VALIDATION_FAILED');
}
}
export class AuthenticationError extends ApolloError {
name = 'UnauthorizedError';
constructor(message: string) {
super(message, 'UNAUTHORIZED');
}
}

View file

@ -10,3 +10,10 @@ export {
default as GraphQLOptions, default as GraphQLOptions,
resolveGraphqlOptions, resolveGraphqlOptions,
} from './graphqlOptions'; } from './graphqlOptions';
export {
ApolloError,
toApolloError,
ParseError,
ValidationError,
AuthenticationError,
} from './errors';

View file

@ -1,15 +1,10 @@
import { import { parse, getOperationAST, DocumentNode, ExecutionResult } from 'graphql';
parse,
getOperationAST,
DocumentNode,
formatError,
ExecutionResult,
} from 'graphql';
import { runQuery } from './runQuery'; import { runQuery } from './runQuery';
import { import {
default as GraphQLOptions, default as GraphQLOptions,
resolveGraphqlOptions, resolveGraphqlOptions,
} from './graphqlOptions'; } from './graphqlOptions';
import { formatError } from './errors';
export interface HttpQueryRequest { export interface HttpQueryRequest {
method: string; method: string;
@ -56,7 +51,12 @@ export async function runHttpQuery(
} catch (e) { } catch (e) {
throw new HttpQueryError(500, e.message); throw new HttpQueryError(500, e.message);
} }
const formatErrorFn = optionsObject.formatError || formatError; const formatErrorFn = error =>
optionsObject.formatError(formatError(error)) || formatError;
const debugDefault =
process.env.NODE_ENV !== 'production' && process.env.NODE_ENV !== 'test';
const debug =
optionsObject.debug !== undefined ? optionsObject.debug : debugDefault;
let requestPayload; let requestPayload;
switch (request.method) { switch (request.method) {
@ -149,7 +149,7 @@ export async function runHttpQuery(
operationName: operationName, operationName: operationName,
logFunction: optionsObject.logFunction, logFunction: optionsObject.logFunction,
validationRules: optionsObject.validationRules, validationRules: optionsObject.validationRules,
formatError: formatErrorFn, formatError: optionsObject.formatError,
formatResponse: optionsObject.formatResponse, formatResponse: optionsObject.formatResponse,
fieldResolver: optionsObject.fieldResolver, fieldResolver: optionsObject.fieldResolver,
debug: optionsObject.debug, debug: optionsObject.debug,
@ -176,6 +176,8 @@ export async function runHttpQuery(
if (!isBatch) { if (!isBatch) {
const gqlResponse = responses[0]; const gqlResponse = responses[0];
//This code is run on parse/validation errors and any other error that
//doesn't reach GraphQL execution
if (gqlResponse.errors && typeof gqlResponse.data === 'undefined') { if (gqlResponse.errors && typeof gqlResponse.data === 'undefined') {
throw new HttpQueryError(400, JSON.stringify(gqlResponse), true, { throw new HttpQueryError(400, JSON.stringify(gqlResponse), true, {
'Content-Type': 'application/json', 'Content-Type': 'application/json',

View file

@ -8,7 +8,6 @@ import {
validate, validate,
execute, execute,
GraphQLError, GraphQLError,
formatError,
specifiedRules, specifiedRules,
ValidationContext, ValidationContext,
} from 'graphql'; } from 'graphql';
@ -24,6 +23,8 @@ import {
CacheControlExtensionOptions, CacheControlExtensionOptions,
} from 'apollo-cache-control'; } from 'apollo-cache-control';
import { fromGraphQLError, formatError } from './errors';
export interface GraphQLResponse { export interface GraphQLResponse {
data?: object; data?: object;
errors?: Array<GraphQLError & object>; errors?: Array<GraphQLError & object>;
@ -83,18 +84,28 @@ function printStackTrace(error: Error) {
console.error(error.stack); console.error(error.stack);
} }
function format(errors: Array<Error>, formatter?: Function): Array<Error> { function format(
return errors.map(error => { errors: Array<Error>,
options?: {
formatter?: Function;
debug?: boolean;
},
): Array<Error> {
const { formatter, debug } = options;
return errors.map(error => formatError(error, debug)).map(error => {
if (formatter !== undefined) { if (formatter !== undefined) {
try { try {
return formatter(error); return formatter(error);
} catch (err) { } catch (err) {
console.error('Error in formatError function:', err); console.error('Error in formatError function:', err);
const newError = new Error('Internal server error'); const newError: GraphQLError = fromGraphQLError(
return formatError(newError); new GraphQLError('Internal server error'),
'INTERNAL_ERROR',
);
return formatError(newError, debug);
} }
} else { } else {
return formatError(error); return error;
} }
}) as Array<Error>; }) as Array<Error>;
} }
@ -164,7 +175,10 @@ function doRunQuery(options: QueryOptions): Promise<GraphQLResponse> {
} catch (syntaxError) { } catch (syntaxError) {
logFunction({ action: LogAction.parse, step: LogStep.end }); logFunction({ action: LogAction.parse, step: LogStep.end });
return Promise.resolve({ return Promise.resolve({
errors: format([syntaxError], options.formatError), errors: format([fromGraphQLError(syntaxError, 'MALFORMED_QUERY')], {
formatter: options.formatError,
debug,
}),
}); });
} }
} else { } else {
@ -178,9 +192,18 @@ function doRunQuery(options: QueryOptions): Promise<GraphQLResponse> {
logFunction({ action: LogAction.validation, step: LogStep.start }); logFunction({ action: LogAction.validation, step: LogStep.start });
const validationErrors = validate(options.schema, documentAST, rules); const validationErrors = validate(options.schema, documentAST, rules);
logFunction({ action: LogAction.validation, step: LogStep.end }); logFunction({ action: LogAction.validation, step: LogStep.end });
if (validationErrors.length) { if (validationErrors.length) {
return Promise.resolve({ return Promise.resolve({
errors: format(validationErrors, options.formatError), errors: format(
validationErrors.map(err =>
fromGraphQLError(err, 'QUERY_VALIDATION_FAILED'),
),
{
formatter: options.formatError,
debug,
},
),
}); });
} }
@ -209,7 +232,10 @@ function doRunQuery(options: QueryOptions): Promise<GraphQLResponse> {
}; };
if (result.errors) { if (result.errors) {
response.errors = format(result.errors, options.formatError); response.errors = format(result.errors, {
formatter: options.formatError,
debug,
});
if (debug) { if (debug) {
result.errors.map(printStackTrace); result.errors.map(printStackTrace);
} }
@ -231,7 +257,10 @@ function doRunQuery(options: QueryOptions): Promise<GraphQLResponse> {
logFunction({ action: LogAction.execute, step: LogStep.end }); logFunction({ action: LogAction.execute, step: LogStep.end });
logFunction({ action: LogAction.request, step: LogStep.end }); logFunction({ action: LogAction.request, step: LogStep.end });
return Promise.resolve({ return Promise.resolve({
errors: format([executionError], options.formatError), errors: format([fromGraphQLError(executionError, 'EXECUTION_ERROR')], {
formatter: options.formatError,
debug,
}),
}); });
} }
} }

View file

@ -1,5 +1,12 @@
// Expose types which can be used by both middleware flavors. // Expose types which can be used by both middleware flavors.
export { GraphQLOptions } from 'apollo-server-core'; export { GraphQLOptions } from 'apollo-server-core';
export {
toApolloError,
ApolloError,
AuthenticationError,
ParseError,
ValidationError,
} from 'apollo-server-core';
// Express Middleware // Express Middleware
export { export {