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

refactor(appview): consolidate three identical error handlers into handleRouteError (#74)

handleReadError, handleWriteError, and handleSecurityCheckError had
byte-for-byte identical implementations. Replaced with a single
handleRouteError function, eliminating ~250 lines of duplicated code
across the error handler and its tests. Updated all 10 call sites.

https://claude.ai/code/session_018SH9pay3PqGo9JDdAv3iRj

Co-authored-by: Claude <noreply@anthropic.com>

authored by

Malpercio
Claude
and committed by
GitHub
bdb0ea96 53a5e8f0

+55 -301
+12 -144
apps/appview/src/lib/__tests__/route-errors.test.ts
··· 1 import { describe, it, expect, vi, afterEach } from "vitest"; 2 import { Hono } from "hono"; 3 import { 4 - handleReadError, 5 - handleWriteError, 6 - handleSecurityCheckError, 7 safeParseJsonBody, 8 getForumAgentOrError, 9 } from "../route-errors.js"; ··· 27 return app; 28 } 29 30 - // ─── handleReadError ────────────────────────────────────────────────────────── 31 32 - describe("handleReadError", () => { 33 it("returns 503 for network errors", async () => { 34 const app = makeApp((c) => 35 - handleReadError(c, new Error("fetch failed"), "Failed to read resource", { 36 operation: "GET /test", 37 logger: createMockLogger(), 38 }) ··· 49 50 it("returns 503 for database errors", async () => { 51 const app = makeApp((c) => 52 - handleReadError(c, new Error("database query failed"), "Failed to read resource", { 53 operation: "GET /test", 54 logger: createMockLogger(), 55 }) ··· 66 67 it("returns 500 for unexpected errors", async () => { 68 const app = makeApp((c) => 69 - handleReadError(c, new Error("Something went wrong"), "Failed to read resource", { 70 operation: "GET /test", 71 logger: createMockLogger(), 72 }) ··· 82 it("logs structured context on error", async () => { 83 const mockLogger = createMockLogger(); 84 const app = makeApp((c) => 85 - handleReadError(c, new Error("boom"), "Failed to fetch things", { 86 operation: "GET /test", 87 logger: mockLogger, 88 resourceId: "123", ··· 105 const mockLogger = createMockLogger(); 106 const programmingError = new TypeError("Cannot read property of undefined"); 107 const app = makeApp((c) => 108 - handleReadError(c, programmingError, "Failed to read resource", { 109 operation: "GET /test", 110 logger: mockLogger, 111 }) ··· 129 expect(mockLogger.error).not.toHaveBeenCalledWith( 130 "Failed to read resource", 131 expect.any(Object) 132 - ); 133 - }); 134 - }); 135 - 136 - // ─── handleWriteError ───────────────────────────────────────────────────────── 137 - 138 - describe("handleWriteError", () => { 139 - it("returns 503 for network errors", async () => { 140 - const app = makeApp((c) => 141 - handleWriteError(c, new Error("fetch failed"), "Failed to create thing", { 142 - operation: "POST /test", 143 - logger: createMockLogger(), 144 - }) 145 - ); 146 - 147 - const res = await app.request("/test", { method: "POST" }); 148 - 149 - expect(res.status).toBe(503); 150 - const data = await res.json(); 151 - expect(data.error).toBe( 152 - "Unable to reach external service. Please try again later." 153 ); 154 }); 155 156 - it("returns 503 for database errors", async () => { 157 - const app = makeApp((c) => 158 - handleWriteError(c, new Error("database connection lost"), "Failed to create thing", { 159 - operation: "POST /test", 160 - logger: createMockLogger(), 161 - }) 162 - ); 163 - 164 - const res = await app.request("/test", { method: "POST" }); 165 - 166 - expect(res.status).toBe(503); 167 - const data = await res.json(); 168 - expect(data.error).toBe( 169 - "Database temporarily unavailable. Please try again later." 170 - ); 171 - }); 172 - 173 - it("returns 500 for unexpected errors", async () => { 174 const app = makeApp((c) => 175 - handleWriteError(c, new Error("Something unexpected"), "Failed to create thing", { 176 - operation: "POST /test", 177 - logger: createMockLogger(), 178 - }) 179 - ); 180 - 181 - const res = await app.request("/test", { method: "POST" }); 182 - 183 - expect(res.status).toBe(500); 184 - const data = await res.json(); 185 - expect(data.error).toBe("Failed to create thing. Please contact support if this persists."); 186 - }); 187 - 188 - it("re-throws TypeError (programming error) and logs CRITICAL", async () => { 189 - const mockLogger = createMockLogger(); 190 - const programmingError = new TypeError("Cannot read property of null"); 191 - const app = makeApp((c) => 192 - handleWriteError(c, programmingError, "Failed to create thing", { 193 - operation: "POST /test", 194 - logger: mockLogger, 195 - }) 196 - ); 197 - 198 - const res = await app.request("/test", { method: "POST" }); 199 - expect(res.status).toBe(500); 200 - 201 - expect(mockLogger.error).toHaveBeenCalledWith( 202 - "CRITICAL: Programming error in POST /test", 203 - expect.objectContaining({ 204 operation: "POST /test", 205 - error: "Cannot read property of null", 206 - stack: expect.any(String), 207 - }) 208 - ); 209 - 210 - expect(mockLogger.error).not.toHaveBeenCalledWith( 211 - "Failed to create thing", 212 - expect.any(Object) 213 - ); 214 - }); 215 - }); 216 - 217 - // ─── handleSecurityCheckError ───────────────────────────────────────────────── 218 - 219 - describe("handleSecurityCheckError", () => { 220 - it("returns 503 for network errors", async () => { 221 - const app = makeApp((c) => 222 - handleSecurityCheckError(c, new Error("fetch failed"), "Unable to verify access", { 223 - operation: "POST /test - security check", 224 logger: createMockLogger(), 225 }) 226 ); ··· 234 ); 235 }); 236 237 - it("returns 503 for database errors", async () => { 238 - const app = makeApp((c) => 239 - handleSecurityCheckError(c, new Error("sql query failed"), "Unable to verify access", { 240 - operation: "POST /test - security check", 241 - logger: createMockLogger(), 242 - }) 243 - ); 244 - 245 - const res = await app.request("/test", { method: "POST" }); 246 - 247 - expect(res.status).toBe(503); 248 - const data = await res.json(); 249 - expect(data.error).toBe( 250 - "Database temporarily unavailable. Please try again later." 251 - ); 252 - }); 253 - 254 - it("returns 500 for unexpected errors (fail closed)", async () => { 255 const app = makeApp((c) => 256 - handleSecurityCheckError(c, new Error("Something unexpected"), "Unable to verify access", { 257 operation: "POST /test - security check", 258 logger: createMockLogger(), 259 }) ··· 265 const data = await res.json(); 266 expect(data.error).toBe( 267 "Unable to verify access. Please contact support if this persists." 268 - ); 269 - }); 270 - 271 - it("re-throws TypeError (programming error) and logs CRITICAL", async () => { 272 - const mockLogger = createMockLogger(); 273 - const programmingError = new TypeError("Cannot read property of undefined"); 274 - const app = makeApp((c) => 275 - handleSecurityCheckError(c, programmingError, "Unable to verify access", { 276 - operation: "POST /test - security check", 277 - logger: mockLogger, 278 - }) 279 - ); 280 - 281 - const res = await app.request("/test", { method: "POST" }); 282 - expect(res.status).toBe(500); 283 - 284 - expect(mockLogger.error).toHaveBeenCalledWith( 285 - "CRITICAL: Programming error in POST /test - security check", 286 - expect.objectContaining({ 287 - operation: "POST /test - security check", 288 - error: "Cannot read property of undefined", 289 - stack: expect.any(String), 290 - }) 291 - ); 292 - 293 - expect(mockLogger.error).not.toHaveBeenCalledWith( 294 - "Unable to verify access", 295 - expect.any(Object) 296 ); 297 }); 298 });
··· 1 import { describe, it, expect, vi, afterEach } from "vitest"; 2 import { Hono } from "hono"; 3 import { 4 + handleRouteError, 5 safeParseJsonBody, 6 getForumAgentOrError, 7 } from "../route-errors.js"; ··· 25 return app; 26 } 27 28 + // ─── handleRouteError ───────────────────────────────────────────────────────── 29 30 + describe("handleRouteError", () => { 31 it("returns 503 for network errors", async () => { 32 const app = makeApp((c) => 33 + handleRouteError(c, new Error("fetch failed"), "Failed to read resource", { 34 operation: "GET /test", 35 logger: createMockLogger(), 36 }) ··· 47 48 it("returns 503 for database errors", async () => { 49 const app = makeApp((c) => 50 + handleRouteError(c, new Error("database query failed"), "Failed to read resource", { 51 operation: "GET /test", 52 logger: createMockLogger(), 53 }) ··· 64 65 it("returns 500 for unexpected errors", async () => { 66 const app = makeApp((c) => 67 + handleRouteError(c, new Error("Something went wrong"), "Failed to read resource", { 68 operation: "GET /test", 69 logger: createMockLogger(), 70 }) ··· 80 it("logs structured context on error", async () => { 81 const mockLogger = createMockLogger(); 82 const app = makeApp((c) => 83 + handleRouteError(c, new Error("boom"), "Failed to fetch things", { 84 operation: "GET /test", 85 logger: mockLogger, 86 resourceId: "123", ··· 103 const mockLogger = createMockLogger(); 104 const programmingError = new TypeError("Cannot read property of undefined"); 105 const app = makeApp((c) => 106 + handleRouteError(c, programmingError, "Failed to read resource", { 107 operation: "GET /test", 108 logger: mockLogger, 109 }) ··· 127 expect(mockLogger.error).not.toHaveBeenCalledWith( 128 "Failed to read resource", 129 expect.any(Object) 130 ); 131 }); 132 133 + it("works for write-path errors (POST endpoints)", async () => { 134 const app = makeApp((c) => 135 + handleRouteError(c, new Error("fetch failed"), "Failed to create thing", { 136 operation: "POST /test", 137 logger: createMockLogger(), 138 }) 139 ); ··· 147 ); 148 }); 149 150 + it("works for security check errors (fail closed)", async () => { 151 const app = makeApp((c) => 152 + handleRouteError(c, new Error("Something unexpected"), "Unable to verify access", { 153 operation: "POST /test - security check", 154 logger: createMockLogger(), 155 }) ··· 161 const data = await res.json(); 162 expect(data.error).toBe( 163 "Unable to verify access. Please contact support if this persists." 164 ); 165 }); 166 });
+2 -114
apps/appview/src/lib/route-errors.ts
··· 25 } 26 27 /** 28 - * Handle errors in read-path route handlers (GET endpoints). 29 * 30 * 1. Re-throws programming errors (TypeError, ReferenceError, SyntaxError) 31 * 2. Logs the error with structured context ··· 39 * @param userMessage - User-facing error message (e.g., "Failed to retrieve forum metadata") 40 * @param ctx - Structured logging context 41 */ 42 - export function handleReadError( 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 - * Handle errors in write-path route handlers (POST/PUT/DELETE endpoints). 85 - * 86 - * 1. Re-throws programming errors (TypeError, ReferenceError, SyntaxError) 87 - * 2. Logs the error with structured context 88 - * 3. Classifies the error and returns the appropriate HTTP status: 89 - * - 503 for network errors (PDS unreachable, user should retry) 90 - * - 503 for database errors (temporary, user should retry) 91 - * - 500 for unexpected errors (server bug, needs investigation) 92 - * 93 - * @param c - Hono context 94 - * @param error - The caught error 95 - * @param userMessage - User-facing error message (e.g., "Failed to create topic") 96 - * @param ctx - Structured logging context 97 - */ 98 - export function handleWriteError( 99 - c: Context, 100 - error: unknown, 101 - userMessage: string, 102 - ctx: ErrorContext 103 - ): Response { 104 - if (isProgrammingError(error)) { 105 - const { message, stack } = formatError(error); 106 - ctx.logger.error(`CRITICAL: Programming error in ${ctx.operation}`, { 107 - ...ctx, 108 - error: message, 109 - stack, 110 - }); 111 - throw error; 112 - } 113 - 114 - ctx.logger.error(userMessage, { 115 - ...ctx, 116 - error: formatError(error).message, 117 - }); 118 - 119 - if (error instanceof Error && isNetworkError(error)) { 120 - return c.json( 121 - { error: "Unable to reach external service. Please try again later." }, 122 - 503 123 - ) as unknown as Response; 124 - } 125 - 126 - if (error instanceof Error && isDatabaseError(error)) { 127 - return c.json( 128 - { error: "Database temporarily unavailable. Please try again later." }, 129 - 503 130 - ) as unknown as Response; 131 - } 132 - 133 - return c.json( 134 - { error: `${userMessage}. Please contact support if this persists.` }, 135 - 500 136 - ) as unknown as Response; 137 - } 138 - 139 - /** 140 - * Handle errors in security-critical checks (ban check, lock check, permission verification). 141 - * 142 - * Fail-closed: returns an error response that denies the operation. 143 - * 144 - * 1. Re-throws programming errors 145 - * 2. Returns 503 for network errors (external service unreachable, user should retry) 146 - * 3. Returns 503 for database errors (temporary, user should retry) 147 - * 4. Returns 500 for unexpected errors (denying access) 148 - * 149 - * @param c - Hono context 150 - * @param error - The caught error 151 - * @param userMessage - User-facing error message (e.g., "Unable to verify permissions") 152 - * @param ctx - Structured logging context 153 - */ 154 - export function handleSecurityCheckError( 155 c: Context, 156 error: unknown, 157 userMessage: string,
··· 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 ··· 39 * @param userMessage - User-facing error message (e.g., "Failed to retrieve forum metadata") 40 * @param ctx - Structured logging context 41 */ 42 + export function handleRouteError( 43 c: Context, 44 error: unknown, 45 userMessage: string,
+2 -2
apps/appview/src/middleware/require-not-banned.ts
··· 1 import type { Context, Next } from "hono"; 2 import type { AppContext } from "../lib/app-context.js"; 3 import type { Variables } from "../types.js"; 4 - import { handleSecurityCheckError } from "../lib/route-errors.js"; 5 import { getActiveBans } from "../routes/helpers.js"; 6 7 /** ··· 26 return c.json({ error: "You are banned from this forum" }, 403); 27 } 28 } catch (error) { 29 - return handleSecurityCheckError(c, error, "Unable to verify ban status", { 30 operation: `${c.req.method} ${c.req.path} - ban check`, 31 logger: ctx.logger, 32 userId: user.did,
··· 1 import type { Context, Next } from "hono"; 2 import type { AppContext } from "../lib/app-context.js"; 3 import type { Variables } from "../types.js"; 4 + import { handleRouteError } from "../lib/route-errors.js"; 5 import { getActiveBans } from "../routes/helpers.js"; 6 7 /** ··· 26 return c.json({ error: "You are banned from this forum" }, 403); 27 } 28 } catch (error) { 29 + return handleRouteError(c, error, "Unable to verify ban status", { 30 operation: `${c.req.method} ${c.req.path} - ban check`, 31 logger: ctx.logger, 32 userId: user.did,
+8 -9
apps/appview/src/routes/admin.ts
··· 9 import { BackfillStatus } from "../lib/backfill-manager.js"; 10 import { CursorManager } from "../lib/cursor-manager.js"; 11 import { 12 - handleReadError, 13 - handleWriteError, 14 safeParseJsonBody, 15 getForumAgentOrError, 16 } from "../lib/route-errors.js"; ··· 128 targetDid, 129 }); 130 } catch (error) { 131 - return handleWriteError(c, error, "Failed to assign role", { 132 operation: "POST /api/admin/members/:did/role", 133 logger: ctx.logger, 134 targetDid, ··· 136 }); 137 } 138 } catch (error) { 139 - return handleReadError(c, error, "Failed to process role assignment", { 140 operation: "POST /api/admin/members/:did/role", 141 logger: ctx.logger, 142 targetDid, ··· 189 190 return c.json({ roles: rolesWithPermissions }); 191 } catch (error) { 192 - return handleReadError(c, error, "Failed to retrieve roles", { 193 operation: "GET /api/admin/roles", 194 logger: ctx.logger, 195 }); ··· 237 isTruncated: membersList.length === 100, 238 }); 239 } catch (error) { 240 - return handleReadError(c, error, "Failed to retrieve members", { 241 operation: "GET /api/admin/members", 242 logger: ctx.logger, 243 }); ··· 301 permissions, 302 }); 303 } catch (error) { 304 - return handleReadError(c, error, "Failed to retrieve your membership", { 305 operation: "GET /api/admin/members/me", 306 logger: ctx.logger, 307 did: user.did, ··· 432 errorMessage: row.errorMessage, 433 }); 434 } catch (error) { 435 - return handleReadError(c, error, "Failed to fetch backfill progress", { 436 operation: "GET /api/admin/backfill/:id", 437 logger: ctx.logger, 438 id, ··· 475 })), 476 }); 477 } catch (error) { 478 - return handleReadError(c, error, "Failed to fetch backfill errors", { 479 operation: "GET /api/admin/backfill/:id/errors", 480 logger: ctx.logger, 481 id,
··· 9 import { BackfillStatus } from "../lib/backfill-manager.js"; 10 import { CursorManager } from "../lib/cursor-manager.js"; 11 import { 12 + handleRouteError, 13 safeParseJsonBody, 14 getForumAgentOrError, 15 } from "../lib/route-errors.js"; ··· 127 targetDid, 128 }); 129 } catch (error) { 130 + return handleRouteError(c, error, "Failed to assign role", { 131 operation: "POST /api/admin/members/:did/role", 132 logger: ctx.logger, 133 targetDid, ··· 135 }); 136 } 137 } catch (error) { 138 + return handleRouteError(c, error, "Failed to process role assignment", { 139 operation: "POST /api/admin/members/:did/role", 140 logger: ctx.logger, 141 targetDid, ··· 188 189 return c.json({ roles: rolesWithPermissions }); 190 } catch (error) { 191 + return handleRouteError(c, error, "Failed to retrieve roles", { 192 operation: "GET /api/admin/roles", 193 logger: ctx.logger, 194 }); ··· 236 isTruncated: membersList.length === 100, 237 }); 238 } catch (error) { 239 + return handleRouteError(c, error, "Failed to retrieve members", { 240 operation: "GET /api/admin/members", 241 logger: ctx.logger, 242 }); ··· 300 permissions, 301 }); 302 } catch (error) { 303 + return handleRouteError(c, error, "Failed to retrieve your membership", { 304 operation: "GET /api/admin/members/me", 305 logger: ctx.logger, 306 did: user.did, ··· 431 errorMessage: row.errorMessage, 432 }); 433 } catch (error) { 434 + return handleRouteError(c, error, "Failed to fetch backfill progress", { 435 operation: "GET /api/admin/backfill/:id", 436 logger: ctx.logger, 437 id, ··· 474 })), 475 }); 476 } catch (error) { 477 + return handleRouteError(c, error, "Failed to fetch backfill errors", { 478 operation: "GET /api/admin/backfill/:id/errors", 479 logger: ctx.logger, 480 id,
+4 -4
apps/appview/src/routes/boards.ts
··· 3 import { boards, categories, posts, users } from "@atbb/db"; 4 import { asc, count, eq, and, desc, isNull } from "drizzle-orm"; 5 import { serializeBoard, parseBigIntParam, serializePost } from "./helpers.js"; 6 - import { handleReadError } from "../lib/route-errors.js"; 7 8 /** 9 * Factory function that creates board routes with access to app context. ··· 23 boards: allBoards.map(({ boards: board }) => serializeBoard(board)), 24 }); 25 } catch (error) { 26 - return handleReadError(c, error, "Failed to retrieve boards", { 27 operation: "GET /api/boards", 28 logger: ctx.logger, 29 }); ··· 49 50 return c.json(serializeBoard(board)); 51 } catch (error) { 52 - return handleReadError(c, error, "Failed to retrieve board", { 53 operation: "GET /api/boards/:id", 54 logger: ctx.logger, 55 boardId: raw, ··· 117 limit, 118 }); 119 } catch (error) { 120 - return handleReadError(c, error, "Failed to retrieve topics", { 121 operation: "GET /api/boards/:id/topics", 122 logger: ctx.logger, 123 boardId: id,
··· 3 import { boards, categories, posts, users } from "@atbb/db"; 4 import { asc, count, eq, and, desc, isNull } from "drizzle-orm"; 5 import { serializeBoard, parseBigIntParam, serializePost } from "./helpers.js"; 6 + import { handleRouteError } from "../lib/route-errors.js"; 7 8 /** 9 * Factory function that creates board routes with access to app context. ··· 23 boards: allBoards.map(({ boards: board }) => serializeBoard(board)), 24 }); 25 } catch (error) { 26 + return handleRouteError(c, error, "Failed to retrieve boards", { 27 operation: "GET /api/boards", 28 logger: ctx.logger, 29 }); ··· 49 50 return c.json(serializeBoard(board)); 51 } catch (error) { 52 + return handleRouteError(c, error, "Failed to retrieve board", { 53 operation: "GET /api/boards/:id", 54 logger: ctx.logger, 55 boardId: raw, ··· 117 limit, 118 }); 119 } catch (error) { 120 + return handleRouteError(c, error, "Failed to retrieve topics", { 121 operation: "GET /api/boards/:id/topics", 122 logger: ctx.logger, 123 boardId: id,
+4 -4
apps/appview/src/routes/categories.ts
··· 3 import { categories, boards } from "@atbb/db"; 4 import { eq, asc } from "drizzle-orm"; 5 import { serializeCategory, serializeBoard, parseBigIntParam } from "./helpers.js"; 6 - import { handleReadError } from "../lib/route-errors.js"; 7 8 /** 9 * Factory function that creates category routes with access to app context. ··· 28 categories: allCategories.map(serializeCategory), 29 }); 30 } catch (error) { 31 - return handleReadError(c, error, "Failed to retrieve categories", { 32 operation: "GET /api/categories", 33 logger: ctx.logger, 34 }); ··· 54 55 return c.json(serializeCategory(category)); 56 } catch (error) { 57 - return handleReadError(c, error, "Failed to retrieve category", { 58 operation: "GET /api/categories/:id", 59 logger: ctx.logger, 60 categoryId: raw, ··· 92 boards: categoryBoards.map(serializeBoard), 93 }); 94 } catch (error) { 95 - return handleReadError(c, error, "Failed to retrieve boards", { 96 operation: "GET /api/categories/:id/boards", 97 logger: ctx.logger, 98 categoryId: id,
··· 3 import { categories, boards } from "@atbb/db"; 4 import { eq, asc } from "drizzle-orm"; 5 import { serializeCategory, serializeBoard, parseBigIntParam } from "./helpers.js"; 6 + import { handleRouteError } from "../lib/route-errors.js"; 7 8 /** 9 * Factory function that creates category routes with access to app context. ··· 28 categories: allCategories.map(serializeCategory), 29 }); 30 } catch (error) { 31 + return handleRouteError(c, error, "Failed to retrieve categories", { 32 operation: "GET /api/categories", 33 logger: ctx.logger, 34 }); ··· 54 55 return c.json(serializeCategory(category)); 56 } catch (error) { 57 + return handleRouteError(c, error, "Failed to retrieve category", { 58 operation: "GET /api/categories/:id", 59 logger: ctx.logger, 60 categoryId: raw, ··· 92 boards: categoryBoards.map(serializeBoard), 93 }); 94 } catch (error) { 95 + return handleRouteError(c, error, "Failed to retrieve boards", { 96 operation: "GET /api/categories/:id/boards", 97 logger: ctx.logger, 98 categoryId: id,
+2 -2
apps/appview/src/routes/forum.ts
··· 3 import { forums } from "@atbb/db"; 4 import { eq } from "drizzle-orm"; 5 import { serializeForum } from "./helpers.js"; 6 - import { handleReadError } from "../lib/route-errors.js"; 7 8 /** 9 * Factory function that creates forum routes with access to app context. ··· 24 25 return c.json(serializeForum(forum)); 26 } catch (error) { 27 - return handleReadError(c, error, "Failed to retrieve forum metadata", { 28 operation: "GET /api/forum", 29 logger: ctx.logger, 30 });
··· 3 import { forums } from "@atbb/db"; 4 import { eq } from "drizzle-orm"; 5 import { serializeForum } from "./helpers.js"; 6 + import { handleRouteError } from "../lib/route-errors.js"; 7 8 /** 9 * Factory function that creates forum routes with access to app context. ··· 24 25 return c.json(serializeForum(forum)); 26 } catch (error) { 27 + return handleRouteError(c, error, "Failed to retrieve forum metadata", { 28 operation: "GET /api/forum", 29 logger: ctx.logger, 30 });
+13 -14
apps/appview/src/routes/mod.ts
··· 6 import { requirePermission } from "../middleware/permissions.js"; 7 import { isProgrammingError } from "../lib/errors.js"; 8 import { 9 - handleWriteError, 10 - handleReadError, 11 safeParseJsonBody, 12 getForumAgentOrError, 13 } from "../lib/route-errors.js"; ··· 136 return c.json({ error: "Target user not found" }, 404); 137 } 138 } catch (error) { 139 - return handleReadError(c, error, "Failed to check user membership", { 140 operation: "POST /api/mod/ban", 141 logger: ctx.logger, 142 targetDid, ··· 193 alreadyActive: false, 194 }); 195 } catch (error) { 196 - return handleWriteError(c, error, "Failed to record moderation action", { 197 operation: "POST /api/mod/ban", 198 logger: ctx.logger, 199 moderatorDid: user.did, ··· 243 return c.json({ error: "Target user not found" }, 404); 244 } 245 } catch (error) { 246 - return handleReadError(c, error, "Failed to check user membership", { 247 operation: "DELETE /api/mod/ban/:did", 248 logger: ctx.logger, 249 targetDid, ··· 301 alreadyActive: false, 302 }); 303 } catch (error) { 304 - return handleWriteError(c, error, "Failed to record moderation action", { 305 operation: "DELETE /api/mod/ban/:did", 306 logger: ctx.logger, 307 moderatorDid: user.did, ··· 356 357 topic = result; 358 } catch (error) { 359 - return handleReadError(c, error, "Failed to check topic", { 360 operation: "POST /api/mod/lock", 361 logger: ctx.logger, 362 topicId, ··· 426 alreadyActive: false, 427 }); 428 } catch (error) { 429 - return handleWriteError(c, error, "Failed to record moderation action", { 430 operation: "POST /api/mod/lock", 431 logger: ctx.logger, 432 moderatorDid: user.did, ··· 481 482 topic = result; 483 } catch (error) { 484 - return handleReadError(c, error, "Failed to check topic", { 485 operation: "DELETE /api/mod/lock/:topicId", 486 logger: ctx.logger, 487 topicId: topicIdParam, ··· 552 alreadyActive: false, 553 }); 554 } catch (error) { 555 - return handleWriteError(c, error, "Failed to record moderation action", { 556 operation: "DELETE /api/mod/lock/:topicId", 557 logger: ctx.logger, 558 moderatorDid: user.did, ··· 616 617 post = result; 618 } catch (error) { 619 - return handleReadError(c, error, "Failed to retrieve post", { 620 operation: "POST /api/mod/hide", 621 logger: ctx.logger, 622 postId, ··· 680 alreadyActive: false, 681 }, 200); 682 } catch (error) { 683 - return handleWriteError(c, error, "Failed to record moderation action", { 684 operation: "POST /api/mod/hide", 685 logger: ctx.logger, 686 moderatorDid: user.did, ··· 740 741 post = result; 742 } catch (error) { 743 - return handleReadError(c, error, "Failed to retrieve post", { 744 operation: "DELETE /api/mod/hide/:postId", 745 logger: ctx.logger, 746 postId: postIdParam, ··· 805 alreadyActive: false, 806 }, 200); 807 } catch (error) { 808 - return handleWriteError(c, error, "Failed to record moderation action", { 809 operation: "DELETE /api/mod/hide/:postId", 810 logger: ctx.logger, 811 moderatorDid: user.did,
··· 6 import { requirePermission } from "../middleware/permissions.js"; 7 import { isProgrammingError } from "../lib/errors.js"; 8 import { 9 + handleRouteError, 10 safeParseJsonBody, 11 getForumAgentOrError, 12 } from "../lib/route-errors.js"; ··· 135 return c.json({ error: "Target user not found" }, 404); 136 } 137 } catch (error) { 138 + return handleRouteError(c, error, "Failed to check user membership", { 139 operation: "POST /api/mod/ban", 140 logger: ctx.logger, 141 targetDid, ··· 192 alreadyActive: false, 193 }); 194 } catch (error) { 195 + return handleRouteError(c, error, "Failed to record moderation action", { 196 operation: "POST /api/mod/ban", 197 logger: ctx.logger, 198 moderatorDid: user.did, ··· 242 return c.json({ error: "Target user not found" }, 404); 243 } 244 } catch (error) { 245 + return handleRouteError(c, error, "Failed to check user membership", { 246 operation: "DELETE /api/mod/ban/:did", 247 logger: ctx.logger, 248 targetDid, ··· 300 alreadyActive: false, 301 }); 302 } catch (error) { 303 + return handleRouteError(c, error, "Failed to record moderation action", { 304 operation: "DELETE /api/mod/ban/:did", 305 logger: ctx.logger, 306 moderatorDid: user.did, ··· 355 356 topic = result; 357 } catch (error) { 358 + return handleRouteError(c, error, "Failed to check topic", { 359 operation: "POST /api/mod/lock", 360 logger: ctx.logger, 361 topicId, ··· 425 alreadyActive: false, 426 }); 427 } catch (error) { 428 + return handleRouteError(c, error, "Failed to record moderation action", { 429 operation: "POST /api/mod/lock", 430 logger: ctx.logger, 431 moderatorDid: user.did, ··· 480 481 topic = result; 482 } catch (error) { 483 + return handleRouteError(c, error, "Failed to check topic", { 484 operation: "DELETE /api/mod/lock/:topicId", 485 logger: ctx.logger, 486 topicId: topicIdParam, ··· 551 alreadyActive: false, 552 }); 553 } catch (error) { 554 + return handleRouteError(c, error, "Failed to record moderation action", { 555 operation: "DELETE /api/mod/lock/:topicId", 556 logger: ctx.logger, 557 moderatorDid: user.did, ··· 615 616 post = result; 617 } catch (error) { 618 + return handleRouteError(c, error, "Failed to retrieve post", { 619 operation: "POST /api/mod/hide", 620 logger: ctx.logger, 621 postId, ··· 679 alreadyActive: false, 680 }, 200); 681 } catch (error) { 682 + return handleRouteError(c, error, "Failed to record moderation action", { 683 operation: "POST /api/mod/hide", 684 logger: ctx.logger, 685 moderatorDid: user.did, ··· 739 740 post = result; 741 } catch (error) { 742 + return handleRouteError(c, error, "Failed to retrieve post", { 743 operation: "DELETE /api/mod/hide/:postId", 744 logger: ctx.logger, 745 postId: postIdParam, ··· 804 alreadyActive: false, 805 }, 200); 806 } catch (error) { 807 + return handleRouteError(c, error, "Failed to record moderation action", { 808 operation: "DELETE /api/mod/hide/:postId", 809 logger: ctx.logger, 810 moderatorDid: user.did,
+4 -4
apps/appview/src/routes/posts.ts
··· 5 import { requireAuth } from "../middleware/auth.js"; 6 import { requirePermission } from "../middleware/permissions.js"; 7 import { requireNotBanned } from "../middleware/require-not-banned.js"; 8 - import { handleWriteError, handleSecurityCheckError, safeParseJsonBody } from "../lib/route-errors.js"; 9 import { 10 validatePostText, 11 parseBigIntParam, ··· 52 return c.json({ error: "This topic is locked and not accepting new replies" }, 403); 53 } 54 } catch (error) { 55 - return handleSecurityCheckError(c, error, "Unable to verify topic status", { 56 operation: "POST /api/posts - lock check", 57 logger: ctx.logger, 58 userId: user.did, ··· 65 try { 66 postsMap = await getPostsByIds(ctx.db, [rootId, parentId]); 67 } catch (error) { 68 - return handleWriteError(c, error, "Failed to look up posts for reply", { 69 operation: "POST /api/posts - post lookup", 70 logger: ctx.logger, 71 userId: user.did, ··· 133 201 134 ); 135 } catch (error) { 136 - return handleWriteError(c, error, "Failed to create post", { 137 operation: "POST /api/posts", 138 logger: ctx.logger, 139 userId: user.did,
··· 5 import { requireAuth } from "../middleware/auth.js"; 6 import { requirePermission } from "../middleware/permissions.js"; 7 import { requireNotBanned } from "../middleware/require-not-banned.js"; 8 + import { handleRouteError, safeParseJsonBody } from "../lib/route-errors.js"; 9 import { 10 validatePostText, 11 parseBigIntParam, ··· 52 return c.json({ error: "This topic is locked and not accepting new replies" }, 403); 53 } 54 } catch (error) { 55 + return handleRouteError(c, error, "Unable to verify topic status", { 56 operation: "POST /api/posts - lock check", 57 logger: ctx.logger, 58 userId: user.did, ··· 65 try { 66 postsMap = await getPostsByIds(ctx.db, [rootId, parentId]); 67 } catch (error) { 68 + return handleRouteError(c, error, "Failed to look up posts for reply", { 69 operation: "POST /api/posts - post lookup", 70 logger: ctx.logger, 71 userId: user.did, ··· 133 201 134 ); 135 } catch (error) { 136 + return handleRouteError(c, error, "Failed to create post", { 137 operation: "POST /api/posts", 138 logger: ctx.logger, 139 userId: user.did,
+4 -4
apps/appview/src/routes/topics.ts
··· 8 import { requirePermission } from "../middleware/permissions.js"; 9 import { requireNotBanned } from "../middleware/require-not-banned.js"; 10 import { parseAtUri } from "../lib/at-uri.js"; 11 - import { handleReadError, handleWriteError, safeParseJsonBody } from "../lib/route-errors.js"; 12 import { isProgrammingError } from "../lib/errors.js"; 13 import { 14 parseBigIntParam, ··· 81 .offset(offset), 82 ]); 83 } catch (error) { 84 - return handleReadError(c, error, "Failed to retrieve replies for topic", { 85 operation: "GET /api/topics/:id - reply query", 86 logger: ctx.logger, 87 topicId: id, ··· 157 limit, 158 }); 159 } catch (error) { 160 - return handleReadError(c, error, "Failed to retrieve topic", { 161 operation: "GET /api/topics/:id - topic query", 162 logger: ctx.logger, 163 topicId: id, ··· 312 201 313 ); 314 } catch (error) { 315 - return handleWriteError(c, error, "Failed to create topic", { 316 operation: "POST /api/topics", 317 logger: ctx.logger, 318 userId: user.did,
··· 8 import { requirePermission } from "../middleware/permissions.js"; 9 import { requireNotBanned } from "../middleware/require-not-banned.js"; 10 import { parseAtUri } from "../lib/at-uri.js"; 11 + import { handleRouteError, safeParseJsonBody } from "../lib/route-errors.js"; 12 import { isProgrammingError } from "../lib/errors.js"; 13 import { 14 parseBigIntParam, ··· 81 .offset(offset), 82 ]); 83 } catch (error) { 84 + return handleRouteError(c, error, "Failed to retrieve replies for topic", { 85 operation: "GET /api/topics/:id - reply query", 86 logger: ctx.logger, 87 topicId: id, ··· 157 limit, 158 }); 159 } catch (error) { 160 + return handleRouteError(c, error, "Failed to retrieve topic", { 161 operation: "GET /api/topics/:id - topic query", 162 logger: ctx.logger, 163 topicId: id, ··· 312 201 313 ); 314 } catch (error) { 315 + return handleRouteError(c, error, "Failed to create topic", { 316 operation: "POST /api/topics", 317 logger: ctx.logger, 318 userId: user.did,