Skip to content

Commit 61ae67c

Browse files
authored
fix(webapp): stop leaking exception messages on 5xx API responses (#3536)
When a webapp API route's catch-all 500 branch handles a non-typed exception, it returns the raw `error.message` to the caller. If the exception originates from an internal subsystem (the ORM client, an infra dependency, etc.) the server-side error string is surfaced verbatim in the response body — exposing implementation details the API surface shouldn't carry. The leak shows up in three shapes across the routes: - `return json({ error: error.message }, { status: 500 })` - `return json({ error: error instanceof Error ? error.message : "Internal Server Error" }, { status: 500 })` - ``return json({ error: `Internal server error: ${error.message}` }, { status: 500 })`` (plus a couple of analogous neverthrow-Result variants on admin routes.) ## Fix Across 19 webapp routes, replace each leaking branch with a generic body (`"Something went wrong"` / `"Internal Server Error"` to match the file's existing fallback) and add `logger.error(...)` so full visibility is preserved server-side. Catch blocks that branch on typed user-input errors (`ServiceValidationError`, `EngineServiceValidationError`, `OutOfEntitlementError`, `PrismaClientKnownRequestError`) are left intact — those messages are constructed deliberately and intended to be customer-facing. ## Test plan - [x] `pnpm run typecheck --filter webapp` - [x] Per-route manual probe: inject a synthetic `Error` at the top of the catch'd `try` block (or fake the wrapped call's rejection / Result error), curl the route with the dev API key, confirm the response body changed from the synthetic message verbatim → generic body. 21/21 leak sites verified end-to-end. - [x] 4xx-typed-error paths spot-checked: throwing `ServiceValidationError` from inside the catch'd try still surfaces its message at 422 as intended.
1 parent 749dc46 commit 61ae67c

23 files changed

Lines changed: 95 additions & 98 deletions
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
area: webapp
3+
type: fix
4+
---
5+
6+
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.

apps/webapp/app/routes/admin.api.v1.platform-notifications.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { type ActionFunctionArgs, json } from "@remix-run/server-runtime";
22
import { err, ok, type Result } from "neverthrow";
3+
import { logger } from "~/services/logger.server";
34
import { authenticateAdminRequest } from "~/services/personalAccessToken.server";
45
import {
56
createPlatformNotification,
@@ -42,7 +43,8 @@ export async function action({ request }: ActionFunctionArgs) {
4243
return json({ error: "Validation failed", details: error.issues }, { status: 400 });
4344
}
4445

45-
return json({ error: error.message }, { status: 500 });
46+
logger.error("Failed to create platform notification", { error });
47+
return json({ error: "Something went wrong, please try again." }, { status: 500 });
4648
}
4749

4850
return json(result.value, { status: 201 });

apps/webapp/app/routes/admin.notifications.tsx

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import {
3737
TableRow,
3838
} from "~/components/primitives/Table";
3939
import { prisma } from "~/db.server";
40+
import { logger } from "~/services/logger.server";
4041
import { requireUserId } from "~/services/session.server";
4142
import {
4243
archivePlatformNotification,
@@ -234,7 +235,8 @@ async function handleCreateAction(formData: FormData, userId: string, isPreview:
234235
{ status: 400 }
235236
);
236237
}
237-
return typedjson({ error: err.message }, { status: 500 });
238+
logger.error("Failed to create platform notification", { error: err });
239+
return typedjson({ error: "Something went wrong, please try again." }, { status: 500 });
238240
}
239241

240242
if (isPreview) {
@@ -249,8 +251,13 @@ async function handleArchiveAction(formData: FormData) {
249251
return typedjson({ error: "Missing notificationId" }, { status: 400 });
250252
}
251253

252-
await archivePlatformNotification(notificationId);
253-
return typedjson({ success: true });
254+
try {
255+
await archivePlatformNotification(notificationId);
256+
return typedjson({ success: true });
257+
} catch (error) {
258+
logger.error("Failed to archive platform notification", { error, notificationId });
259+
return typedjson({ error: "Failed to archive notification, please try again." }, { status: 500 });
260+
}
254261
}
255262

256263
async function handleDeleteAction(formData: FormData) {
@@ -259,8 +266,13 @@ async function handleDeleteAction(formData: FormData) {
259266
return typedjson({ error: "Missing notificationId" }, { status: 400 });
260267
}
261268

262-
await deletePlatformNotification(notificationId);
263-
return typedjson({ success: true });
269+
try {
270+
await deletePlatformNotification(notificationId);
271+
return typedjson({ success: true });
272+
} catch (error) {
273+
logger.error("Failed to delete platform notification", { error, notificationId });
274+
return typedjson({ error: "Failed to delete notification, please try again." }, { status: 500 });
275+
}
264276
}
265277

266278
async function handlePublishNowAction(formData: FormData) {
@@ -269,8 +281,13 @@ async function handlePublishNowAction(formData: FormData) {
269281
return typedjson({ error: "Missing notificationId" }, { status: 400 });
270282
}
271283

272-
await publishNowPlatformNotification(notificationId);
273-
return typedjson({ success: true });
284+
try {
285+
await publishNowPlatformNotification(notificationId);
286+
return typedjson({ success: true });
287+
} catch (error) {
288+
logger.error("Failed to publish platform notification", { error, notificationId });
289+
return typedjson({ error: "Failed to publish notification, please try again." }, { status: 500 });
290+
}
274291
}
275292

276293
async function handleEditAction(formData: FormData) {
@@ -310,7 +327,8 @@ async function handleEditAction(formData: FormData) {
310327
{ status: 400 }
311328
);
312329
}
313-
return typedjson({ error: err.message }, { status: 500 });
330+
logger.error("Failed to update platform notification", { error: err });
331+
return typedjson({ error: "Something went wrong, please try again." }, { status: 500 });
314332
}
315333

316334
return typedjson({ success: true, id: result.value.id });

apps/webapp/app/routes/api.v1.batches.$batchParam.results.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { z } from "zod";
44
import { ApiBatchResultsPresenter } from "~/presenters/v3/ApiBatchResultsPresenter.server";
55
import { ApiRunResultPresenter } from "~/presenters/v3/ApiRunResultPresenter.server";
66
import { authenticateApiRequest } from "~/services/apiAuth.server";
7+
import { logger } from "~/services/logger.server";
78

89
const ParamsSchema = z.object({
910
/* This is the batch friendly ID */
@@ -36,10 +37,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) {
3637

3738
return json(result);
3839
} catch (error) {
39-
if (error instanceof Error) {
40-
return json({ error: error.message }, { status: 500 });
41-
} else {
42-
return json({ error: JSON.stringify(error) }, { status: 500 });
43-
}
40+
logger.error("Failed to load batch results", { error });
41+
return json({ error: "Something went wrong, please try again." }, { status: 500 });
4442
}
4543
}

apps/webapp/app/routes/api.v1.deployments.$deploymentId.finalize.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -54,12 +54,9 @@ export async function action({ request, params }: ActionFunctionArgs) {
5454
} catch (error) {
5555
if (error instanceof ServiceValidationError) {
5656
return json({ error: error.message }, { status: 400 });
57-
} else if (error instanceof Error) {
58-
logger.error("Error finalizing deployment", { error: error.message });
59-
return json({ error: `Internal server error: ${error.message}` }, { status: 500 });
60-
} else {
61-
logger.error("Error finalizing deployment", { error: String(error) });
62-
return json({ error: "Internal server error" }, { status: 500 });
6357
}
58+
59+
logger.error("Error finalizing deployment", { error });
60+
return json({ error: "Internal server error" }, { status: 500 });
6461
}
6562
}

apps/webapp/app/routes/api.v1.deployments.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,13 +55,10 @@ export async function action({ request, params }: ActionFunctionArgs) {
5555
} catch (error) {
5656
if (error instanceof ServiceValidationError) {
5757
return json({ error: error.message }, { status: 400 });
58-
} else if (error instanceof Error) {
59-
logger.error("Error initializing deployment", { error: error.message });
60-
return json({ error: `Internal server error: ${error.message}` }, { status: 500 });
61-
} else {
62-
logger.error("Error initializing deployment", { error: String(error) });
63-
return json({ error: "Internal server error" }, { status: 500 });
6458
}
59+
60+
logger.error("Error initializing deployment", { error });
61+
return json({ error: "Internal server error" }, { status: 500 });
6562
}
6663
}
6764

apps/webapp/app/routes/api.v1.projects.$projectRef.alertChannels.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
ApiAlertChannelPresenter,
66
ApiCreateAlertChannel,
77
} from "~/presenters/v3/ApiAlertChannelPresenter.server";
8+
import { logger } from "~/services/logger.server";
89
import { authenticateApiRequestWithPersonalAccessToken } from "~/services/personalAccessToken.server";
910
import { CreateAlertChannelService } from "~/v3/services/alerts/createAlertChannel.server";
1011
import { ServiceValidationError } from "~/v3/services/baseService.server";
@@ -88,9 +89,7 @@ export async function action({ request, params }: ActionFunctionArgs) {
8889
return json({ error: error.message }, { status: 422 });
8990
}
9091

91-
return json(
92-
{ error: error instanceof Error ? error.message : "Internal Server Error" },
93-
{ status: 500 }
94-
);
92+
logger.error("Failed to create alert channel", { error });
93+
return json({ error: "Something went wrong, please try again." }, { status: 500 });
9594
}
9695
}

apps/webapp/app/routes/api.v1.queues.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { json } from "@remix-run/server-runtime";
22
import { type QueueItem } from "@trigger.dev/core/v3";
33
import { z } from "zod";
44
import { QueueListPresenter } from "~/presenters/v3/QueueListPresenter.server";
5+
import { logger } from "~/services/logger.server";
56
import { createLoaderApiRoute } from "~/services/routeBuilders/apiBuilder.server";
67
import { ServiceValidationError } from "~/v3/services/baseService.server";
78

@@ -35,10 +36,8 @@ export const loader = createLoaderApiRoute(
3536
return json({ error: error.message }, { status: 422 });
3637
}
3738

38-
return json(
39-
{ error: error instanceof Error ? error.message : "Internal Server Error" },
40-
{ status: 500 }
41-
);
39+
logger.error("Failed to list queues", { error });
40+
return json({ error: "Something went wrong, please try again." }, { status: 500 });
4241
}
4342
}
4443
);

apps/webapp/app/routes/api.v1.runs.$runFriendlyId.input-streams.wait.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
deleteInputStreamWaitpoint,
1212
setInputStreamWaitpoint,
1313
} from "~/services/inputStreamWaitpointCache.server";
14+
import { logger } from "~/services/logger.server";
1415
import { getRealtimeStreamInstance } from "~/services/realtime/v1StreamsGlobal.server";
1516
import { createActionApiRoute } from "~/services/routeBuilders/apiBuilder.server";
1617
import { parseDelay } from "~/utils/delays";
@@ -138,10 +139,9 @@ const { action, loader } = createActionApiRoute(
138139
} catch (error) {
139140
if (error instanceof ServiceValidationError) {
140141
return json({ error: error.message }, { status: 422 });
141-
} else if (error instanceof Error) {
142-
return json({ error: error.message }, { status: 500 });
143142
}
144143

144+
logger.error("Failed to create input-stream waitpoint", { error });
145145
return json({ error: "Something went wrong" }, { status: 500 });
146146
}
147147
}

apps/webapp/app/routes/api.v1.runs.$runId.tags.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { z } from "zod";
44
import { prisma } from "~/db.server";
55
import { MAX_TAGS_PER_RUN } from "~/models/taskRunTag.server";
66
import { authenticateApiRequest } from "~/services/apiAuth.server";
7+
import { logger } from "~/services/logger.server";
78

89
const ParamsSchema = z.object({
910
runId: z.string(),
@@ -85,9 +86,7 @@ export async function action({ request, params }: ActionFunctionArgs) {
8586

8687
return json({ message: `Successfully set ${newTags.length} new tags.` }, { status: 200 });
8788
} catch (error) {
88-
return json(
89-
{ error: error instanceof Error ? error.message : "Internal Server Error" },
90-
{ status: 500 }
91-
);
89+
logger.error("Failed to add run tags", { error });
90+
return json({ error: "Something went wrong, please try again." }, { status: 500 });
9291
}
9392
}

0 commit comments

Comments
 (0)