From 66732d2345b70c5f71bc27281f85aea9480c7d92 Mon Sep 17 00:00:00 2001 From: KMKoushik Date: Mon, 22 Apr 2024 14:10:34 +1000 Subject: [PATCH] Add error handling --- apps/web/src/server/public-api/api-error.ts | 147 ++++++++++++++++++ .../api/{get_domains.ts => get-domains.ts} | 0 .../api/{send_email.ts => send-email.ts} | 0 apps/web/src/server/public-api/auth.ts | 16 +- apps/web/src/server/public-api/hono.ts | 3 + apps/web/src/server/public-api/index.ts | 4 +- 6 files changed, 165 insertions(+), 5 deletions(-) create mode 100644 apps/web/src/server/public-api/api-error.ts rename apps/web/src/server/public-api/api/{get_domains.ts => get-domains.ts} (100%) rename apps/web/src/server/public-api/api/{send_email.ts => send-email.ts} (100%) diff --git a/apps/web/src/server/public-api/api-error.ts b/apps/web/src/server/public-api/api-error.ts new file mode 100644 index 0000000..08095ec --- /dev/null +++ b/apps/web/src/server/public-api/api-error.ts @@ -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): 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 { + 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; + + constructor({ + code, + message, + }: { + code: z.infer; + 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 } + ); +} diff --git a/apps/web/src/server/public-api/api/get_domains.ts b/apps/web/src/server/public-api/api/get-domains.ts similarity index 100% rename from apps/web/src/server/public-api/api/get_domains.ts rename to apps/web/src/server/public-api/api/get-domains.ts diff --git a/apps/web/src/server/public-api/api/send_email.ts b/apps/web/src/server/public-api/api/send-email.ts similarity index 100% rename from apps/web/src/server/public-api/api/send_email.ts rename to apps/web/src/server/public-api/api/send-email.ts diff --git a/apps/web/src/server/public-api/auth.ts b/apps/web/src/server/public-api/auth.ts index 8d8951a..9f28d8d 100644 --- a/apps/web/src/server/public-api/auth.ts +++ b/apps/web/src/server/public-api/auth.ts @@ -1,15 +1,22 @@ import { Context } from "hono"; import { hashToken } from "../auth"; import { db } from "../db"; +import { UnsendApiError } from "./api-error"; export const getTeamFromToken = async (c: Context) => { const authHeader = c.req.header("Authorization"); 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 " if (!token) { - throw new Error("No bearer token provided"); + throw new UnsendApiError({ + code: "UNAUTHORIZED", + message: "No Authorization header provided", + }); } const hashedToken = hashToken(token); @@ -25,7 +32,10 @@ export const getTeamFromToken = async (c: Context) => { }); if (!team) { - throw new Error("No team found for this token"); + throw new UnsendApiError({ + code: "FORBIDDEN", + message: "Invalid API token", + }); } return team; diff --git a/apps/web/src/server/public-api/hono.ts b/apps/web/src/server/public-api/hono.ts index 93edb70..134efb8 100644 --- a/apps/web/src/server/public-api/hono.ts +++ b/apps/web/src/server/public-api/hono.ts @@ -1,9 +1,12 @@ import { OpenAPIHono } from "@hono/zod-openapi"; import { swaggerUI } from "@hono/swagger-ui"; +import { handleError } from "./api-error"; export function getApp() { const app = new OpenAPIHono().basePath("/api"); + app.onError(handleError); + // The OpenAPI documentation will be available at /doc app.doc("/v1/doc", (c) => ({ openapi: "3.0.0", diff --git a/apps/web/src/server/public-api/index.ts b/apps/web/src/server/public-api/index.ts index 1bbd431..4fc662c 100644 --- a/apps/web/src/server/public-api/index.ts +++ b/apps/web/src/server/public-api/index.ts @@ -1,6 +1,6 @@ import { getApp } from "./hono"; -import getDomains from "./api/get_domains"; -import sendEmail from "./api/send_email"; +import getDomains from "./api/get-domains"; +import sendEmail from "./api/send-email"; export const app = getApp();