Openstatus
www.openstatus.dev
1// Props to Unkey: https://github.com/unkeyed/unkey/blob/main/apps/api/src/pkg/errors/http.ts
2import type { Context } from "hono";
3import { HTTPException } from "hono/http-exception";
4
5import type { ErrorCode } from "@openstatus/error";
6import {
7 ErrorCodes,
8 SchemaError,
9 codeToStatus,
10 statusToCode,
11} from "@openstatus/error";
12
13import { z } from "@hono/zod-openapi";
14import { getLogger } from "@logtape/logtape";
15import { ZodError } from "zod";
16
17const logger = getLogger("api-server");
18export class OpenStatusApiError extends HTTPException {
19 public readonly code: ErrorCode;
20
21 constructor({
22 code,
23 message,
24 }: {
25 code: ErrorCode;
26 message: HTTPException["message"];
27 }) {
28 const status = codeToStatus(code);
29 super(status, { message });
30 this.code = code;
31 }
32}
33
34export function handleError(err: Error, c: Context): Response {
35 if (err instanceof ZodError) {
36 const error = SchemaError.fromZod(err, c);
37
38 // If the error is a client error, we disable Sentry
39 c.get("sentry").setEnabled(false);
40
41 return c.json<ErrorSchema>(
42 {
43 code: "BAD_REQUEST",
44 message: error.message,
45 docs: "https://docs.openstatus.dev/api-references/errors/code/BAD_REQUEST",
46 requestId: c.get("requestId"),
47 },
48 { status: 400 },
49 );
50 }
51
52 /**
53 * This is a custom error that we throw in our code so we can handle it
54 */
55 if (err instanceof OpenStatusApiError) {
56 const code = statusToCode(err.status);
57
58 // If the error is a client error, we disable Sentry
59 if (err.status < 499) {
60 c.get("sentry").setEnabled(false);
61 }
62
63 return c.json<ErrorSchema>(
64 {
65 code: code,
66 message: err.message,
67 docs: `https://docs.openstatus.dev/api-references/errors/code/${code}`,
68 requestId: c.get("requestId"),
69 },
70 { status: err.status },
71 );
72 }
73
74 if (err instanceof HTTPException) {
75 const code = statusToCode(err.status);
76 return c.json<ErrorSchema>(
77 {
78 code: code,
79 message: err.message,
80 docs: `https://docs.openstatus.dev/api-references/errors/code/${code}`,
81 requestId: c.get("requestId"),
82 },
83 { status: err.status },
84 );
85 }
86
87 logger.error("Request error", {
88 error: {
89 name: err.name,
90 message: err.message,
91 stack: err.stack,
92 },
93 method: c.req.method,
94 url: c.req.url,
95 });
96 c.get("sentry").captureException(err);
97
98 return c.json<ErrorSchema>(
99 {
100 code: "INTERNAL_SERVER_ERROR",
101 message: err.message ?? "Something went wrong",
102 docs: "https://docs.openstatus.dev/api-references/errors/code/INTERNAL_SERVER_ERROR",
103 requestId: c.get("requestId"),
104 },
105
106 { status: 500 },
107 );
108}
109
110export function handleZodError(
111 result:
112 | {
113 success: true;
114 data: unknown;
115 }
116 | {
117 success: false;
118 error: ZodError;
119 },
120 c: Context,
121) {
122 if (!result.success) {
123 const error = SchemaError.fromZod(result.error, c);
124 return c.json<z.infer<ReturnType<typeof createErrorSchema>>>(
125 {
126 code: "BAD_REQUEST",
127 docs: "https://docs.openstatus.dev/api-references/errors/code/BAD_REQUEST",
128 message: error.message,
129 requestId: c.get("requestId"),
130 },
131 { status: 400 },
132 );
133 }
134}
135
136export function createErrorSchema(code: ErrorCode) {
137 return z.object({
138 code: z.enum(ErrorCodes).openapi({
139 example: code,
140 description: "The error code related to the status code.",
141 }),
142 message: z.string().openapi({
143 description: "A human readable message describing the issue.",
144 example: "<string>",
145 }),
146 docs: z.string().openapi({
147 description: "A link to the documentation for the error.",
148 example: `https://docs.openstatus.dev/api-references/errors/code/${code}`,
149 }),
150 requestId: z.string().openapi({
151 description:
152 "The request id to be used for debugging and error reporting.",
153 example: "<uuid>",
154 }),
155 });
156}
157
158export type ErrorSchema = z.infer<ReturnType<typeof createErrorSchema>>;