WIP! A BB-style forum, on the ATmosphere! We're still working... we'll be back soon when we have something to show off!
node typescript hono htmx atproto
at root/atb-56-theme-caching-layer 162 lines 4.8 kB view raw
1import type { Context } from "hono"; 2import type { AtpAgent } from "@atproto/api"; 3import type { AppContext } from "./app-context.js"; 4import type { Logger } from "@atbb/logger"; 5import { isProgrammingError, isNetworkError, isDatabaseError } from "./errors.js"; 6 7/** 8 * Structured context for error logging in route handlers. 9 */ 10interface ErrorContext { 11 operation: string; 12 logger: Logger; 13 [key: string]: unknown; 14} 15 16/** 17 * Format an error for structured logging. 18 * Extracts message and optionally stack from Error instances. 19 */ 20function formatError(error: unknown): { message: string; stack?: string } { 21 if (error instanceof Error) { 22 return { message: error.message, stack: error.stack }; 23 } 24 return { message: String(error) }; 25} 26 27/** 28 * Handle errors in route handlers and security checks. 29 * 30 * 1. Re-throws programming errors (TypeError, ReferenceError, SyntaxError) 31 * 2. Logs the error with structured context 32 * 3. Classifies the error and returns the appropriate HTTP status: 33 * - 503 for network errors (external service unreachable, user should retry) 34 * - 503 for database errors (temporary, user should retry) 35 * - 500 for unexpected errors (server bug, needs investigation) 36 * 37 * @param c - Hono context 38 * @param error - The caught error 39 * @param userMessage - User-facing error message (e.g., "Failed to retrieve forum metadata") 40 * @param ctx - Structured logging context 41 */ 42export function handleRouteError( 43 c: Context, 44 error: unknown, 45 userMessage: string, 46 ctx: ErrorContext 47): Response { 48 if (isProgrammingError(error)) { 49 const { message, stack } = formatError(error); 50 ctx.logger.error(`CRITICAL: Programming error in ${ctx.operation}`, { 51 ...ctx, 52 error: message, 53 stack, 54 }); 55 throw error; 56 } 57 58 ctx.logger.error(userMessage, { 59 ...ctx, 60 error: formatError(error).message, 61 }); 62 63 if (error instanceof Error && isNetworkError(error)) { 64 return c.json( 65 { error: "Unable to reach external service. Please try again later." }, 66 503 67 ) as unknown as Response; 68 } 69 70 if (error instanceof Error && isDatabaseError(error)) { 71 return c.json( 72 { error: "Database temporarily unavailable. Please try again later." }, 73 503 74 ) as unknown as Response; 75 } 76 77 return c.json( 78 { error: `${userMessage}. Please contact support if this persists.` }, 79 500 80 ) as unknown as Response; 81} 82 83/** 84 * Get the ForumAgent's authenticated AtpAgent, or return an error response. 85 * 86 * Checks both that ForumAgent exists (server configuration) and that it's 87 * authenticated (has valid credentials). Returns appropriate error responses: 88 * - 500 if ForumAgent is not configured (server misconfiguration) 89 * - 503 if ForumAgent is not authenticated (temporary, should retry) 90 * 91 * Usage: 92 * ```typescript 93 * const { agent, error } = getForumAgentOrError(ctx, c, "POST /api/mod/ban"); 94 * if (error) return error; 95 * // agent is guaranteed to be non-null here 96 * ``` 97 */ 98export function getForumAgentOrError( 99 appCtx: AppContext, 100 c: Context, 101 operation: string 102): { agent: AtpAgent; error: null } | { agent: null; error: Response } { 103 if (!appCtx.forumAgent) { 104 appCtx.logger.error("CRITICAL: ForumAgent not available", { 105 operation, 106 forumDid: appCtx.config.forumDid, 107 }); 108 return { 109 agent: null, 110 error: c.json({ 111 error: "Forum agent not available. Server configuration issue.", 112 }, 500) as unknown as Response, 113 }; 114 } 115 116 const agent = appCtx.forumAgent.getAgent(); 117 if (!agent) { 118 appCtx.logger.error("ForumAgent not authenticated", { 119 operation, 120 forumDid: appCtx.config.forumDid, 121 }); 122 return { 123 agent: null, 124 error: c.json({ 125 error: "Forum agent not authenticated. Please try again later.", 126 }, 503) as unknown as Response, 127 }; 128 } 129 130 return { agent, error: null }; 131} 132 133/** 134 * Parse JSON body and return 400 if malformed. 135 * 136 * Returns `{ body, error }` where: 137 * - On success: `{ body: parsedObject, error: null }` 138 * - On failure: `{ body: null, error: Response }` — caller should `return error` 139 * 140 * Usage: 141 * ```typescript 142 * const { body, error } = await safeParseJsonBody(c); 143 * if (error) return error; 144 * const { text, boardUri } = body; 145 * ``` 146 */ 147export async function safeParseJsonBody( 148 c: Context 149): Promise<{ body: any; error: null } | { body: null; error: Response }> { 150 try { 151 const body = await c.req.json(); 152 return { body, error: null }; 153 } catch (error) { 154 // Only SyntaxError is expected here (malformed JSON from user input). 155 // Re-throw anything unexpected so programming bugs are not silently swallowed. 156 if (!(error instanceof SyntaxError)) throw error; 157 return { 158 body: null, 159 error: c.json({ error: "Invalid JSON in request body" }, 400) as unknown as Response, 160 }; 161 } 162}