From 15b1eb8fa6dd1bcd2e711e9cd71b860879ce5375 Mon Sep 17 00:00:00 2001 From: Dan Sutton Date: Fri, 8 May 2026 09:15:01 +0100 Subject: [PATCH 1/5] fix(webapp): stop leaking exception messages on 5xx API responses Across the webapp API routes, the catch-all 500 branch surfaced raw `error.message` verbatim. When the underlying exception originated from an internal subsystem, this leaked server-side details into customer-visible responses. Replace each leaking branch with a generic body and route the full error through logger.error so visibility is preserved server-side. 4xx branches that intentionally surface user-facing messages (e.g. ServiceValidationError) are left intact. --- .server-changes/sanitize-api-500-errors.md | 6 ++++++ .../routes/admin.api.v1.platform-notifications.ts | 4 +++- apps/webapp/app/routes/admin.notifications.tsx | 7 +++++-- .../routes/api.v1.batches.$batchParam.results.ts | 8 +++----- apps/webapp/app/routes/api.v1.deployments.ts | 9 +++------ .../api.v1.projects.$projectRef.alertChannels.ts | 7 +++---- apps/webapp/app/routes/api.v1.queues.ts | 7 +++---- ...api.v1.runs.$runFriendlyId.input-streams.wait.ts | 4 ++-- apps/webapp/app/routes/api.v1.runs.$runId.tags.ts | 7 +++---- .../app/routes/api.v1.runs.$runParam.attempts.ts | 7 +++---- .../app/routes/api.v1.runs.$runParam.reschedule.ts | 8 ++++---- .../app/routes/api.v1.runs.$runParam.result.ts | 8 +++----- .../routes/api.v1.schedules.$scheduleId.activate.ts | 7 +++---- .../api.v1.schedules.$scheduleId.deactivate.ts | 7 +++---- .../app/routes/api.v1.schedules.$scheduleId.ts | 13 +++++-------- apps/webapp/app/routes/api.v1.schedules.ts | 7 +++---- .../app/routes/api.v1.tasks.$taskId.trigger.ts | 3 +-- apps/webapp/app/routes/api.v1.waitpoints.tokens.ts | 4 ++-- .../app/routes/api.v3.batches.$batchId.items.ts | 4 ---- .../realtime.v1.sessions.$session.$io.append.ts | 7 ++++++- 20 files changed, 64 insertions(+), 70 deletions(-) create mode 100644 .server-changes/sanitize-api-500-errors.md diff --git a/.server-changes/sanitize-api-500-errors.md b/.server-changes/sanitize-api-500-errors.md new file mode 100644 index 00000000000..1621e15a16f --- /dev/null +++ b/.server-changes/sanitize-api-500-errors.md @@ -0,0 +1,6 @@ +--- +area: webapp +type: fix +--- + +Stop leaking raw exception messages on 500 responses across webapp API routes; return a generic error string and log the full error server-side instead. diff --git a/apps/webapp/app/routes/admin.api.v1.platform-notifications.ts b/apps/webapp/app/routes/admin.api.v1.platform-notifications.ts index 3798d9fa734..f3e4d89bdda 100644 --- a/apps/webapp/app/routes/admin.api.v1.platform-notifications.ts +++ b/apps/webapp/app/routes/admin.api.v1.platform-notifications.ts @@ -1,5 +1,6 @@ import { type ActionFunctionArgs, json } from "@remix-run/server-runtime"; import { err, ok, type Result } from "neverthrow"; +import { logger } from "~/services/logger.server"; import { authenticateAdminRequest } from "~/services/personalAccessToken.server"; import { createPlatformNotification, @@ -42,7 +43,8 @@ export async function action({ request }: ActionFunctionArgs) { return json({ error: "Validation failed", details: error.issues }, { status: 400 }); } - return json({ error: error.message }, { status: 500 }); + logger.error("Failed to create platform notification", { error }); + return json({ error: "Something went wrong" }, { status: 500 }); } return json(result.value, { status: 201 }); diff --git a/apps/webapp/app/routes/admin.notifications.tsx b/apps/webapp/app/routes/admin.notifications.tsx index 179ab23c3ee..44e93ecbd99 100644 --- a/apps/webapp/app/routes/admin.notifications.tsx +++ b/apps/webapp/app/routes/admin.notifications.tsx @@ -37,6 +37,7 @@ import { TableRow, } from "~/components/primitives/Table"; import { prisma } from "~/db.server"; +import { logger } from "~/services/logger.server"; import { requireUserId } from "~/services/session.server"; import { archivePlatformNotification, @@ -234,7 +235,8 @@ async function handleCreateAction(formData: FormData, userId: string, isPreview: { status: 400 } ); } - return typedjson({ error: err.message }, { status: 500 }); + logger.error("Failed to create platform notification", { error: err }); + return typedjson({ error: "Something went wrong" }, { status: 500 }); } if (isPreview) { @@ -310,7 +312,8 @@ async function handleEditAction(formData: FormData) { { status: 400 } ); } - return typedjson({ error: err.message }, { status: 500 }); + logger.error("Failed to update platform notification", { error: err }); + return typedjson({ error: "Something went wrong" }, { status: 500 }); } return typedjson({ success: true, id: result.value.id }); diff --git a/apps/webapp/app/routes/api.v1.batches.$batchParam.results.ts b/apps/webapp/app/routes/api.v1.batches.$batchParam.results.ts index 7eb2fd4207b..33ac786bb7f 100644 --- a/apps/webapp/app/routes/api.v1.batches.$batchParam.results.ts +++ b/apps/webapp/app/routes/api.v1.batches.$batchParam.results.ts @@ -4,6 +4,7 @@ import { z } from "zod"; import { ApiBatchResultsPresenter } from "~/presenters/v3/ApiBatchResultsPresenter.server"; import { ApiRunResultPresenter } from "~/presenters/v3/ApiRunResultPresenter.server"; import { authenticateApiRequest } from "~/services/apiAuth.server"; +import { logger } from "~/services/logger.server"; const ParamsSchema = z.object({ /* This is the batch friendly ID */ @@ -36,10 +37,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { return json(result); } catch (error) { - if (error instanceof Error) { - return json({ error: error.message }, { status: 500 }); - } else { - return json({ error: JSON.stringify(error) }, { status: 500 }); - } + logger.error("Failed to load batch results", { error }); + return json({ error: "Something went wrong" }, { status: 500 }); } } diff --git a/apps/webapp/app/routes/api.v1.deployments.ts b/apps/webapp/app/routes/api.v1.deployments.ts index 0190ba123d5..8fa5b432950 100644 --- a/apps/webapp/app/routes/api.v1.deployments.ts +++ b/apps/webapp/app/routes/api.v1.deployments.ts @@ -55,13 +55,10 @@ export async function action({ request, params }: ActionFunctionArgs) { } catch (error) { if (error instanceof ServiceValidationError) { return json({ error: error.message }, { status: 400 }); - } else if (error instanceof Error) { - logger.error("Error initializing deployment", { error: error.message }); - return json({ error: `Internal server error: ${error.message}` }, { status: 500 }); - } else { - logger.error("Error initializing deployment", { error: String(error) }); - return json({ error: "Internal server error" }, { status: 500 }); } + + logger.error("Error initializing deployment", { error }); + return json({ error: "Internal server error" }, { status: 500 }); } } diff --git a/apps/webapp/app/routes/api.v1.projects.$projectRef.alertChannels.ts b/apps/webapp/app/routes/api.v1.projects.$projectRef.alertChannels.ts index ebc5b176478..53c457ef67a 100644 --- a/apps/webapp/app/routes/api.v1.projects.$projectRef.alertChannels.ts +++ b/apps/webapp/app/routes/api.v1.projects.$projectRef.alertChannels.ts @@ -5,6 +5,7 @@ import { ApiAlertChannelPresenter, ApiCreateAlertChannel, } from "~/presenters/v3/ApiAlertChannelPresenter.server"; +import { logger } from "~/services/logger.server"; import { authenticateApiRequestWithPersonalAccessToken } from "~/services/personalAccessToken.server"; import { CreateAlertChannelService } from "~/v3/services/alerts/createAlertChannel.server"; import { ServiceValidationError } from "~/v3/services/baseService.server"; @@ -88,9 +89,7 @@ export async function action({ request, params }: ActionFunctionArgs) { return json({ error: error.message }, { status: 422 }); } - return json( - { error: error instanceof Error ? error.message : "Internal Server Error" }, - { status: 500 } - ); + logger.error("Failed to create alert channel", { error }); + return json({ error: "Something went wrong" }, { status: 500 }); } } diff --git a/apps/webapp/app/routes/api.v1.queues.ts b/apps/webapp/app/routes/api.v1.queues.ts index 551b3c2f34f..d2f2f32e720 100644 --- a/apps/webapp/app/routes/api.v1.queues.ts +++ b/apps/webapp/app/routes/api.v1.queues.ts @@ -2,6 +2,7 @@ import { json } from "@remix-run/server-runtime"; import { type QueueItem } from "@trigger.dev/core/v3"; import { z } from "zod"; import { QueueListPresenter } from "~/presenters/v3/QueueListPresenter.server"; +import { logger } from "~/services/logger.server"; import { createLoaderApiRoute } from "~/services/routeBuilders/apiBuilder.server"; import { ServiceValidationError } from "~/v3/services/baseService.server"; @@ -35,10 +36,8 @@ export const loader = createLoaderApiRoute( return json({ error: error.message }, { status: 422 }); } - return json( - { error: error instanceof Error ? error.message : "Internal Server Error" }, - { status: 500 } - ); + logger.error("Failed to list queues", { error }); + return json({ error: "Something went wrong" }, { status: 500 }); } } ); diff --git a/apps/webapp/app/routes/api.v1.runs.$runFriendlyId.input-streams.wait.ts b/apps/webapp/app/routes/api.v1.runs.$runFriendlyId.input-streams.wait.ts index a0f24f9abd8..0924bf3fc91 100644 --- a/apps/webapp/app/routes/api.v1.runs.$runFriendlyId.input-streams.wait.ts +++ b/apps/webapp/app/routes/api.v1.runs.$runFriendlyId.input-streams.wait.ts @@ -11,6 +11,7 @@ import { deleteInputStreamWaitpoint, setInputStreamWaitpoint, } from "~/services/inputStreamWaitpointCache.server"; +import { logger } from "~/services/logger.server"; import { getRealtimeStreamInstance } from "~/services/realtime/v1StreamsGlobal.server"; import { createActionApiRoute } from "~/services/routeBuilders/apiBuilder.server"; import { parseDelay } from "~/utils/delays"; @@ -138,10 +139,9 @@ const { action, loader } = createActionApiRoute( } catch (error) { if (error instanceof ServiceValidationError) { return json({ error: error.message }, { status: 422 }); - } else if (error instanceof Error) { - return json({ error: error.message }, { status: 500 }); } + logger.error("Failed to create input-stream waitpoint", { error }); return json({ error: "Something went wrong" }, { status: 500 }); } } diff --git a/apps/webapp/app/routes/api.v1.runs.$runId.tags.ts b/apps/webapp/app/routes/api.v1.runs.$runId.tags.ts index 2a48ded529e..1b02b24af9e 100644 --- a/apps/webapp/app/routes/api.v1.runs.$runId.tags.ts +++ b/apps/webapp/app/routes/api.v1.runs.$runId.tags.ts @@ -4,6 +4,7 @@ import { z } from "zod"; import { prisma } from "~/db.server"; import { MAX_TAGS_PER_RUN } from "~/models/taskRunTag.server"; import { authenticateApiRequest } from "~/services/apiAuth.server"; +import { logger } from "~/services/logger.server"; const ParamsSchema = z.object({ runId: z.string(), @@ -85,9 +86,7 @@ export async function action({ request, params }: ActionFunctionArgs) { return json({ message: `Successfully set ${newTags.length} new tags.` }, { status: 200 }); } catch (error) { - return json( - { error: error instanceof Error ? error.message : "Internal Server Error" }, - { status: 500 } - ); + logger.error("Failed to add run tags", { error }); + return json({ error: "Something went wrong" }, { status: 500 }); } } diff --git a/apps/webapp/app/routes/api.v1.runs.$runParam.attempts.ts b/apps/webapp/app/routes/api.v1.runs.$runParam.attempts.ts index 33894f8493c..223f35ac9eb 100644 --- a/apps/webapp/app/routes/api.v1.runs.$runParam.attempts.ts +++ b/apps/webapp/app/routes/api.v1.runs.$runParam.attempts.ts @@ -2,6 +2,7 @@ import type { ActionFunctionArgs } from "@remix-run/server-runtime"; import { json } from "@remix-run/server-runtime"; import { z } from "zod"; import { authenticateApiRequest } from "~/services/apiAuth.server"; +import { logger } from "~/services/logger.server"; import { ServiceValidationError } from "~/v3/services/baseService.server"; import { CreateTaskRunAttemptService } from "~/v3/services/createTaskRunAttempt.server"; @@ -40,9 +41,7 @@ export async function action({ request, params }: ActionFunctionArgs) { return json({ error: error.message }, { status: error.status ?? 422 }); } - return json( - { error: error instanceof Error ? error.message : "Internal Server Error" }, - { status: 500 } - ); + logger.error("Failed to create run attempt", { error }); + return json({ error: "Something went wrong" }, { status: 500 }); } } diff --git a/apps/webapp/app/routes/api.v1.runs.$runParam.reschedule.ts b/apps/webapp/app/routes/api.v1.runs.$runParam.reschedule.ts index f4e08831f4f..5f652d63809 100644 --- a/apps/webapp/app/routes/api.v1.runs.$runParam.reschedule.ts +++ b/apps/webapp/app/routes/api.v1.runs.$runParam.reschedule.ts @@ -6,6 +6,7 @@ import { getApiVersion } from "~/api/versions"; import { prisma } from "~/db.server"; import { ApiRetrieveRunPresenter } from "~/presenters/v3/ApiRetrieveRunPresenter.server"; import { authenticateApiRequest } from "~/services/apiAuth.server"; +import { logger } from "~/services/logger.server"; import { ServiceValidationError } from "~/v3/services/baseService.server"; import { RescheduleTaskRunService } from "~/v3/services/rescheduleTaskRun.server"; @@ -84,10 +85,9 @@ export async function action({ request, params }: ActionFunctionArgs) { } catch (error) { if (error instanceof ServiceValidationError) { return json({ error: error.message }, { status: 400 }); - } else if (error instanceof Error) { - return json({ error: error.message }, { status: 500 }); - } else { - return json({ error: "An unknown error occurred" }, { status: 500 }); } + + logger.error("Failed to reschedule run", { error }); + return json({ error: "Something went wrong" }, { status: 500 }); } } diff --git a/apps/webapp/app/routes/api.v1.runs.$runParam.result.ts b/apps/webapp/app/routes/api.v1.runs.$runParam.result.ts index 16343a91434..40b7f7153be 100644 --- a/apps/webapp/app/routes/api.v1.runs.$runParam.result.ts +++ b/apps/webapp/app/routes/api.v1.runs.$runParam.result.ts @@ -3,6 +3,7 @@ import { json } from "@remix-run/server-runtime"; import { z } from "zod"; import { ApiRunResultPresenter } from "~/presenters/v3/ApiRunResultPresenter.server"; import { authenticateApiRequest } from "~/services/apiAuth.server"; +import { logger } from "~/services/logger.server"; const ParamsSchema = z.object({ /* This is the run friendly ID */ @@ -35,10 +36,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { return json(result); } catch (error) { - if (error instanceof Error) { - return json({ error: error.message }, { status: 500 }); - } else { - return json({ error: JSON.stringify(error) }, { status: 500 }); - } + logger.error("Failed to load run result", { error }); + return json({ error: "Something went wrong" }, { status: 500 }); } } diff --git a/apps/webapp/app/routes/api.v1.schedules.$scheduleId.activate.ts b/apps/webapp/app/routes/api.v1.schedules.$scheduleId.activate.ts index 7eb281c0520..4648c664cd6 100644 --- a/apps/webapp/app/routes/api.v1.schedules.$scheduleId.activate.ts +++ b/apps/webapp/app/routes/api.v1.schedules.$scheduleId.activate.ts @@ -5,6 +5,7 @@ import { prisma } from "~/db.server"; import { scheduleUniqWhereClause, scheduleWhereClause } from "~/models/schedules.server"; import { ViewSchedulePresenter } from "~/presenters/v3/ViewSchedulePresenter.server"; import { authenticateApiRequest } from "~/services/apiAuth.server"; +import { logger } from "~/services/logger.server"; const ParamsSchema = z.object({ scheduleId: z.string(), @@ -68,9 +69,7 @@ export async function action({ request, params }: ActionFunctionArgs) { return json(presenter.toJSONResponse(result), { status: 200 }); } catch (error) { - return json( - { error: error instanceof Error ? error.message : "Internal Server Error" }, - { status: 500 } - ); + logger.error("Failed to activate schedule", { error }); + return json({ error: "Something went wrong" }, { status: 500 }); } } diff --git a/apps/webapp/app/routes/api.v1.schedules.$scheduleId.deactivate.ts b/apps/webapp/app/routes/api.v1.schedules.$scheduleId.deactivate.ts index e9b2997116f..903c8d8d96c 100644 --- a/apps/webapp/app/routes/api.v1.schedules.$scheduleId.deactivate.ts +++ b/apps/webapp/app/routes/api.v1.schedules.$scheduleId.deactivate.ts @@ -5,6 +5,7 @@ import { prisma } from "~/db.server"; import { scheduleUniqWhereClause, scheduleWhereClause } from "~/models/schedules.server"; import { ViewSchedulePresenter } from "~/presenters/v3/ViewSchedulePresenter.server"; import { authenticateApiRequest } from "~/services/apiAuth.server"; +import { logger } from "~/services/logger.server"; const ParamsSchema = z.object({ scheduleId: z.string(), @@ -68,9 +69,7 @@ export async function action({ request, params }: ActionFunctionArgs) { return json(presenter.toJSONResponse(result), { status: 200 }); } catch (error) { - return json( - { error: error instanceof Error ? error.message : "Internal Server Error" }, - { status: 500 } - ); + logger.error("Failed to deactivate schedule", { error }); + return json({ error: "Something went wrong" }, { status: 500 }); } } diff --git a/apps/webapp/app/routes/api.v1.schedules.$scheduleId.ts b/apps/webapp/app/routes/api.v1.schedules.$scheduleId.ts index b9fc8e2caff..5072c2cbc97 100644 --- a/apps/webapp/app/routes/api.v1.schedules.$scheduleId.ts +++ b/apps/webapp/app/routes/api.v1.schedules.$scheduleId.ts @@ -6,6 +6,7 @@ import { Prisma, prisma } from "~/db.server"; import { scheduleUniqWhereClause } from "~/models/schedules.server"; import { ViewSchedulePresenter } from "~/presenters/v3/ViewSchedulePresenter.server"; import { authenticateApiRequest } from "~/services/apiAuth.server"; +import { logger } from "~/services/logger.server"; import { UpsertSchedule } from "~/v3/schedules"; import { ServiceValidationError } from "~/v3/services/baseService.server"; import { UpsertTaskScheduleService } from "~/v3/services/upsertTaskSchedule.server"; @@ -57,10 +58,8 @@ export async function action({ request, params }: ActionFunctionArgs) { { status: error.code === "P2025" ? 404 : 422 } ); } else { - return json( - { error: error instanceof Error ? error.message : "Internal Server Error" }, - { status: 500 } - ); + logger.error("Failed to delete schedule", { error }); + return json({ error: "Something went wrong" }, { status: 500 }); } } } @@ -110,10 +109,8 @@ export async function action({ request, params }: ActionFunctionArgs) { return json({ error: error.message }, { status: 422 }); } - return json( - { error: error instanceof Error ? error.message : "Internal Server Error" }, - { status: 500 } - ); + logger.error("Failed to upsert schedule", { error }); + return json({ error: "Something went wrong" }, { status: 500 }); } } } diff --git a/apps/webapp/app/routes/api.v1.schedules.ts b/apps/webapp/app/routes/api.v1.schedules.ts index beb232c9757..5cec09cc4e2 100644 --- a/apps/webapp/app/routes/api.v1.schedules.ts +++ b/apps/webapp/app/routes/api.v1.schedules.ts @@ -4,6 +4,7 @@ import { CreateScheduleOptions, ScheduleObject } from "@trigger.dev/core/v3"; import { z } from "zod"; import { ScheduleListPresenter } from "~/presenters/v3/ScheduleListPresenter.server"; import { authenticateApiRequest } from "~/services/apiAuth.server"; +import { logger } from "~/services/logger.server"; import { UpsertSchedule } from "~/v3/schedules"; import { ServiceValidationError } from "~/v3/services/baseService.server"; import { UpsertTaskScheduleService } from "~/v3/services/upsertTaskSchedule.server"; @@ -71,10 +72,8 @@ export async function action({ request }: ActionFunctionArgs) { return json({ error: error.message }, { status: 422 }); } - return json( - { error: error instanceof Error ? error.message : "Internal Server Error" }, - { status: 500 } - ); + logger.error("Failed to create schedule", { error }); + return json({ error: "Something went wrong" }, { status: 500 }); } } diff --git a/apps/webapp/app/routes/api.v1.tasks.$taskId.trigger.ts b/apps/webapp/app/routes/api.v1.tasks.$taskId.trigger.ts index 5811fc67709..e39f4b3cc8f 100644 --- a/apps/webapp/app/routes/api.v1.tasks.$taskId.trigger.ts +++ b/apps/webapp/app/routes/api.v1.tasks.$taskId.trigger.ts @@ -153,10 +153,9 @@ const { action, loader } = createActionApiRoute( return json({ error: error.message }, { status: error.status ?? 422 }); } else if (error instanceof OutOfEntitlementError) { return json({ error: error.message }, { status: 422 }); - } else if (error instanceof Error) { - return json({ error: error.message }, { status: 500 }); } + logger.error("Trigger task failed", { error }); return json({ error: "Something went wrong" }, { status: 500 }); } } diff --git a/apps/webapp/app/routes/api.v1.waitpoints.tokens.ts b/apps/webapp/app/routes/api.v1.waitpoints.tokens.ts index 4542236d488..b7ef988b728 100644 --- a/apps/webapp/app/routes/api.v1.waitpoints.tokens.ts +++ b/apps/webapp/app/routes/api.v1.waitpoints.tokens.ts @@ -10,6 +10,7 @@ import { ApiWaitpointListSearchParams, } from "~/presenters/v3/ApiWaitpointListPresenter.server"; import { type AuthenticatedEnvironment } from "~/services/apiAuth.server"; +import { logger } from "~/services/logger.server"; import { generateHttpCallbackUrl } from "~/services/httpCallback.server"; import { createActionApiRoute, @@ -92,10 +93,9 @@ const { action } = createActionApiRoute( } catch (error) { if (error instanceof ServiceValidationError) { return json({ error: error.message }, { status: 422 }); - } else if (error instanceof Error) { - return json({ error: error.message }, { status: 500 }); } + logger.error("Failed to create waitpoint token", { error }); return json({ error: "Something went wrong" }, { status: 500 }); } } diff --git a/apps/webapp/app/routes/api.v3.batches.$batchId.items.ts b/apps/webapp/app/routes/api.v3.batches.$batchId.items.ts index 0e26bae94e3..49e0d9053cc 100644 --- a/apps/webapp/app/routes/api.v3.batches.$batchId.items.ts +++ b/apps/webapp/app/routes/api.v3.batches.$batchId.items.ts @@ -112,10 +112,6 @@ export async function action({ request, params }: ActionFunctionArgs) { }, }); - if (error instanceof Error) { - return json({ error: error.message }, { status: 500 }); - } - return json({ error: "Something went wrong" }, { status: 500 }); } } diff --git a/apps/webapp/app/routes/realtime.v1.sessions.$session.$io.append.ts b/apps/webapp/app/routes/realtime.v1.sessions.$session.$io.append.ts index 45fbde5924b..91e6ec589ec 100644 --- a/apps/webapp/app/routes/realtime.v1.sessions.$session.$io.append.ts +++ b/apps/webapp/app/routes/realtime.v1.sessions.$session.$io.append.ts @@ -135,7 +135,12 @@ const { action, loader } = createActionApiRoute( { status: appendError.status ?? 422 } ); } - return json({ ok: false, error: appendError.message }, { status: 500 }); + logger.error("Failed to append to session stream", { + sessionId: session.id, + io: params.io, + error: appendError, + }); + return json({ ok: false, error: "Something went wrong" }, { status: 500 }); } // Fire any run-scoped waitpoints registered against this channel. Best From 6b877bc73d5c8da1d4b80864ef7e505e35069b11 Mon Sep 17 00:00:00 2001 From: Dan Sutton Date: Fri, 8 May 2026 10:47:00 +0100 Subject: [PATCH 2/5] fix(webapp): wrap admin.notifications archive/delete/publish handlers in try/catch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The three Prisma-backed dashboard action helpers (archivePlatformNotification, deletePlatformNotification, publishNowPlatformNotification) throw on underlying DB errors. Without try/catch in the route handler, the throw escapes to Remix's default action error response, which serialises message + stack into the JSON body — exposing server-side details and absolute build paths to the dashboard caller. Wrap each handler in try/catch with logger.error + a generic 500 typedjson body, matching the pattern already used by handleCreateAction and handleEditAction in the same file. --- .../webapp/app/routes/admin.notifications.tsx | 27 ++++++++++++++----- 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/apps/webapp/app/routes/admin.notifications.tsx b/apps/webapp/app/routes/admin.notifications.tsx index 44e93ecbd99..c5c0577cd7b 100644 --- a/apps/webapp/app/routes/admin.notifications.tsx +++ b/apps/webapp/app/routes/admin.notifications.tsx @@ -251,8 +251,13 @@ async function handleArchiveAction(formData: FormData) { return typedjson({ error: "Missing notificationId" }, { status: 400 }); } - await archivePlatformNotification(notificationId); - return typedjson({ success: true }); + try { + await archivePlatformNotification(notificationId); + return typedjson({ success: true }); + } catch (error) { + logger.error("Failed to archive platform notification", { error }); + return typedjson({ error: "Failed to archive notification" }, { status: 500 }); + } } async function handleDeleteAction(formData: FormData) { @@ -261,8 +266,13 @@ async function handleDeleteAction(formData: FormData) { return typedjson({ error: "Missing notificationId" }, { status: 400 }); } - await deletePlatformNotification(notificationId); - return typedjson({ success: true }); + try { + await deletePlatformNotification(notificationId); + return typedjson({ success: true }); + } catch (error) { + logger.error("Failed to delete platform notification", { error }); + return typedjson({ error: "Failed to delete notification" }, { status: 500 }); + } } async function handlePublishNowAction(formData: FormData) { @@ -271,8 +281,13 @@ async function handlePublishNowAction(formData: FormData) { return typedjson({ error: "Missing notificationId" }, { status: 400 }); } - await publishNowPlatformNotification(notificationId); - return typedjson({ success: true }); + try { + await publishNowPlatformNotification(notificationId); + return typedjson({ success: true }); + } catch (error) { + logger.error("Failed to publish platform notification", { error }); + return typedjson({ error: "Failed to publish notification" }, { status: 500 }); + } } async function handleEditAction(formData: FormData) { From 19f29b78ec6131bfe31e07c6b945ebb7f3713535 Mon Sep 17 00:00:00 2001 From: Dan Sutton Date: Fri, 8 May 2026 11:32:58 +0100 Subject: [PATCH 3/5] fix(webapp): include notificationId in admin.notifications error logs Add notificationId to the structured log context for the archive/delete/publishNow catch handlers so failures are traceable to the offending row. --- apps/webapp/app/routes/admin.notifications.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/webapp/app/routes/admin.notifications.tsx b/apps/webapp/app/routes/admin.notifications.tsx index c5c0577cd7b..0b92c795c53 100644 --- a/apps/webapp/app/routes/admin.notifications.tsx +++ b/apps/webapp/app/routes/admin.notifications.tsx @@ -255,7 +255,7 @@ async function handleArchiveAction(formData: FormData) { await archivePlatformNotification(notificationId); return typedjson({ success: true }); } catch (error) { - logger.error("Failed to archive platform notification", { error }); + logger.error("Failed to archive platform notification", { error, notificationId }); return typedjson({ error: "Failed to archive notification" }, { status: 500 }); } } @@ -270,7 +270,7 @@ async function handleDeleteAction(formData: FormData) { await deletePlatformNotification(notificationId); return typedjson({ success: true }); } catch (error) { - logger.error("Failed to delete platform notification", { error }); + logger.error("Failed to delete platform notification", { error, notificationId }); return typedjson({ error: "Failed to delete notification" }, { status: 500 }); } } @@ -285,7 +285,7 @@ async function handlePublishNowAction(formData: FormData) { await publishNowPlatformNotification(notificationId); return typedjson({ success: true }); } catch (error) { - logger.error("Failed to publish platform notification", { error }); + logger.error("Failed to publish platform notification", { error, notificationId }); return typedjson({ error: "Failed to publish notification" }, { status: 500 }); } } From 6c1fda8a9d4b82cd7cd45e9dc4c32ad0cf6e121d Mon Sep 17 00:00:00 2001 From: Dan Sutton Date: Fri, 8 May 2026 11:32:15 +0100 Subject: [PATCH 4/5] fix(webapp): sanitise 500 leaks on deployment finalize routes Devin's PR review flagged three deployment finalize routes (api.v1/v2/v3.deployments.$deploymentId.finalize.ts) that still surfaced raw `error.message` via the template-string variant (`Internal server error: ${error.message}`) on the catch-all 500 branch. Replace with a generic body and route the full error through logger.error for server-side visibility, matching the pattern used in the rest of this PR. Also fixes the same template-string leak inside the v3 SSE stream's .catch handler, where the raw message would have been written into the SSE event:error data payload. --- .../api.v1.deployments.$deploymentId.finalize.ts | 9 +++------ .../api.v2.deployments.$deploymentId.finalize.ts | 9 +++------ .../api.v3.deployments.$deploymentId.finalize.ts | 14 ++++---------- 3 files changed, 10 insertions(+), 22 deletions(-) diff --git a/apps/webapp/app/routes/api.v1.deployments.$deploymentId.finalize.ts b/apps/webapp/app/routes/api.v1.deployments.$deploymentId.finalize.ts index 9bd12e4bd3f..9bafd8644af 100644 --- a/apps/webapp/app/routes/api.v1.deployments.$deploymentId.finalize.ts +++ b/apps/webapp/app/routes/api.v1.deployments.$deploymentId.finalize.ts @@ -54,12 +54,9 @@ export async function action({ request, params }: ActionFunctionArgs) { } catch (error) { if (error instanceof ServiceValidationError) { return json({ error: error.message }, { status: 400 }); - } else if (error instanceof Error) { - logger.error("Error finalizing deployment", { error: error.message }); - return json({ error: `Internal server error: ${error.message}` }, { status: 500 }); - } else { - logger.error("Error finalizing deployment", { error: String(error) }); - return json({ error: "Internal server error" }, { status: 500 }); } + + logger.error("Error finalizing deployment", { error }); + return json({ error: "Internal server error" }, { status: 500 }); } } diff --git a/apps/webapp/app/routes/api.v2.deployments.$deploymentId.finalize.ts b/apps/webapp/app/routes/api.v2.deployments.$deploymentId.finalize.ts index 768212ee8dd..380f2c2e600 100644 --- a/apps/webapp/app/routes/api.v2.deployments.$deploymentId.finalize.ts +++ b/apps/webapp/app/routes/api.v2.deployments.$deploymentId.finalize.ts @@ -54,12 +54,9 @@ export async function action({ request, params }: ActionFunctionArgs) { } catch (error) { if (error instanceof ServiceValidationError) { return json({ error: error.message }, { status: 400 }); - } else if (error instanceof Error) { - logger.error("Error finalizing deployment", { error: error.message }); - return json({ error: `Internal server error: ${error.message}` }, { status: 500 }); - } else { - logger.error("Error finalizing deployment", { error: String(error) }); - return json({ error: "Internal server error" }, { status: 500 }); } + + logger.error("Error finalizing deployment", { error }); + return json({ error: "Internal server error" }, { status: 500 }); } } diff --git a/apps/webapp/app/routes/api.v3.deployments.$deploymentId.finalize.ts b/apps/webapp/app/routes/api.v3.deployments.$deploymentId.finalize.ts index d6594c2520d..f08b3ed0525 100644 --- a/apps/webapp/app/routes/api.v3.deployments.$deploymentId.finalize.ts +++ b/apps/webapp/app/routes/api.v3.deployments.$deploymentId.finalize.ts @@ -75,11 +75,8 @@ export async function action({ request, params }: ActionFunctionArgs) { if (error instanceof ServiceValidationError) { errorMessage = { error: error.message }; - } else if (error instanceof Error) { - logger.error("Error finalizing deployment", { error: error.message }); - errorMessage = { error: `Internal server error: ${error.message}` }; } else { - logger.error("Error finalizing deployment", { error: String(error) }); + logger.error("Error finalizing deployment", { error }); errorMessage = { error: "Internal server error" }; } @@ -93,12 +90,9 @@ export async function action({ request, params }: ActionFunctionArgs) { } catch (error) { if (error instanceof ServiceValidationError) { return json({ error: error.message }, { status: 400 }); - } else if (error instanceof Error) { - logger.error("Error finalizing deployment", { error: error.message }); - return json({ error: `Internal server error: ${error.message}` }, { status: 500 }); - } else { - logger.error("Error finalizing deployment", { error: String(error) }); - return json({ error: "Internal server error" }, { status: 500 }); } + + logger.error("Error finalizing deployment", { error }); + return json({ error: "Internal server error" }, { status: 500 }); } } From d559275bf71a02bad38599759369ac313892c7bb Mon Sep 17 00:00:00 2001 From: Dan Sutton Date: Fri, 8 May 2026 13:21:45 +0100 Subject: [PATCH 5/5] fix(webapp): soften 500 error copy with retry hint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update the user-facing copy I introduced in the 5xx sanitisation sweep to include a retry hint ("Something went wrong, please try again." / "Failed to notification, please try again."). Routes that already used "Internal server error" or "Something went wrong" before this PR are left as-is — no copy churn for unaffected routes. --- .../app/routes/admin.api.v1.platform-notifications.ts | 2 +- apps/webapp/app/routes/admin.notifications.tsx | 10 +++++----- .../app/routes/api.v1.batches.$batchParam.results.ts | 2 +- .../api.v1.projects.$projectRef.alertChannels.ts | 2 +- apps/webapp/app/routes/api.v1.queues.ts | 2 +- apps/webapp/app/routes/api.v1.runs.$runId.tags.ts | 2 +- .../app/routes/api.v1.runs.$runParam.attempts.ts | 2 +- .../app/routes/api.v1.runs.$runParam.reschedule.ts | 2 +- apps/webapp/app/routes/api.v1.runs.$runParam.result.ts | 2 +- .../routes/api.v1.schedules.$scheduleId.activate.ts | 2 +- .../routes/api.v1.schedules.$scheduleId.deactivate.ts | 2 +- apps/webapp/app/routes/api.v1.schedules.$scheduleId.ts | 4 ++-- apps/webapp/app/routes/api.v1.schedules.ts | 2 +- .../routes/realtime.v1.sessions.$session.$io.append.ts | 2 +- 14 files changed, 19 insertions(+), 19 deletions(-) diff --git a/apps/webapp/app/routes/admin.api.v1.platform-notifications.ts b/apps/webapp/app/routes/admin.api.v1.platform-notifications.ts index f3e4d89bdda..c0b59631864 100644 --- a/apps/webapp/app/routes/admin.api.v1.platform-notifications.ts +++ b/apps/webapp/app/routes/admin.api.v1.platform-notifications.ts @@ -44,7 +44,7 @@ export async function action({ request }: ActionFunctionArgs) { } logger.error("Failed to create platform notification", { error }); - return json({ error: "Something went wrong" }, { status: 500 }); + return json({ error: "Something went wrong, please try again." }, { status: 500 }); } return json(result.value, { status: 201 }); diff --git a/apps/webapp/app/routes/admin.notifications.tsx b/apps/webapp/app/routes/admin.notifications.tsx index 0b92c795c53..f05397d3c20 100644 --- a/apps/webapp/app/routes/admin.notifications.tsx +++ b/apps/webapp/app/routes/admin.notifications.tsx @@ -236,7 +236,7 @@ async function handleCreateAction(formData: FormData, userId: string, isPreview: ); } logger.error("Failed to create platform notification", { error: err }); - return typedjson({ error: "Something went wrong" }, { status: 500 }); + return typedjson({ error: "Something went wrong, please try again." }, { status: 500 }); } if (isPreview) { @@ -256,7 +256,7 @@ async function handleArchiveAction(formData: FormData) { return typedjson({ success: true }); } catch (error) { logger.error("Failed to archive platform notification", { error, notificationId }); - return typedjson({ error: "Failed to archive notification" }, { status: 500 }); + return typedjson({ error: "Failed to archive notification, please try again." }, { status: 500 }); } } @@ -271,7 +271,7 @@ async function handleDeleteAction(formData: FormData) { return typedjson({ success: true }); } catch (error) { logger.error("Failed to delete platform notification", { error, notificationId }); - return typedjson({ error: "Failed to delete notification" }, { status: 500 }); + return typedjson({ error: "Failed to delete notification, please try again." }, { status: 500 }); } } @@ -286,7 +286,7 @@ async function handlePublishNowAction(formData: FormData) { return typedjson({ success: true }); } catch (error) { logger.error("Failed to publish platform notification", { error, notificationId }); - return typedjson({ error: "Failed to publish notification" }, { status: 500 }); + return typedjson({ error: "Failed to publish notification, please try again." }, { status: 500 }); } } @@ -328,7 +328,7 @@ async function handleEditAction(formData: FormData) { ); } logger.error("Failed to update platform notification", { error: err }); - return typedjson({ error: "Something went wrong" }, { status: 500 }); + return typedjson({ error: "Something went wrong, please try again." }, { status: 500 }); } return typedjson({ success: true, id: result.value.id }); diff --git a/apps/webapp/app/routes/api.v1.batches.$batchParam.results.ts b/apps/webapp/app/routes/api.v1.batches.$batchParam.results.ts index 33ac786bb7f..1a5889fab1d 100644 --- a/apps/webapp/app/routes/api.v1.batches.$batchParam.results.ts +++ b/apps/webapp/app/routes/api.v1.batches.$batchParam.results.ts @@ -38,6 +38,6 @@ export async function loader({ request, params }: LoaderFunctionArgs) { return json(result); } catch (error) { logger.error("Failed to load batch results", { error }); - return json({ error: "Something went wrong" }, { status: 500 }); + return json({ error: "Something went wrong, please try again." }, { status: 500 }); } } diff --git a/apps/webapp/app/routes/api.v1.projects.$projectRef.alertChannels.ts b/apps/webapp/app/routes/api.v1.projects.$projectRef.alertChannels.ts index 53c457ef67a..a2f2dcf417f 100644 --- a/apps/webapp/app/routes/api.v1.projects.$projectRef.alertChannels.ts +++ b/apps/webapp/app/routes/api.v1.projects.$projectRef.alertChannels.ts @@ -90,6 +90,6 @@ export async function action({ request, params }: ActionFunctionArgs) { } logger.error("Failed to create alert channel", { error }); - return json({ error: "Something went wrong" }, { status: 500 }); + return json({ error: "Something went wrong, please try again." }, { status: 500 }); } } diff --git a/apps/webapp/app/routes/api.v1.queues.ts b/apps/webapp/app/routes/api.v1.queues.ts index d2f2f32e720..18c0f688370 100644 --- a/apps/webapp/app/routes/api.v1.queues.ts +++ b/apps/webapp/app/routes/api.v1.queues.ts @@ -37,7 +37,7 @@ export const loader = createLoaderApiRoute( } logger.error("Failed to list queues", { error }); - return json({ error: "Something went wrong" }, { status: 500 }); + return json({ error: "Something went wrong, please try again." }, { status: 500 }); } } ); diff --git a/apps/webapp/app/routes/api.v1.runs.$runId.tags.ts b/apps/webapp/app/routes/api.v1.runs.$runId.tags.ts index 1b02b24af9e..eae94375b9f 100644 --- a/apps/webapp/app/routes/api.v1.runs.$runId.tags.ts +++ b/apps/webapp/app/routes/api.v1.runs.$runId.tags.ts @@ -87,6 +87,6 @@ export async function action({ request, params }: ActionFunctionArgs) { return json({ message: `Successfully set ${newTags.length} new tags.` }, { status: 200 }); } catch (error) { logger.error("Failed to add run tags", { error }); - return json({ error: "Something went wrong" }, { status: 500 }); + return json({ error: "Something went wrong, please try again." }, { status: 500 }); } } diff --git a/apps/webapp/app/routes/api.v1.runs.$runParam.attempts.ts b/apps/webapp/app/routes/api.v1.runs.$runParam.attempts.ts index 223f35ac9eb..790e52bee4e 100644 --- a/apps/webapp/app/routes/api.v1.runs.$runParam.attempts.ts +++ b/apps/webapp/app/routes/api.v1.runs.$runParam.attempts.ts @@ -42,6 +42,6 @@ export async function action({ request, params }: ActionFunctionArgs) { } logger.error("Failed to create run attempt", { error }); - return json({ error: "Something went wrong" }, { status: 500 }); + return json({ error: "Something went wrong, please try again." }, { status: 500 }); } } diff --git a/apps/webapp/app/routes/api.v1.runs.$runParam.reschedule.ts b/apps/webapp/app/routes/api.v1.runs.$runParam.reschedule.ts index 5f652d63809..0ac8aec8351 100644 --- a/apps/webapp/app/routes/api.v1.runs.$runParam.reschedule.ts +++ b/apps/webapp/app/routes/api.v1.runs.$runParam.reschedule.ts @@ -88,6 +88,6 @@ export async function action({ request, params }: ActionFunctionArgs) { } logger.error("Failed to reschedule run", { error }); - return json({ error: "Something went wrong" }, { status: 500 }); + return json({ error: "Something went wrong, please try again." }, { status: 500 }); } } diff --git a/apps/webapp/app/routes/api.v1.runs.$runParam.result.ts b/apps/webapp/app/routes/api.v1.runs.$runParam.result.ts index 40b7f7153be..4cbf27d3275 100644 --- a/apps/webapp/app/routes/api.v1.runs.$runParam.result.ts +++ b/apps/webapp/app/routes/api.v1.runs.$runParam.result.ts @@ -37,6 +37,6 @@ export async function loader({ request, params }: LoaderFunctionArgs) { return json(result); } catch (error) { logger.error("Failed to load run result", { error }); - return json({ error: "Something went wrong" }, { status: 500 }); + return json({ error: "Something went wrong, please try again." }, { status: 500 }); } } diff --git a/apps/webapp/app/routes/api.v1.schedules.$scheduleId.activate.ts b/apps/webapp/app/routes/api.v1.schedules.$scheduleId.activate.ts index 4648c664cd6..9cc8b7173c7 100644 --- a/apps/webapp/app/routes/api.v1.schedules.$scheduleId.activate.ts +++ b/apps/webapp/app/routes/api.v1.schedules.$scheduleId.activate.ts @@ -70,6 +70,6 @@ export async function action({ request, params }: ActionFunctionArgs) { return json(presenter.toJSONResponse(result), { status: 200 }); } catch (error) { logger.error("Failed to activate schedule", { error }); - return json({ error: "Something went wrong" }, { status: 500 }); + return json({ error: "Something went wrong, please try again." }, { status: 500 }); } } diff --git a/apps/webapp/app/routes/api.v1.schedules.$scheduleId.deactivate.ts b/apps/webapp/app/routes/api.v1.schedules.$scheduleId.deactivate.ts index 903c8d8d96c..eb985bea728 100644 --- a/apps/webapp/app/routes/api.v1.schedules.$scheduleId.deactivate.ts +++ b/apps/webapp/app/routes/api.v1.schedules.$scheduleId.deactivate.ts @@ -70,6 +70,6 @@ export async function action({ request, params }: ActionFunctionArgs) { return json(presenter.toJSONResponse(result), { status: 200 }); } catch (error) { logger.error("Failed to deactivate schedule", { error }); - return json({ error: "Something went wrong" }, { status: 500 }); + return json({ error: "Something went wrong, please try again." }, { status: 500 }); } } diff --git a/apps/webapp/app/routes/api.v1.schedules.$scheduleId.ts b/apps/webapp/app/routes/api.v1.schedules.$scheduleId.ts index 5072c2cbc97..e76f65e6e8a 100644 --- a/apps/webapp/app/routes/api.v1.schedules.$scheduleId.ts +++ b/apps/webapp/app/routes/api.v1.schedules.$scheduleId.ts @@ -59,7 +59,7 @@ export async function action({ request, params }: ActionFunctionArgs) { ); } else { logger.error("Failed to delete schedule", { error }); - return json({ error: "Something went wrong" }, { status: 500 }); + return json({ error: "Something went wrong, please try again." }, { status: 500 }); } } } @@ -110,7 +110,7 @@ export async function action({ request, params }: ActionFunctionArgs) { } logger.error("Failed to upsert schedule", { error }); - return json({ error: "Something went wrong" }, { status: 500 }); + return json({ error: "Something went wrong, please try again." }, { status: 500 }); } } } diff --git a/apps/webapp/app/routes/api.v1.schedules.ts b/apps/webapp/app/routes/api.v1.schedules.ts index 5cec09cc4e2..56250eaac55 100644 --- a/apps/webapp/app/routes/api.v1.schedules.ts +++ b/apps/webapp/app/routes/api.v1.schedules.ts @@ -73,7 +73,7 @@ export async function action({ request }: ActionFunctionArgs) { } logger.error("Failed to create schedule", { error }); - return json({ error: "Something went wrong" }, { status: 500 }); + return json({ error: "Something went wrong, please try again." }, { status: 500 }); } } diff --git a/apps/webapp/app/routes/realtime.v1.sessions.$session.$io.append.ts b/apps/webapp/app/routes/realtime.v1.sessions.$session.$io.append.ts index 91e6ec589ec..a21b5202317 100644 --- a/apps/webapp/app/routes/realtime.v1.sessions.$session.$io.append.ts +++ b/apps/webapp/app/routes/realtime.v1.sessions.$session.$io.append.ts @@ -140,7 +140,7 @@ const { action, loader } = createActionApiRoute( io: params.io, error: appendError, }); - return json({ ok: false, error: "Something went wrong" }, { status: 500 }); + return json({ ok: false, error: "Something went wrong, please try again." }, { status: 500 }); } // Fire any run-scoped waitpoints registered against this channel. Best