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 bdb0ea96fa8ad8a8daea16e580e4064c91ac8645 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}