Openstatus www.openstatus.dev
at 4c0f4c00a38753a5d0dfd7e7b7b7706dec6f1503 158 lines 4.1 kB view raw
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>>;