lukeautry/tsoa

How can I catch errors by their status code?

dgreene1 opened this issue · 4 comments

Figured out another thing that I'd like to share with the community.

Goal: You want to try/catch the results of your entire app, and if the error that's thrown has an http status code, then you want to return that to the user so they have more information. This is a common pattern due to the use of Boom

Here's how I wrote it:

import * as Router from 'koa-router';
import * as httpStatusCodes from 'http-status-codes';
import { AxiosError } from 'axios';
import * as createHttpError from 'http-errors';
import * as Boom from '@hapi/boom';
import { assertUnreachable } from '../agnosticUtilities/assertUnreachable';

function isAxiosError(input: unknown & object): input is AxiosError {
	const innocentUntilGuilty = input as Partial<AxiosError>;
	return !!innocentUntilGuilty.stack && !!innocentUntilGuilty.code && typeof innocentUntilGuilty.code === 'string';
}

function isHttpError(input: unknown & object): input is createHttpError.HttpError {
	const innocentUntilGuilty = input as Partial<createHttpError.HttpError>;
	return (
		!!innocentUntilGuilty.stack &&
		!!innocentUntilGuilty.statusCode &&
		typeof innocentUntilGuilty.statusCode === 'number'
	);
}

const grabStatusCodeFromError = (error: Error | AxiosError | createHttpError.HttpError | Boom): number | undefined => {
	// ######################
	// IMPORTANT NOTE: Boom must be first in the check since some libraries (like tsoa) automatically set the ".code" to 500 if there wasn't one.
	//      So if .code is the first thing you check, then you'd never arrive at .output.statusCode
	// ######################
	if (Boom.isBoom(error)) {
		return error.output.statusCode;
	}
	if (isHttpError(error)) {
		return error.statusCode;
	}
	if (isAxiosError(error)) {
		return error.code ? parseInt(error.code) : undefined;
	}
	if (error instanceof Error) {
		return undefined; // since standard errors don't have http status codes
	} else {
		return assertUnreachable(error, 'We got some kind of error that we do not have a handler for');
	}
};

export const errorResponder: Router.IMiddleware = async (ctx, next) => {
	try {
		await next();
	} catch (err) {
		const expose = process.env.EXPOSE_STACK;
		// tslint:disable-next-line: no-unsafe-any
		const statusCode = grabStatusCodeFromError(err) || httpStatusCodes.INTERNAL_SERVER_ERROR;

		ctx.status = statusCode;
		ctx.body = {
			// tslint:disable-next-line: no-unsafe-any
			message: err.message || 'An error occurred',
			// tslint:disable-next-line: no-unsafe-any
			correlationId: ctx.state.correlationId,
			// tslint:disable-next-line: no-unsafe-any
			stack: expose ? err.stack : undefined,
		};
	}
};

And then in your index.ts just remember to "install" the middleware:

app.use(errorResponder)

Note: just remember to add the middleware before the routes are added (as I describe in #381)

k, so here's how you would throw a 404 now that you have this middleware in place that supports Boom errors:

@Get('{id}')
public async getThingById(id: string, @Request() request: koa.Request): Promise<IThing> {
	const theThing = myDb.findById(id);
        if(!theThing){
        	throw Boom.notFound(`Could not find a resource by id ${id}`)
        }
        return theThing;
}

What about with Express?

In express I get:
TypeError: Boom.badRequest is not a function

If I try to throw a Boom error after registering an express-boom handler with the server.

In express I get:
TypeError: Boom.badRequest is not a function

If I try to throw a Boom error after registering an express-boom handler with the server.

Please take that question to either StackOverflow or to the Boom github repo. It’s not tsoa related.

ok np.