Replace ApolloErrorProcessor class with processApolloError function

Allows error handling to be configured through an options object instead of by re-implementing the class.
This commit is contained in:
Simon Garner
2021-10-28 16:22:56 +13:00
parent 9b0d0e06e4
commit 6923ead0e6
6 changed files with 245 additions and 255 deletions
+12 -10
View File
@@ -10,36 +10,38 @@ import {
} from './types';
export interface ApolloErrorHandlerResultInterface {
processedErrors: ProcessedApolloError[];
allErrors: ProcessedApolloError[];
validationRuleViolations?: ValidationRuleViolation[];
}
export class ApolloErrorHandlerResult implements ApolloErrorHandlerResultInterface {
public readonly allErrors: ProcessedApolloError[];
public constructor(
public readonly processedErrors: ProcessedApolloError[],
public readonly unhandledErrors: ProcessedApolloError[],
public readonly handledErrors: ProcessedApolloError[],
) {}
) {
this.allErrors = [...unhandledErrors, ...handledErrors];
}
public get networkErrors(): NetworkError[] {
return this.processedErrors.filter((e): e is NetworkError => e.type === ApolloErrorType.NETWORK_ERROR);
return this.allErrors.filter((e): e is NetworkError => e.type === ApolloErrorType.NETWORK_ERROR);
}
public get serverErrors(): ServerError[] {
return this.processedErrors.filter((e): e is ServerError => e.type === ApolloErrorType.SERVER_ERROR);
return this.allErrors.filter((e): e is ServerError => e.type === ApolloErrorType.SERVER_ERROR);
}
public get unauthorizedErrors(): UnauthorizedError[] {
return this.processedErrors.filter((e): e is UnauthorizedError => e.type === ApolloErrorType.UNAUTHORIZED_ERROR);
return this.allErrors.filter((e): e is UnauthorizedError => e.type === ApolloErrorType.UNAUTHORIZED_ERROR);
}
public get userInputErrors(): UserInputError[] {
return this.processedErrors.filter((e): e is UserInputError => e.type === ApolloErrorType.BAD_USER_INPUT);
return this.allErrors.filter((e): e is UserInputError => e.type === ApolloErrorType.BAD_USER_INPUT);
}
public get inputValidationErrors(): InputValidationError[] {
return this.processedErrors.filter(
(e): e is InputValidationError => e.type === ApolloErrorType.INPUT_VALIDATION_ERROR,
);
return this.allErrors.filter((e): e is InputValidationError => e.type === ApolloErrorType.INPUT_VALIDATION_ERROR);
}
public get validationRuleViolations(): ValidationRuleViolation[] {
-236
View File
@@ -1,236 +0,0 @@
import Vue from 'vue';
import { ApolloOperationContext } from '../types';
import {
ApolloError,
ApolloErrorType,
GraphQLError,
InputValidationError,
ProcessedApolloError,
ServerError,
UnauthorizedError,
} from './types';
import { ApolloErrorHandlerResult } from './ApolloErrorHandlerResult';
export function isApolloError(error: ApolloError | any): error is ApolloError {
return error.graphQLErrors !== undefined;
}
export function isGraphQLError(error: GraphQLError | any): error is GraphQLError {
return error.extensions !== undefined;
}
export class ApolloErrorProcessor<TApp = Vue, TContext = ApolloOperationContext> {
public static FriendlyMessages: Record<string, string> = {
FAILED_TO_FETCH:
'Unable to communicate with server. The service may be down or you may be offline. Try again in a moment.',
INTERNAL_SERVER_ERROR: `A server error has occurred.`,
};
public readonly processedErrors: ProcessedApolloError[];
protected readonly originalError: Error;
protected readonly app: TApp;
protected readonly context: TContext;
protected handledErrors: ProcessedApolloError[] = [];
public constructor(error: ApolloError, app: TApp, context: TContext) {
this.originalError = error;
this.app = app;
this.context = context;
this.processedErrors = this.processApolloError(error);
}
public get result(): ApolloErrorHandlerResult {
return new ApolloErrorHandlerResult(this.processedErrors, this.handledErrors);
}
public showErrorNotifications(): void {
// This is just an example - to do something else (e.g. showing a visible notification to the user), you should
// implement your own class that extends ApolloErrorProcessor and replace this showErrorNotifications method.
this.processedErrors.forEach(error => {
console.error(`${error.type}: ${error.message}`, error.error);
});
}
public cleanError(error: ApolloError | GraphQLError | Record<string, any>): Error {
if (error instanceof Error) {
return error;
}
// the `error` object we have may not be an actual Error instance
// create a new one suitable for e.g. capturing to Sentry
const cleanError = new Error(error.message);
if (isGraphQLError(error)) {
cleanError.name = 'GraphQLError' + (error.extensions?.code != null ? `[${error.extensions.code}]` : '');
cleanError.stack = this.originalError.stack;
Object.defineProperty(cleanError, 'nodes', { value: error.nodes });
Object.defineProperty(cleanError, 'source', { value: error.source });
Object.defineProperty(cleanError, 'positions', { value: error.positions });
Object.defineProperty(cleanError, 'path', { value: error.path });
Object.defineProperty(cleanError, 'extensions', { value: JSON.stringify(error.extensions) });
Object.defineProperty(cleanError, 'originalError', { value: this.originalError });
} else {
Object.keys(error).forEach(key => Object.defineProperty(cleanError, key, { value: error[key] }));
}
return cleanError;
}
protected isUnauthorizedError(error: GraphQLError): boolean {
return (
error.message === 'Unauthorized' ||
error.extensions?.code === 'FORBIDDEN' ||
error.extensions?.exception?.status === 401
);
}
// eslint-disable-next-line @typescript-eslint/no-empty-function,@typescript-eslint/no-unused-vars
protected onUnauthorizedError(error: UnauthorizedError): void {
// extending classes can take action here, e.g. go to log in page
}
// eslint-disable-next-line @typescript-eslint/no-empty-function,@typescript-eslint/no-unused-vars
protected onServerError(error: ServerError): void {
// extending classes can take action here, e.g. capture to Sentry
}
// eslint-disable-next-line @typescript-eslint/no-empty-function,@typescript-eslint/no-unused-vars
protected onInputValidationError(error: InputValidationError): void {
// extending classes can take action here, e.g. capture to Sentry
}
protected getFriendlyMessage(errorCode: string, errorMessage: string): string;
protected getFriendlyMessage(errorCode: string): string | undefined;
protected getFriendlyMessage(errorCode: string, errorMessage?: string): string | undefined {
return (this.constructor as typeof ApolloErrorProcessor).FriendlyMessages[errorCode] ?? errorMessage;
}
protected processErrorMessage(message: GraphQLError['message']): string {
if (typeof message === 'object') {
if (message.error != null) {
return message.error;
}
return 'Unknown error: ' + JSON.stringify(message);
}
return message;
}
protected normalizeError(error: GraphQLError | Error): Error {
if (isGraphQLError(error)) {
return {
...error,
message: this.processErrorMessage(error.message),
};
}
return error;
}
private processApolloError(error: ApolloError): ProcessedApolloError[] {
if (error.graphQLErrors != null && error.graphQLErrors.length > 0) {
// Successful request but with errors from the resolver
return error.graphQLErrors.flatMap(graphQLError => this.processGraphQLError(graphQLError));
}
if (
error.networkError != null &&
error.networkError.result != null &&
error.networkError.result.errors != null &&
error.networkError.result.errors.length > 0
) {
// Network error that contains GraphQL errors inside it. Can occur when server responds with a non-200 status code
return error.networkError.result.errors.flatMap(graphQLError => this.processGraphQLError(graphQLError));
}
if (error.networkError != null) {
// Network error, e.g. server is not responding or some other exception occurs
return this.processNetworkError(error);
}
// Some other internal server error
const processedError: ServerError = {
type: ApolloErrorType.SERVER_ERROR,
error,
message: error.message,
};
this.onServerError(processedError);
return [processedError];
}
private processGraphQLError(error: GraphQLError): ProcessedApolloError[] {
if (this.isUnauthorizedError(error)) {
// Unauthorized (not logged in, or not allowed) error
const processedError: UnauthorizedError = {
type: ApolloErrorType.UNAUTHORIZED_ERROR,
error: this.normalizeError(error),
message: this.processErrorMessage(error.message),
path: error.path,
};
this.onUnauthorizedError(processedError);
return [processedError];
}
if (error.extensions?.validationErrors != null) {
// User input validation error
const processedError: InputValidationError = {
type: ApolloErrorType.INPUT_VALIDATION_ERROR,
error: this.normalizeError(error),
message: this.processErrorMessage(error.message),
path: error.path,
invalidArgs: error.extensions.invalidArgs,
violations: error.extensions.validationErrors,
};
this.onInputValidationError(processedError);
return [processedError];
}
// Other GraphQL resolver error - probably a bug
const processedError: ServerError = {
type: error.extensions?.code != null ? error.extensions.code : ApolloErrorType.SERVER_ERROR,
error: this.normalizeError(error),
message: this.getFriendlyMessage('INTERNAL_SERVER_ERROR', this.processErrorMessage(error.message)),
path: error.path,
};
this.onServerError(processedError);
return [processedError];
}
private processNetworkError(error: ApolloError): ProcessedApolloError[] {
const errors: ProcessedApolloError[] = [];
let message: string;
if (error.networkError != null && error.networkError.message != null) {
message = this.processErrorMessage(error.networkError.message);
} else {
message = this.processErrorMessage(error.message);
}
switch (message) {
case 'Failed to fetch':
message = this.getFriendlyMessage('FAILED_TO_FETCH', message);
break;
}
errors.push({
type: ApolloErrorType.NETWORK_ERROR,
error,
statusCode: error.networkError != null ? error.networkError.statusCode : undefined,
message,
});
return errors;
}
}
+18 -7
View File
@@ -1,8 +1,8 @@
import { ApolloErrorProcessor } from './ApolloErrorProcessor';
import { processApolloError } from './processApolloError';
import { ApolloOperationContext } from '../types';
import { Vue } from 'vue/types/vue';
import { ApolloError, ApolloOperationErrorHandlerFunction } from './types';
import { ApolloErrorHandlerResultInterface } from './ApolloErrorHandlerResult';
import { ApolloErrorHandlerResult, ApolloErrorHandlerResultInterface } from './ApolloErrorHandlerResult';
/**
* This is a simple example of an error handler function. You can copy this and implement your own in your application.
@@ -12,11 +12,22 @@ export const handleApolloError: ApolloOperationErrorHandlerFunction<ApolloError,
app: Vue,
context?: ApolloOperationContext,
): ApolloErrorHandlerResultInterface => {
const processor = new ApolloErrorProcessor(error, app, context ?? {});
const { unhandledErrors, handledErrors } = processApolloError(error, {
app,
context,
// Example of a handler function for a particular type of error:
onUnauthorizedError: error => {
console.warn('Unauthorized! Logging you out...', error);
//logout();
processor.showErrorNotifications();
// Returning true indicates to the processor that we've handled this error
return true;
},
});
return {
processedErrors: processor.processedErrors,
};
unhandledErrors.forEach(error => {
console.error(`${error.type}: ${error.message}`, error.error);
});
return new ApolloErrorHandlerResult(unhandledErrors, handledErrors);
};
+1 -1
View File
@@ -3,6 +3,6 @@
*/
export * from './ApolloErrorHandlerResult';
export * from './ApolloErrorProcessor';
export * from './handleApolloError';
export * from './processApolloError';
export * from './types';
+213
View File
@@ -0,0 +1,213 @@
import Vue from 'vue';
import { ApolloOperationContext } from '../types';
import {
ApolloError,
ApolloErrorType,
GraphQLError,
InputValidationError,
NetworkError,
ProcessedApolloError,
ServerError,
UnauthorizedError,
} from './types';
export function isApolloError(error: ApolloError | any): error is ApolloError {
return error.graphQLErrors !== undefined;
}
export function isGraphQLError(error: GraphQLError | any): error is GraphQLError {
return error.extensions !== undefined;
}
function normalizeErrorMessage(message: GraphQLError['message']): string {
if (typeof message === 'object') {
if (message.error != null) {
return message.error;
}
return 'Unknown error: ' + JSON.stringify(message);
}
return message;
}
function normalizeError(error: GraphQLError | Error): Error {
if (isGraphQLError(error)) {
return {
...error,
message: normalizeErrorMessage(error.message),
};
}
return error;
}
function translateErrorMessage(messageOrCode: string, translations: Record<string, string>): string {
return translations[messageOrCode] ?? messageOrCode;
}
export interface ApolloErrorProcessorOptions<TApp = Vue, TContext = ApolloOperationContext> {
app?: TApp;
context?: TContext;
isUnauthorizedError?: (error: GraphQLError) => boolean;
onUnauthorizedError?: (error: UnauthorizedError) => boolean | void;
onInputValidationError?: (error: InputValidationError) => boolean | void;
onServerError?: (error: ServerError) => boolean | void;
onNetworkError?: (error: NetworkError) => boolean | void;
translations?: Record<string, string>;
}
export const defaultErrorMessageTranslations: Record<string, string> = {
FAILED_TO_FETCH:
'Unable to communicate with server. The service may be down or you may be offline. Try again in a moment.',
INTERNAL_SERVER_ERROR: `A server error has occurred.`,
};
const defaultProcessorOptions: ApolloErrorProcessorOptions = {
isUnauthorizedError: (error: GraphQLError) =>
error.message === 'Unauthorized' ||
error.extensions?.code === 'FORBIDDEN' ||
error.extensions?.exception?.status === 401,
translations: defaultErrorMessageTranslations,
};
export interface ApolloErrorProcessorResult {
unhandledErrors: ProcessedApolloError[];
handledErrors: ProcessedApolloError[];
}
function processGraphQLError(
error: GraphQLError,
options: ApolloErrorProcessorOptions = {},
): ApolloErrorProcessorResult {
options = { ...defaultProcessorOptions, ...options };
const { isUnauthorizedError, onUnauthorizedError, onInputValidationError, onServerError, translations } = options;
if (isUnauthorizedError != null && isUnauthorizedError(error)) {
// Unauthorized (not logged in, or not allowed) error
const processedError: UnauthorizedError = {
type: ApolloErrorType.UNAUTHORIZED_ERROR,
error: normalizeError(error),
message: normalizeErrorMessage(error.message),
path: error.path,
};
if (onUnauthorizedError != null && onUnauthorizedError(processedError)) {
return { unhandledErrors: [], handledErrors: [processedError] };
}
return { unhandledErrors: [processedError], handledErrors: [] };
}
if (error.extensions?.validationErrors != null) {
// User input validation error
const processedError: InputValidationError = {
type: ApolloErrorType.INPUT_VALIDATION_ERROR,
error: normalizeError(error),
message: normalizeErrorMessage(error.message),
path: error.path,
invalidArgs: error.extensions.invalidArgs,
violations: error.extensions.validationErrors,
};
if (onInputValidationError != null && onInputValidationError(processedError)) {
return { unhandledErrors: [], handledErrors: [processedError] };
}
return { unhandledErrors: [processedError], handledErrors: [] };
}
// Other GraphQL resolver error - probably a bug
const processedError: ServerError = {
type: error.extensions?.code != null ? error.extensions.code : ApolloErrorType.SERVER_ERROR,
error: normalizeError(error),
message: translateErrorMessage('INTERNAL_SERVER_ERROR', translations ?? defaultErrorMessageTranslations),
path: error.path,
};
if (onServerError != null && onServerError(processedError)) {
return { unhandledErrors: [], handledErrors: [processedError] };
}
return { unhandledErrors: [processedError], handledErrors: [] };
}
function processNetworkError(
error: ApolloError,
options: ApolloErrorProcessorOptions = {},
): ApolloErrorProcessorResult {
options = { ...defaultProcessorOptions, ...options };
const { onNetworkError, translations } = options;
let message: string =
error.networkError != null && error.networkError.message != null
? normalizeErrorMessage(error.networkError.message)
: normalizeErrorMessage(error.message);
switch (message) {
case 'Failed to fetch':
message = translateErrorMessage('FAILED_TO_FETCH', translations ?? defaultErrorMessageTranslations);
break;
}
const processedError: NetworkError = {
type: ApolloErrorType.NETWORK_ERROR,
error,
statusCode: error.networkError != null ? error.networkError.statusCode : undefined,
message,
};
if (onNetworkError != null && onNetworkError(processedError)) {
return { unhandledErrors: [], handledErrors: [processedError] };
}
return { unhandledErrors: [processedError], handledErrors: [processedError] };
}
export function processApolloError(
error: ApolloError,
options: ApolloErrorProcessorOptions = {},
): ApolloErrorProcessorResult {
options = { ...defaultProcessorOptions, ...options };
const { onServerError } = options;
if (error.graphQLErrors != null && error.graphQLErrors.length > 0) {
// Successful request but with errors from the resolver
const errorProcessorResults = error.graphQLErrors.map(graphQLError => processGraphQLError(graphQLError, options));
return {
unhandledErrors: errorProcessorResults.flatMap(result => result.unhandledErrors),
handledErrors: errorProcessorResults.flatMap(result => result.handledErrors),
};
}
if (error.networkError?.result?.errors != null && error.networkError.result.errors.length > 0) {
// Network error that contains GraphQL errors inside it. Can occur when server responds with a non-200 status code
const errorProcessorResults = error.networkError.result.errors.map(graphQLError =>
processGraphQLError(graphQLError, options),
);
return {
unhandledErrors: errorProcessorResults.flatMap(result => result.unhandledErrors),
handledErrors: errorProcessorResults.flatMap(result => result.handledErrors),
};
}
if (error.networkError != null) {
// Network error, e.g. server is not responding or some other exception occurs
return processNetworkError(error, options);
}
// Some other internal server error
const processedError: ServerError = {
type: ApolloErrorType.SERVER_ERROR,
error,
message: error.message,
};
if (onServerError != null && onServerError(processedError)) {
return { unhandledErrors: [], handledErrors: [processedError] };
}
return { unhandledErrors: [processedError], handledErrors: [] };
}
+1 -1
View File
@@ -113,7 +113,7 @@ export async function mutateWithErrorHandling<
return {
success: false,
errors: errorHandlerResult?.processedErrors,
errors: errorHandlerResult?.allErrors,
validationRuleViolations: errorHandlerResult?.validationRuleViolations,
};
}