Skip to content

Commit

Permalink
fix: Retain status codes when combining errors
Browse files Browse the repository at this point in the history
  • Loading branch information
joachimvh committed Oct 2, 2020
1 parent 7a41108 commit 10723bb
Show file tree
Hide file tree
Showing 4 changed files with 71 additions and 7 deletions.
22 changes: 18 additions & 4 deletions src/util/CompositeAsyncHandler.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import type { AsyncHandler } from './AsyncHandler';
import { HttpError } from './errors/HttpError';
import { InternalServerError } from './errors/InternalServerError';
import { UnsupportedHttpError } from './errors/UnsupportedHttpError';

/**
Expand Down Expand Up @@ -70,24 +72,36 @@ export class CompositeAsyncHandler<TIn, TOut> implements AsyncHandler<TIn, TOut>
* @returns A promise resolving to a handler that supports the data or otherwise rejecting.
*/
private async findHandler(input: TIn): Promise<AsyncHandler<TIn, TOut>> {
const errors: Error[] = [];
const errors: HttpError[] = [];

for (const handler of this.handlers) {
try {
await handler.canHandle(input);

return handler;
} catch (error: unknown) {
if (error instanceof Error) {
if (error instanceof HttpError) {
errors.push(error);
} else if (error instanceof Error) {
errors.push(new InternalServerError(error.message));
} else {
errors.push(new Error('Unknown error.'));
errors.push(new InternalServerError('Unknown error.'));
}
}
}

const joined = errors.map((error: Error): string => error.message).join(', ');
const message = `No handler supports the given input: [${joined}].`;

throw new UnsupportedHttpError(`No handler supports the given input: [${joined}].`);
// Check if all errors have the same status code
if (errors.every((error): boolean => error.statusCode === errors[0].statusCode)) {
throw new HttpError(errors[0].statusCode, errors[0].name, message);
}

// Find the error range (4xx or 5xx)
if (errors.some((error): boolean => error.statusCode >= 500)) {
throw new InternalServerError(message);
}
throw new UnsupportedHttpError(message);
}
}
6 changes: 3 additions & 3 deletions src/util/errors/HttpError.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
/**
* An abstract class for all errors that could be thrown by Solid.
* A class for all errors that could be thrown by Solid.
* All errors inheriting from this should fix the status code thereby hiding the HTTP internals from other components.
*/
export abstract class HttpError extends Error {
export class HttpError extends Error {
public statusCode: number;

/**
Expand All @@ -11,7 +11,7 @@ export abstract class HttpError extends Error {
* @param name - Error name. Useful for logging and stack tracing.
* @param message - Message to be thrown.
*/
protected constructor(statusCode: number, name: string, message?: string) {
public constructor(statusCode: number, name: string, message?: string) {
super(message);
this.statusCode = statusCode;
this.name = name;
Expand Down
9 changes: 9 additions & 0 deletions src/util/errors/InternalServerError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { HttpError } from './HttpError';
/**
* A generic error message, given when an unexpected condition was encountered and no more specific message is suitable.
*/
export class InternalServerError extends HttpError {
public constructor(message?: string) {
super(500, 'InternalServerError', message);
}
}
41 changes: 41 additions & 0 deletions test/unit/util/CompositeAsyncHandler.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import type { AsyncHandler } from '../../../src/util/AsyncHandler';
import { CompositeAsyncHandler } from '../../../src/util/CompositeAsyncHandler';
import { HttpError } from '../../../src/util/errors/HttpError';
import { UnsupportedHttpError } from '../../../src/util/errors/UnsupportedHttpError';
import { StaticAsyncHandler } from '../../util/StaticAsyncHandler';

describe('A CompositeAsyncHandler', (): void => {
Expand Down Expand Up @@ -81,5 +83,44 @@ describe('A CompositeAsyncHandler', (): void => {

await expect(handler.handleSafe(null)).rejects.toThrow('[Not supported., Not supported.]');
});

it('throws an error with matching status code if all handlers threw the same.', async(): Promise<void> => {
handlerTrue.canHandle = async(): Promise<void> => {
throw new HttpError(401, 'UnauthorizedHttpError');
};
const handler = new CompositeAsyncHandler([ handlerTrue, handlerTrue ]);

await expect(handler.canHandle(null)).rejects.toMatchObject({
statusCode: 401,
name: 'UnauthorizedHttpError',
});
});

it('throws an internal server error if one of the handlers threw one.', async(): Promise<void> => {
handlerTrue.canHandle = async(): Promise<void> => {
throw new HttpError(401, 'UnauthorizedHttpError');
};
handlerFalse.canHandle = async(): Promise<void> => {
throw new Error('Server is crashing!');
};
const handler = new CompositeAsyncHandler([ handlerTrue, handlerFalse ]);

await expect(handler.canHandle(null)).rejects.toMatchObject({
statusCode: 500,
name: 'InternalServerError',
});
});

it('throws an UnsupportedHttpError if handlers throw different errors.', async(): Promise<void> => {
handlerTrue.canHandle = async(): Promise<void> => {
throw new HttpError(401, 'UnauthorizedHttpError');
};
handlerFalse.canHandle = async(): Promise<void> => {
throw new HttpError(415, 'UnsupportedMediaTypeHttpError');
};
const handler = new CompositeAsyncHandler([ handlerTrue, handlerFalse ]);

await expect(handler.canHandle(null)).rejects.toThrow(UnsupportedHttpError);
});
});
});

0 comments on commit 10723bb

Please sign in to comment.