Add error handling
This commit is contained in:
147
apps/web/src/server/public-api/api-error.ts
Normal file
147
apps/web/src/server/public-api/api-error.ts
Normal 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 }
|
||||
);
|
||||
}
|
@@ -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 <token>"
|
||||
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;
|
||||
|
@@ -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",
|
||||
|
@@ -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();
|
||||
|
||||
|
Reference in New Issue
Block a user