From 8fb77712233462e86b4b5ef192f08af82526df03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ullrich=20Sch=C3=A4fer?= Date: Tue, 24 Mar 2026 23:39:33 +0100 Subject: [PATCH] Add withDb() helper for graceful DB error handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Shared withDb() wrapper in @trails-cool/db catches database connection errors and throws a 503 Response instead of crashing the server. Re-throws React Router responses (redirects, data()). Applied to Planner session routes — replaces manual try/catch. Any route can use withDb(() => ...) for automatic 503 on DB failure. Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/planner/app/routes/api.sessions.ts | 22 ++++++---------------- apps/planner/app/routes/session.$id.tsx | 8 +++----- packages/db/src/index.ts | 24 ++++++++++++++++++++++++ 3 files changed, 33 insertions(+), 21 deletions(-) diff --git a/apps/planner/app/routes/api.sessions.ts b/apps/planner/app/routes/api.sessions.ts index 1827f7a..3778341 100644 --- a/apps/planner/app/routes/api.sessions.ts +++ b/apps/planner/app/routes/api.sessions.ts @@ -2,6 +2,7 @@ import { data } from "react-router"; import type { Route } from "./+types/api.sessions"; import { createSession, listSessions } from "~/lib/sessions"; import { parseGpxAsync } from "@trails-cool/gpx"; +import { withDb } from "@trails-cool/db"; export async function action({ request }: Route.ActionArgs) { if (request.method !== "POST") { @@ -15,7 +16,7 @@ export async function action({ request }: Route.ActionArgs) { gpx?: string; }; - try { + return withDb(async () => { const session = await createSession({ callbackUrl, callbackToken }); let initialWaypoints: Array<{ lat: number; lon: number; name?: string }> | undefined; @@ -29,26 +30,15 @@ export async function action({ request }: Route.ActionArgs) { } return data( - { - sessionId: session.id, - url: `/session/${session.id}`, - initialWaypoints, - }, + { sessionId: session.id, url: `/session/${session.id}`, initialWaypoints }, { status: 201 }, ); - } catch (e) { - return data( - { error: "Database unavailable", details: (e as Error).message }, - { status: 503 }, - ); - } + }); } export async function loader(_args: Route.LoaderArgs) { - try { + return withDb(async () => { const sessions = await listSessions(); return data({ sessions }); - } catch { - return data({ sessions: [], error: "Database unavailable" }); - } + }); } diff --git a/apps/planner/app/routes/session.$id.tsx b/apps/planner/app/routes/session.$id.tsx index 67b3043..2c630d7 100644 --- a/apps/planner/app/routes/session.$id.tsx +++ b/apps/planner/app/routes/session.$id.tsx @@ -2,6 +2,7 @@ import { useParams, useSearchParams } from "react-router"; import type { Route } from "./+types/session.$id"; import { getSession } from "~/lib/sessions"; import { data } from "react-router"; +import { withDb } from "@trails-cool/db"; import { ClientOnly } from "~/components/ClientOnly"; import { lazy, Suspense } from "react"; @@ -14,7 +15,7 @@ export function meta(_args: Route.MetaArgs) { } export async function loader({ params }: Route.LoaderArgs) { - try { + return withDb(async () => { const session = await getSession(params.id); if (!session) { throw data({ error: "Session not found" }, { status: 404 }); @@ -24,10 +25,7 @@ export async function loader({ params }: Route.LoaderArgs) { callbackUrl: session.callbackUrl ?? null, callbackToken: session.callbackToken ?? null, }); - } catch (e) { - if (e instanceof Response) throw e; // Re-throw data() responses - throw data({ error: "Database unavailable" }, { status: 503 }); - } + }); } export default function SessionPage({ loaderData }: Route.ComponentProps) { diff --git a/packages/db/src/index.ts b/packages/db/src/index.ts index b051bfd..0cd5ba6 100644 --- a/packages/db/src/index.ts +++ b/packages/db/src/index.ts @@ -14,4 +14,28 @@ export function createDb(connectionString?: string) { export type Database = ReturnType; +/** + * Wraps a route handler (loader/action) to catch database errors + * and return a 503 Service Unavailable instead of crashing. + */ +export function withDb(handler: () => Promise): Promise { + return handler().catch((error) => { + if (error instanceof Response) throw error; // Re-throw React Router responses (redirects, data()) + const message = error instanceof Error ? error.message : "Unknown error"; + if ( + message.includes("connect") || + message.includes("ECONNREFUSED") || + message.includes("DrizzleQueryError") || + message.includes("AggregateError") || + message.includes("database") + ) { + throw new Response(JSON.stringify({ error: "Database unavailable" }), { + status: 503, + headers: { "Content-Type": "application/json" }, + }); + } + throw error; + }); +} + export { plannerSchema, journalSchema };