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:
@@ -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[] {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
|
||||
export * from './ApolloErrorHandlerResult';
|
||||
export * from './ApolloErrorProcessor';
|
||||
export * from './handleApolloError';
|
||||
export * from './processApolloError';
|
||||
export * from './types';
|
||||
|
||||
@@ -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
@@ -113,7 +113,7 @@ export async function mutateWithErrorHandling<
|
||||
|
||||
return {
|
||||
success: false,
|
||||
errors: errorHandlerResult?.processedErrors,
|
||||
errors: errorHandlerResult?.allErrors,
|
||||
validationRuleViolations: errorHandlerResult?.validationRuleViolations,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user