Add withDb() helper for graceful DB error handling
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) <noreply@anthropic.com>
This commit is contained in:
parent
cadcf753a7
commit
8fb7771223
3 changed files with 33 additions and 21 deletions
|
|
@ -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" });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -14,4 +14,28 @@ export function createDb(connectionString?: string) {
|
|||
|
||||
export type Database = ReturnType<typeof createDb>;
|
||||
|
||||
/**
|
||||
* Wraps a route handler (loader/action) to catch database errors
|
||||
* and return a 503 Service Unavailable instead of crashing.
|
||||
*/
|
||||
export function withDb<T>(handler: () => Promise<T>): Promise<T> {
|
||||
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 };
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue