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
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

refactor: extract network error classification helpers (#20)

Added isProgrammingError() and isNetworkError() helpers to routes/helpers.ts
to replace ~35 lines of duplicated error classification logic in the POST
catch blocks of topics.ts and posts.ts.

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

authored by

Malpercio
Claude
and committed by
GitHub
9a93b3e9 b758cd90

+99 -35
+60 -1
apps/appview/src/routes/__tests__/helpers.test.ts
··· 1 1 import { describe, it, expect, beforeEach, afterEach } from "vitest"; 2 - import { validatePostText, getForumByUri, getPostsByIds, validateReplyParent } from "../helpers.js"; 2 + import { validatePostText, getForumByUri, getPostsByIds, validateReplyParent, isProgrammingError, isNetworkError } from "../helpers.js"; 3 3 import { createTestContext, type TestContext } from "../../lib/__tests__/test-context.js"; 4 4 import { posts, users } from "@atbb/db"; 5 5 import { eq } from "drizzle-orm"; ··· 239 239 expect(result.error).toContain("does not belong to this thread"); 240 240 }); 241 241 }); 242 + 243 + describe("isProgrammingError", () => { 244 + it("returns true for TypeError", () => { 245 + expect(isProgrammingError(new TypeError("Cannot read property"))).toBe(true); 246 + }); 247 + 248 + it("returns true for ReferenceError", () => { 249 + expect(isProgrammingError(new ReferenceError("x is not defined"))).toBe(true); 250 + }); 251 + 252 + it("returns false for generic Error", () => { 253 + expect(isProgrammingError(new Error("something went wrong"))).toBe(false); 254 + }); 255 + 256 + it("returns false for SyntaxError", () => { 257 + expect(isProgrammingError(new SyntaxError("Unexpected token"))).toBe(false); 258 + }); 259 + 260 + it("returns false for non-Error values", () => { 261 + expect(isProgrammingError("string error")).toBe(false); 262 + expect(isProgrammingError(null)).toBe(false); 263 + expect(isProgrammingError(undefined)).toBe(false); 264 + expect(isProgrammingError(42)).toBe(false); 265 + }); 266 + }); 267 + 268 + describe("isNetworkError", () => { 269 + it("returns true for fetch failed", () => { 270 + expect(isNetworkError(new Error("fetch failed"))).toBe(true); 271 + }); 272 + 273 + it("returns true for network errors", () => { 274 + expect(isNetworkError(new Error("Network request failed"))).toBe(true); 275 + }); 276 + 277 + it("returns true for timeout errors", () => { 278 + expect(isNetworkError(new Error("Request timeout"))).toBe(true); 279 + expect(isNetworkError(new Error("timeout of 5000ms exceeded"))).toBe(true); 280 + }); 281 + 282 + it("returns true for ECONNREFUSED", () => { 283 + expect(isNetworkError(new Error("connect ECONNREFUSED 127.0.0.1:3000"))).toBe(true); 284 + }); 285 + 286 + it("returns true for ENOTFOUND", () => { 287 + expect(isNetworkError(new Error("getaddrinfo ENOTFOUND example.com"))).toBe(true); 288 + }); 289 + 290 + it("is case-insensitive", () => { 291 + expect(isNetworkError(new Error("FETCH FAILED"))).toBe(true); 292 + expect(isNetworkError(new Error("TIMEOUT"))).toBe(true); 293 + }); 294 + 295 + it("returns false for non-network errors", () => { 296 + expect(isNetworkError(new Error("Internal Server Error"))).toBe(false); 297 + expect(isNetworkError(new Error("Database connection lost"))).toBe(false); 298 + expect(isNetworkError(new Error("PDS internal error"))).toBe(false); 299 + }); 300 + });
+19
apps/appview/src/routes/helpers.ts
··· 138 138 } 139 139 } 140 140 141 + /** 142 + * Check if an error is a programming bug (TypeError, ReferenceError) 143 + * that should be re-thrown rather than caught. 144 + */ 145 + export function isProgrammingError(error: unknown): boolean { 146 + return error instanceof TypeError || error instanceof ReferenceError; 147 + } 148 + 149 + /** 150 + * Check if an Error represents a network-level failure 151 + * (fetch failed, timeout, DNS resolution, connection refused). 152 + */ 153 + export function isNetworkError(error: Error): boolean { 154 + const msg = error.message.toLowerCase(); 155 + return ["fetch failed", "network", "timeout", "econnrefused", "enotfound"].some( 156 + (k) => msg.includes(k) 157 + ); 158 + } 159 + 141 160 export type PostRow = typeof posts.$inferSelect; 142 161 143 162 /**
+10 -17
apps/appview/src/routes/posts.ts
··· 7 7 parseBigIntParam, 8 8 getPostsByIds, 9 9 validateReplyParent, 10 + isProgrammingError, 11 + isNetworkError, 10 12 } from "./helpers.js"; 11 13 12 14 export function createPostsRoutes(ctx: AppContext) { ··· 104 106 ); 105 107 } catch (error) { 106 108 // Re-throw programming bugs (don't catch TypeError, ReferenceError) 107 - if (error instanceof TypeError || error instanceof ReferenceError) { 109 + if (isProgrammingError(error)) { 108 110 console.error("CRITICAL: Programming error in POST /api/posts", { 109 111 operation: "POST /api/posts", 110 112 userId: user.did, ··· 125 127 }); 126 128 127 129 // Distinguish network errors from server errors 128 - if (error instanceof Error) { 129 - const msg = error.message.toLowerCase(); 130 - if ( 131 - msg.includes("fetch failed") || 132 - msg.includes("network") || 133 - msg.includes("timeout") || 134 - msg.includes("econnrefused") || 135 - msg.includes("enotfound") 136 - ) { 137 - return c.json( 138 - { 139 - error: "Unable to reach your PDS. Please try again later.", 140 - }, 141 - 503 142 - ); 143 - } 130 + if (error instanceof Error && isNetworkError(error)) { 131 + return c.json( 132 + { 133 + error: "Unable to reach your PDS. Please try again later.", 134 + }, 135 + 503 136 + ); 144 137 } 145 138 146 139 return c.json(
+10 -17
apps/appview/src/routes/topics.ts
··· 11 11 serializeDate, 12 12 validatePostText, 13 13 getForumByUri, 14 + isProgrammingError, 15 + isNetworkError, 14 16 } from "./helpers.js"; 15 17 16 18 /** ··· 153 155 ); 154 156 } catch (error) { 155 157 // Re-throw programming bugs (don't catch TypeError, ReferenceError) 156 - if (error instanceof TypeError || error instanceof ReferenceError) { 158 + if (isProgrammingError(error)) { 157 159 console.error("CRITICAL: Programming error in POST /api/topics", { 158 160 operation: "POST /api/topics", 159 161 userId: user.did, ··· 170 172 }); 171 173 172 174 // Distinguish network errors from server errors 173 - if (error instanceof Error) { 174 - const msg = error.message.toLowerCase(); 175 - if ( 176 - msg.includes("fetch failed") || 177 - msg.includes("network") || 178 - msg.includes("timeout") || 179 - msg.includes("econnrefused") || 180 - msg.includes("enotfound") 181 - ) { 182 - return c.json( 183 - { 184 - error: "Unable to reach your PDS. Please try again later.", 185 - }, 186 - 503 187 - ); 188 - } 175 + if (error instanceof Error && isNetworkError(error)) { 176 + return c.json( 177 + { 178 + error: "Unable to reach your PDS. Please try again later.", 179 + }, 180 + 503 181 + ); 189 182 } 190 183 191 184 return c.json(