Skip to content

fix(api): withhold internal failure messages from internal route responses - #7015

Merged
waleedlatif1 merged 1 commit into
stagingfrom
deslop-codebase
Aug 24, 2026
Merged

fix(api): withhold internal failure messages from internal route responses#7015
waleedlatif1 merged 1 commit into
stagingfrom
deslop-codebase

Conversation

@waleedlatif1

Copy link
Copy Markdown
Collaborator

What

An orchestration result carrying errorCode: 'internal' holds whatever text the fault happened to have. The workflow-lifecycle.ts catch-alls (:366, :514, :636) return toError(error).message, which for a failed transaction is the driver's SQL:

insert into "workflow" ("id") values ($1) - duplicate key value violates unique constraint "workflow_pkey"

Three application helpers projected that straight into an OrchestrationError, and internalOrchestrationErrorPolicy rendered classified.message into a 500 body — so raw SQL reached clients. The v2 envelope already scrubbed the same failures (app/api/v2/lib/response.ts:481); internal routes did not.

Why it drifted

messageForOrchestrationError already encoded the rule, and its TSDoc states it plainly: an unclassified failure "carries whatever text the fault happened to have — a driver's failed SQL, say — so the caller gets the route's own generic wording instead."

Two sites honored it. Three hand-rolled it and disagreed. workflow-vfs.ts disagreed with itself:

new OrchestrationError(
  result.errorCode ?? 'internal',                  // defaults the code
  result.errorCode === 'internal' ? '…' : result.error ?? '…'   // compares the RAW code
)

An uncoded failure was therefore classified internal (a 500) while still rendering its own message.

The fix

Two seams, deliberately both:

  1. throwOrchestrationFailure (lib/core/orchestration/types.ts) pairs the code with the message the policy permits for it, so the two cannot disagree. All four hand-rolled helpers collapse onto it. This is the semantic seam — it still knows which text was caller-written.
  2. The internal route boundary now scrubs too, matching v2. This is the containment seam — site N+1 cannot reopen the hole by forgetting the rule.

The boundary guard has no false-positive cost: new OrchestrationError('internal', …) has zero matches repo-wide, so no call site authors a curated internal message that could be masked.

Behavior change

internal-coded failures on internal routes now return the route's fallback wording instead of the underlying message. Status codes are unchanged. Two workflow-vfs/workflow-folders fallback strings collapse to one each; no test pinned them.

Testing

  • messageForOrchestrationError and throwOrchestrationFailure now have direct tests (previously untested despite 8 call sites).
  • Verified the tests fail without the fix — reverting transition-result.ts reproduces the leak, with the raw duplicate key value violates unique constraint text in the assertion output.
  • bun run type-check clean; bun run check:api-validation passes.
  • 3751 tests passing across app/api; 585 across the touched lib/ areas.

Provenance

Found by running mattpocock/skills code-review (Fowler smell baseline) over the codebase. Six sibling findings from the same sweep were refuted under adversarial verification and deliberately left out — this was the one that survived.

…onses

An orchestration result carrying `errorCode: 'internal'` holds whatever text
the fault happened to have — `workflow-lifecycle.ts` catch-alls return
`toError(error).message`, which is the driver's failed SQL. Three application
helpers projected that straight into an `OrchestrationError`, and the internal
route policy rendered its message into a 500 body, so raw SQL reached clients.
The v2 envelope already scrubbed the same failures; internal routes did not.

`messageForOrchestrationError` already encoded the rule and two sites honored
it. The three that hand-rolled it disagreed, and `workflow-vfs` disagreed with
itself: it defaulted the code with `?? 'internal'` but compared the raw
`errorCode` against `'internal'`, so an uncoded failure was classified
internal and still rendered its own message.

Pair the two in `throwOrchestrationFailure` so a code and its message cannot
disagree, and scrub at the internal route boundary as well, matching v2 — no
call site authors a curated `internal` message, so nothing legitimate is
masked, and site N+1 cannot reopen this by forgetting the rule.
@vercel

vercel Bot commented Aug 23, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Aug 23, 2026 6:46pm

Request Review

@cursor

cursor Bot commented Aug 23, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Touches API error bodies for internal routes (information disclosure). Status codes stay the same; classified client-facing messages are preserved.

Overview
Stops internal 500s from returning raw driver text (e.g. Postgres unique-constraint SQL) that orchestration catch-alls stuffed into errorCode: 'internal'.

Adds throwOrchestrationFailure so code and message always go through messageForOrchestrationError together. Folder/workflow helpers (requireWorkflowTransition, knowledge/workflow folder throws, VFS) no longer hand-roll that pair — the previous mismatch classified uncoded failures as internal while still rendering the raw message.

internalOrchestrationErrorPolicy now scrubs at the route boundary as well, matching v2. Classified failures still return their caller-facing text and status.

Reviewed by Cursor Bugbot for commit 017abbf. Configure here.

@greptile-apps

greptile-apps Bot commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR prevents internal and unclassified orchestration failures from exposing underlying infrastructure messages while preserving classified client-facing errors and their status mappings.

  • Adds a shared helper that consistently pairs orchestration error codes with permitted messages.
  • Applies defense-in-depth sanitization at the internal JSON route boundary.
  • Migrates workflow and knowledge-folder failure adapters to the shared behavior.
  • Adds focused tests for internal-message withholding and classified-error preservation.

Confidence Score: 5/5

The PR appears safe to merge, with internal failure details consistently withheld and classified client-facing errors preserved.

The shared helper and route boundary apply the intended sanitization without changing status mappings, and the reachable orchestration producers preserve actionable messages for non-internal classifications.

Important Files Changed

Filename Overview
apps/sim/lib/api/server/routes/internal-json-route.ts Sanitizes internal orchestration messages at the route boundary while preserving existing status mapping and classified messages.
apps/sim/lib/core/orchestration/types.ts Adds a centralized throwing helper that keeps orchestration error classification and message policy consistent.
apps/sim/lib/core/orchestration/types.test.ts Covers message withholding, classified-message preservation, missing-message fallback behavior, and helper output.
apps/sim/lib/workflows/application/transition-result.ts Delegates failed workflow transition conversion to the shared orchestration failure helper.
apps/sim/lib/workflows/application/transition-result.test.ts Verifies successful transitions, internal-message scrubbing, and preservation of classified failure status metadata.
apps/sim/lib/workflows/application/workflow-folders.ts Replaces hand-rolled folder mutation error conversion with the centralized policy.
apps/sim/lib/workflows/application/workflow-vfs.ts Fixes uncoded folder failures that previously received an internal code while retaining their underlying message.
apps/sim/lib/knowledge/application/folders.ts Prevents uncoded knowledge-folder failures from propagating raw underlying error text.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Orchestration result] --> B{Success?}
  B -->|Yes| C[Continue]
  B -->|No| D[throwOrchestrationFailure]
  D --> E{Internal or uncoded?}
  E -->|Yes| F[Use operation fallback]
  E -->|No| G[Preserve classified message]
  F --> H[OrchestrationError]
  G --> H
  H --> I[Internal route policy]
  I --> J{Internal code?}
  J -->|Yes| K[Return generic 500 message]
  J -->|No| L[Return classified message and mapped status]
Loading

Reviews (1): Last reviewed commit: "fix(api): withhold internal failure mess..." | Re-trigger Greptile

@waleedlatif1
waleedlatif1 merged commit fc7aa66 into staging Aug 24, 2026
30 checks passed
@waleedlatif1
waleedlatif1 deleted the deslop-codebase branch August 24, 2026 00:18
@waleedlatif1
waleedlatif1 restored the deslop-codebase branch August 24, 2026 00:19
@waleedlatif1
waleedlatif1 deleted the deslop-codebase branch August 24, 2026 00:20
waleedlatif1 added a commit that referenced this pull request Aug 24, 2026
…7047)

`check-utils-enforcement.ts` scanned line by line, and every idiom it bans is a
multi-token expression the formatter wraps at 100 columns. So it printed
`✓ No banned patterns found` while eleven files carried the wrapped form of

    e instanceof Error ? e.message : fallback

which CLAUDE.md mandates `getErrorMessage` for. The same class as the two blind
spots already fixed in check-react-query-patterns.

Patterns now run against the whole file, with match offsets mapped back to line
numbers by binary search over the line-start table — verified against every
offset of a multi-line fixture.

Eight of the eleven are now `getErrorMessage(error, fallback)`.
`auto-layout-utils` collapses a redundant `instanceof ApiClientError` arm on the
way, since that class extends `Error`; `upgrade.ts` keeps its `rawBody ?? message`
arm, which the helper cannot express, and only its tail collapses.

The other three stay, because the helper genuinely does not fit, and they carry a
`// utils-lint-allow: <reason>` annotation — the same escape hatch
check-react-query-patterns already has, which this gate lacked:

- the two auth routes return the message to an unauthenticated caller, so a
  non-Error throw must surface the fixed copy rather than its own text.
  `getErrorMessage` passes a thrown string straight through, which is the
  disclosure shape #7015 closed.
- `e2b.ts` probes E2B's own error shape — a record-like carrying `message` or
  `value` — which has no equivalent.

An annotation with no reason does not suppress, so the hatch cannot be used to
silence a finding without saying why.

Also corrects the header, which claimed Biome's `noRestrictedImports` covers
"crypto named imports". It lists only `nanoid` and `uuid`. Named crypto imports
pass both gates deliberately — server code building cipher IVs wants node's
crypto, not the cross-context wrapper — and the comment asserting otherwise would
mislead the next person auditing this.

Verified the gate can fail in both directions: reintroducing a wrapped ternary
reports it, and emptying an annotation's reason reports it too.
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.

1 participant