Skip to content

fix(webapp): catch loader/action throws before Remix serializes them#3664

Merged
ericallam merged 6 commits into
mainfrom
sanitize-loader-action-leaks
May 19, 2026
Merged

fix(webapp): catch loader/action throws before Remix serializes them#3664
ericallam merged 6 commits into
mainfrom
sanitize-loader-action-leaks

Conversation

@d-cs
Copy link
Copy Markdown
Collaborator

@d-cs d-cs commented May 18, 2026

Summary

Companion to #3536, which patched routes that already had a leaking catch (e) { return json({error: e.message}, 500) }. That pattern can't reach routes which have no catch in the first place — when those throw, Remix's default error path serializes error.message into the response body, and the SDK then wraps the leaked string as TriggerApiError.

Across 28 raw api.v1 loaders/actions plus one dashboard polling endpoint, each handler now:

  • Wraps its body in try { ... } catch (error) { ... }.
  • Re-throws Response instances so auth helpers' throw json(...) / throw redirect(...) pass through unchanged.
  • Logs non-Response errors via logger.error so server-side visibility is preserved.
  • Returns a generic body — {"error": "Internal Server Error"} 500 for raw API routes, or { changelogs: [] } 200 for the polling widget (degrade silently across transient blips; the consumer hook already coped with empty payloads).

For six routes where #3536 left an inner try/catch covering only a service call (alertChannels, batches.results, deployments.finalize, deployments.background-workers, deployments.promote, projects.background-workers): an outer try/catch is added so auth/parsing failures are also sanitized. Inner typed-error handling (ServiceValidationError → 422 with message, etc.) is preserved exactly.

For two routes whose existing catch returned 400 + error.message (api.v1.authorization-code, api.v1.orgs.\$orgParam.projects action): the body is sanitized to a generic per-route string. Status code stays 400 — clients that key on the 4xx/5xx distinction (and the SDK's no-retry-on-4xx behavior) are unaffected.

Test plan

  • `pnpm run typecheck --filter webapp`
  • Per-route synthetic-throw probe: inject `throw new Error("SYNTHETIC ...")` at the top of each catch'd try, curl the route with a dummy bearer, confirm the response body is the generic shape and that the synthetic message lands server-side via `logger.error`. 29 routes verified.
  • Real-P1001 probe on the envvars loader: `docker stop database` mid-flight, confirm response is generic 500 (not the leaked Prisma message).
  • Sampled legitimate 4xx/2xx paths across each pattern variant (naked-wrap, partial-expanded, 400-preserved) to confirm the wraps don't interfere with normal control flow.

d-cs added 3 commits May 18, 2026 18:39
Two webapp routes left their loader/action bodies uncaught. When the
underlying call (Prisma, etc.) threw, Remix's default error path
serialized `error.message` into the 500 response body, surfacing
implementation detail to API consumers — and via the SDK, to users.

This complements the earlier sweep over `catch (e) { return json({error:
e.message}, 500) }` shapes; that fix could not reach routes which had no
catch in the first place.

Each handler now wraps its body in try/catch, re-throws `Response`
instances so auth helpers' `throw json(...)` / `throw redirect(...)`
pass through unchanged, logs non-Response errors, and returns a generic
body. The polling changelogs widget returns `{ changelogs: [] }` 200
instead of a 500 — degrading silently across a transient blip is better
UX for a 60s-cadence widget, and the leak risk is identical (neither
shape carries the error message).

Covers:
- apps/webapp/app/routes/api.v1.projects.\$projectRef.envvars.\$slug.\$name.ts (loader + action)
- apps/webapp/app/routes/resources.platform-changelogs.tsx (loader)
…leaks

Earlier passes covered routes with leaking `catch (e) { return json({error:
e.message}, 500) }` shapes, and two specific naked routes. This sweep
covers the rest of the API surface that doesn't go through
`createLoaderApiRoute`/`createActionApiRoute` — 26 files across deploy,
projects, orgs, deployments, auth-jwt, artifacts, and alert-channel routes.

Each handler now wraps its body in try/catch, re-throws `Response`
instances so auth helpers' `throw json(...)` / `throw redirect(...)`
pass through unchanged, logs non-Response errors via `logger.error`, and
returns a generic `{ error: "Internal Server Error" }` 500.

Routes that already had an inner try/catch covering a service call but
with auth/parsing outside the try (alertChannels, batches.results,
deployments.finalize, several others) get an outer try/catch added so the
inner typed-error handling is preserved. The api.v1.authorization-code.ts
catch branch was returning `error.message` verbatim — switched to a generic
body.

Each route verified locally with the established synthetic-throw probe:
inject `throw new Error("SYNTHETIC ...")` at the top of the catch'd try,
curl with a dummy bearer, confirm the response body is the generic shape
and that the synthetic message lands server-side via `logger.error`.
Sampled legitimate 4xx paths (no-auth 401s, auth-helper Response throws,
happy 200 returns) across each pattern variant to confirm the wrap does
not interfere with normal control flow.
…e leak

The previous sweep collapsed two existing 400 branches to 500 along with
the leak-sanitization. Keep the 400 status — the inner branches were
catching errors that callers had been informed of via 400, and clients
may depend on that. Just replace the leaky `error.message` body with a
generic per-route message.

- api.v1.orgs.$orgParam.projects.ts (createProject failure): 500 → 400
  with `"Failed to create project"`.
- api.v1.authorization-code.ts (instanceof Error branch): 500 → 400 with
  `"Failed to create authorization code"`.

Both branches probed locally: synthetic failure forces the path and the
response is the documented 400 + generic body.
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 18, 2026

⚠️ No Changeset found

Latest commit: af7bbde

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 18, 2026

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: f5ef4682-65c6-4ce5-8720-34103b965509

📥 Commits

Reviewing files that changed from the base of the PR and between dbca4f2 and af7bbde.

📒 Files selected for processing (1)
  • .server-changes/sanitize-loader-action-leaks.md
📜 Recent review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (14)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (6, 8)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (1, 8)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (2, 8)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (8, 8)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (4, 8)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (3, 8)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (7, 8)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (5, 8)
  • GitHub Check: e2e-webapp / 🧪 E2E Tests: Webapp
  • GitHub Check: typecheck / typecheck
  • GitHub Check: 🛡️ E2E Auth Tests (full)
  • GitHub Check: audit
  • GitHub Check: Analyze (javascript-typescript)
  • GitHub Check: Analyze (python)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2026-05-14T14:54:39.095Z
Learnt from: ericallam
Repo: triggerdotdev/trigger.dev PR: 3545
File: .server-changes/agent-view-sessions.md:10-10
Timestamp: 2026-05-14T14:54:39.095Z
Learning: In the `trigger.dev` repository, do not flag inconsistent dot vs slash notation in route/path strings inside `.server-changes/*.md` files. These markdown files are consumed verbatim into the changelog, so the mixed notation (e.g., `resources.orgs.../runs.$runParam/...`) is intentional and should be preserved as-is.

Applied to files:

  • .server-changes/sanitize-loader-action-leaks.md
🔇 Additional comments (1)
.server-changes/sanitize-loader-action-leaks.md (1)

1-6: LGTM!


Walkthrough

This PR adds centralized error handling to 30+ API route handlers across the webapp. Each affected route now wraps its main execution in a try/catch block that: rethrows Remix Response errors unchanged (preserving normal response control flow), logs non-Response exceptions via a server logger, and returns a standardized 500 JSON { error: "Internal Server Error" } response. Inner service-specific validation mappings remain intact. A changelog entry documents the expanded sanitization to prevent internal error messages (e.g., database exceptions) from leaking to API callers in 5xx response bodies. One route (api.v1.authorization-code) also changes its error message from raw error.message to a fixed generic message.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 6.25% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title accurately describes the main objective: adding error sanitization to loader/action handlers to prevent Remix from serializing thrown errors into responses.
Description check ✅ Passed The PR description is comprehensive and complete, covering summary, implementation details, test plan, and addressing specific patterns across multiple route categories.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch sanitize-loader-action-leaks

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@d-cs d-cs self-assigned this May 18, 2026
coderabbitai[bot]

This comment was marked as resolved.

@d-cs d-cs force-pushed the sanitize-loader-action-leaks branch from 6336713 to 7529ea8 Compare May 18, 2026 18:46
coderabbitai[bot]

This comment was marked as resolved.

@d-cs d-cs marked this pull request as ready for review May 18, 2026 19:19
Comment thread .server-changes/sanitize-api-loader-action-leaks-sweep.md Outdated
Comment thread .server-changes/sanitize-loader-action-leaks.md Outdated
@ericallam ericallam merged commit 2f261e5 into main May 19, 2026
32 checks passed
@ericallam ericallam deleted the sanitize-loader-action-leaks branch May 19, 2026 08:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants