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 { 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;
|
||||||
|
@@ -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",
|
||||||
|
@@ -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();
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user