Add error handling

This commit is contained in:
KMKoushik
2024-04-22 14:10:34 +10:00
parent 493e9fb63a
commit 66732d2345
6 changed files with 165 additions and 5 deletions

View File

@@ -0,0 +1,147 @@
import { Context } from "hono";
import { HTTPException } from "hono/http-exception";
import { StatusCode } from "hono/utils/http-status";
import { z } from "zod";
const ErrorCode = z.enum([
"BAD_REQUEST",
"FORBIDDEN",
"INTERNAL_SERVER_ERROR",
"USAGE_EXCEEDED",
"DISABLED",
"NOT_FOUND",
"NOT_UNIQUE",
"RATE_LIMITED",
"UNAUTHORIZED",
"PRECONDITION_FAILED",
"INSUFFICIENT_PERMISSIONS",
"METHOD_NOT_ALLOWED",
]);
function codeToStatus(code: z.infer<typeof ErrorCode>): StatusCode {
switch (code) {
case "BAD_REQUEST":
return 400;
case "UNAUTHORIZED":
return 401;
case "FORBIDDEN":
case "DISABLED":
case "INSUFFICIENT_PERMISSIONS":
case "USAGE_EXCEEDED":
return 403;
case "NOT_FOUND":
return 404;
case "METHOD_NOT_ALLOWED":
return 405;
case "NOT_UNIQUE":
return 409;
case "PRECONDITION_FAILED":
return 412;
case "RATE_LIMITED":
return 429;
case "INTERNAL_SERVER_ERROR":
return 500;
}
}
function statusToCode(status: StatusCode): z.infer<typeof ErrorCode> {
switch (status) {
case 400:
return "BAD_REQUEST";
case 401:
return "UNAUTHORIZED";
case 403:
return "FORBIDDEN";
case 404:
return "NOT_FOUND";
case 405:
return "METHOD_NOT_ALLOWED";
case 500:
return "INTERNAL_SERVER_ERROR";
default:
return "INTERNAL_SERVER_ERROR";
}
}
export class UnsendApiError extends HTTPException {
public readonly code: z.infer<typeof ErrorCode>;
constructor({
code,
message,
}: {
code: z.infer<typeof ErrorCode>;
message: string;
}) {
super(codeToStatus(code), { message });
this.code = code;
}
}
export function handleError(err: Error, c: Context): Response {
/**
* We can handle this very well, as it is something we threw ourselves
*/
if (err instanceof UnsendApiError) {
if (err.status >= 500) {
console.error(err.message, {
name: err.name,
code: err.code,
status: err.status,
});
}
return c.json(
{
error: {
code: err.code,
message: err.message,
},
},
{ status: err.status }
);
}
/**
* HTTPExceptions from hono at least give us some idea of what to do as they provide a status and
* message
*/
if (err instanceof HTTPException) {
if (err.status >= 500) {
console.error("HTTPException", {
message: err.message,
status: err.status,
});
}
const code = statusToCode(err.status);
return c.json(
{
error: {
code,
message: err.message,
},
},
{ status: err.status }
);
}
/**
* We're lost here, all we can do is return a 500 and log it to investigate
*/
console.error("unhandled exception", {
name: err.name,
message: err.message,
cause: err.cause,
stack: err.stack,
});
return c.json(
{
error: {
code: "INTERNAL_SERVER_ERROR",
message: "something unexpected happened",
},
},
{ status: 500 }
);
}

View File

@@ -1,15 +1,22 @@
import { Context } from "hono"; import { Context } from "hono";
import { hashToken } from "../auth"; import { hashToken } from "../auth";
import { db } from "../db"; import { db } from "../db";
import { UnsendApiError } from "./api-error";
export const getTeamFromToken = async (c: Context) => { export const getTeamFromToken = async (c: Context) => {
const authHeader = c.req.header("Authorization"); const authHeader = c.req.header("Authorization");
if (!authHeader) { if (!authHeader) {
throw new Error("No Authorization header provided"); throw new UnsendApiError({
code: "UNAUTHORIZED",
message: "No Authorization header provided",
});
} }
const token = authHeader.split(" ")[1]; // Assuming the Authorization header is in the format "Bearer <token>" const token = authHeader.split(" ")[1]; // Assuming the Authorization header is in the format "Bearer <token>"
if (!token) { if (!token) {
throw new Error("No bearer token provided"); throw new UnsendApiError({
code: "UNAUTHORIZED",
message: "No Authorization header provided",
});
} }
const hashedToken = hashToken(token); const hashedToken = hashToken(token);
@@ -25,7 +32,10 @@ export const getTeamFromToken = async (c: Context) => {
}); });
if (!team) { if (!team) {
throw new Error("No team found for this token"); throw new UnsendApiError({
code: "FORBIDDEN",
message: "Invalid API token",
});
} }
return team; return team;

View File

@@ -1,9 +1,12 @@
import { OpenAPIHono } from "@hono/zod-openapi"; import { OpenAPIHono } from "@hono/zod-openapi";
import { swaggerUI } from "@hono/swagger-ui"; import { swaggerUI } from "@hono/swagger-ui";
import { handleError } from "./api-error";
export function getApp() { export function getApp() {
const app = new OpenAPIHono().basePath("/api"); const app = new OpenAPIHono().basePath("/api");
app.onError(handleError);
// The OpenAPI documentation will be available at /doc // The OpenAPI documentation will be available at /doc
app.doc("/v1/doc", (c) => ({ app.doc("/v1/doc", (c) => ({
openapi: "3.0.0", openapi: "3.0.0",

View File

@@ -1,6 +1,6 @@
import { getApp } from "./hono"; import { getApp } from "./hono";
import getDomains from "./api/get_domains"; import getDomains from "./api/get-domains";
import sendEmail from "./api/send_email"; import sendEmail from "./api/send-email";
export const app = getApp(); export const app = getApp();