import type { Context } from "hono"; import type { AtpAgent } from "@atproto/api"; import type { AppContext } from "./app-context.js"; import type { Logger } from "@atbb/logger"; import { isProgrammingError, isNetworkError, isDatabaseError } from "./errors.js"; /** * Structured context for error logging in route handlers. */ interface ErrorContext { operation: string; logger: Logger; [key: string]: unknown; } /** * Format an error for structured logging. * Extracts message and optionally stack from Error instances. */ function formatError(error: unknown): { message: string; stack?: string } { if (error instanceof Error) { return { message: error.message, stack: error.stack }; } return { message: String(error) }; } /** * Handle errors in route handlers and security checks. * * 1. Re-throws programming errors (TypeError, ReferenceError, SyntaxError) * 2. Logs the error with structured context * 3. Classifies the error and returns the appropriate HTTP status: * - 503 for network errors (external service unreachable, user should retry) * - 503 for database errors (temporary, user should retry) * - 500 for unexpected errors (server bug, needs investigation) * * @param c - Hono context * @param error - The caught error * @param userMessage - User-facing error message (e.g., "Failed to retrieve forum metadata") * @param ctx - Structured logging context */ export function handleRouteError( c: Context, error: unknown, userMessage: string, ctx: ErrorContext ): Response { if (isProgrammingError(error)) { const { message, stack } = formatError(error); ctx.logger.error(`CRITICAL: Programming error in ${ctx.operation}`, { ...ctx, error: message, stack, }); throw error; } ctx.logger.error(userMessage, { ...ctx, error: formatError(error).message, }); if (error instanceof Error && isNetworkError(error)) { return c.json( { error: "Unable to reach external service. Please try again later." }, 503 ) as unknown as Response; } if (error instanceof Error && isDatabaseError(error)) { return c.json( { error: "Database temporarily unavailable. Please try again later." }, 503 ) as unknown as Response; } return c.json( { error: `${userMessage}. Please contact support if this persists.` }, 500 ) as unknown as Response; } /** * Get the ForumAgent's authenticated AtpAgent, or return an error response. * * Checks both that ForumAgent exists (server configuration) and that it's * authenticated (has valid credentials). Returns appropriate error responses: * - 500 if ForumAgent is not configured (server misconfiguration) * - 503 if ForumAgent is not authenticated (temporary, should retry) * * Usage: * ```typescript * const { agent, error } = getForumAgentOrError(ctx, c, "POST /api/mod/ban"); * if (error) return error; * // agent is guaranteed to be non-null here * ``` */ export function getForumAgentOrError( appCtx: AppContext, c: Context, operation: string ): { agent: AtpAgent; error: null } | { agent: null; error: Response } { if (!appCtx.forumAgent) { appCtx.logger.error("CRITICAL: ForumAgent not available", { operation, forumDid: appCtx.config.forumDid, }); return { agent: null, error: c.json({ error: "Forum agent not available. Server configuration issue.", }, 500) as unknown as Response, }; } const agent = appCtx.forumAgent.getAgent(); if (!agent) { appCtx.logger.error("ForumAgent not authenticated", { operation, forumDid: appCtx.config.forumDid, }); return { agent: null, error: c.json({ error: "Forum agent not authenticated. Please try again later.", }, 503) as unknown as Response, }; } return { agent, error: null }; } /** * Parse JSON body and return 400 if malformed. * * Returns `{ body, error }` where: * - On success: `{ body: parsedObject, error: null }` * - On failure: `{ body: null, error: Response }` — caller should `return error` * * Usage: * ```typescript * const { body, error } = await safeParseJsonBody(c); * if (error) return error; * const { text, boardUri } = body; * ``` */ export async function safeParseJsonBody( c: Context ): Promise<{ body: any; error: null } | { body: null; error: Response }> { try { const body = await c.req.json(); return { body, error: null }; } catch (error) { // Only SyntaxError is expected here (malformed JSON from user input). // Re-throw anything unexpected so programming bugs are not silently swallowed. if (!(error instanceof SyntaxError)) throw error; return { body: null, error: c.json({ error: "Invalid JSON in request body" }, 400) as unknown as Response, }; } }