Openstatus
www.openstatus.dev
1import { Code, ConnectError, type Interceptor } from "@connectrpc/connect";
2import { getLogger } from "@logtape/logtape";
3import type { ErrorCode } from "@openstatus/error";
4
5import { OpenStatusApiError } from "@/libs/errors";
6import { RPC_CONTEXT_KEY } from "./auth";
7
8const logger = getLogger("api-server");
9
10/**
11 * Mapping from OpenStatus error codes to ConnectRPC codes.
12 */
13const ERROR_CODE_MAP: Record<ErrorCode, Code> = {
14 BAD_REQUEST: Code.InvalidArgument,
15 UNAUTHORIZED: Code.Unauthenticated,
16 PAYMENT_REQUIRED: Code.ResourceExhausted,
17 FORBIDDEN: Code.PermissionDenied,
18 NOT_FOUND: Code.NotFound,
19 METHOD_NOT_ALLOWED: Code.Unimplemented,
20 CONFLICT: Code.AlreadyExists,
21 UNPROCESSABLE_ENTITY: Code.InvalidArgument,
22 INTERNAL_SERVER_ERROR: Code.Internal,
23};
24
25/**
26 * Error mapping interceptor for ConnectRPC.
27 * Converts OpenStatusApiError to ConnectError with appropriate codes.
28 * Logs server errors and passes through client errors.
29 */
30export function errorInterceptor(): Interceptor {
31 return (next) => async (req) => {
32 try {
33 return await next(req);
34 } catch (error) {
35 const rpcCtx = req.contextValues.get(RPC_CONTEXT_KEY);
36
37 // Already a ConnectError, pass through
38 if (error instanceof ConnectError) {
39 throw error;
40 }
41
42 // Map OpenStatusApiError to ConnectError
43 if (error instanceof OpenStatusApiError) {
44 const code = ERROR_CODE_MAP[error.code] ?? Code.Internal;
45
46 // Log server errors (5xx equivalent)
47 if (error.status >= 500) {
48 logger.error("RPC server error", {
49 error: {
50 code: error.code,
51 message: error.message,
52 },
53 requestId: rpcCtx?.requestId,
54 });
55 }
56
57 throw new ConnectError(error.message, code);
58 }
59
60 // Unknown error - log and wrap as Internal
61 logger.error("RPC unexpected error", {
62 error: {
63 name: error instanceof Error ? error.name : "Unknown",
64 message: error instanceof Error ? error.message : String(error),
65 stack: error instanceof Error ? error.stack : undefined,
66 },
67 requestId: rpcCtx?.requestId,
68 });
69
70 throw new ConnectError(
71 error instanceof Error ? error.message : "Internal server error",
72 Code.Internal,
73 );
74 }
75 };
76}