Skip to content

fix(api): stop leaking failed SQL in v1 table 500s and restore the 423 lock field - #6569

Merged
waleedlatif1 merged 2 commits into
improvement/v2-route-standardizationfrom
fix/v1-response-parity
Aug 11, 2026
Merged

fix(api): stop leaking failed SQL in v1 table 500s and restore the 423 lock field#6569
waleedlatif1 merged 2 commits into
improvement/v2-route-standardizationfrom
fix/v1-response-parity

Conversation

@waleedlatif1

Copy link
Copy Markdown
Collaborator

Stacked on #6568#6567#6565#6560. Review only this PR's own commits; merge after its parents.

v1 is the live public API. It was rewritten to delegate to the shared application layer, and three response behaviors drifted from origin/main.

1. Failed SQL and bound parameters leaked in 500 bodies

performDeleteTable / performDeleteTableRow return toError(error).message for unclassified throws, and both routes rendered outcome.error verbatim. Both delete paths run inside locked transactions, where drizzle wraps a throw in a DrizzleQueryErrorwhose message is the failed SQL. main returned a fixed generic string.

Reverting the fix shows exactly what any API-key holder could harvest:

- "error": "Failed to delete table",
+ "error": "Failed query: delete from \"user_table\" where ... params: 2222..."

2. The 423 lock field was dropped

main returned { error, lock }; these routes returned { error } only, while still computing lock and discarding it. lock is the only thing telling a client which lock to clear.

3. Duplicate table name: 400 → 409 — keeping 409

This changed on POST /api/v1/tables, and the recommendation is to keep the new status rather than restore parity:

  • The OrchestrationError TSDoc documents the exact failure mode this replaced: "adding 'already exists' to a message demoted a 409 to a 400." Main's 400 came from string-matching 'already exists' — the anti-pattern the typed-code migration exists to kill.
  • v1 knowledge, v1 files, and v1 workflow-import already return 409. Tables was the lone outlier.
  • There is no v1 OpenAPI spec (all seven generated docs are v2), the v1 contract declares no error statuses, and no in-repo consumer branches on it.

Tradeoff accepted: a client matching 400 specifically for a name collision now sees 409 — but such a client already needs a 409 branch for every sibling v1 endpoint.

Fix

One shared projection, orchestrationOutcomeErrorResponse, the result-returning counterpart to the existing throw-path helper. Applied at nine call sites across three v1 and three internal table routes; statusForOrchestrationError no longer appears in any of them. It reuses messageForOrchestrationError, which genericizes only unclassified/internal — classified errors keep their specific text.

Also removes the rowWriteErrorResponse alias (15 sites): one touched file imported both names for the same function object and used one in PATCH, the other in DELETE.

Deliberately not converted:import, import-csv, and restore still hand-roll the projection. Converting them is a behavior change, not a tidy — the hand-rolled form returns outcome.error when errorCode is undefined, and that widening is the leak fix. It belongs in a change that owns the behavior.

Reverting turns 6 tests red. type-check · biome · 1113 tests · check:api-validation · check:openapi — all pass.

@vercel

vercelBot commented Aug 11, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
docsSkippedSkippedAug 11, 2026 11:25pm

Request Review

@cursor

cursorBot commented Aug 11, 2026

Copy link
Copy Markdown

PR Summary

High Risk
Touches public v1 table API error responses, including sanitization of internal failures and lock rejection payloads that clients rely on.

Overview
Stops unclassified orchestration failures from leaking driver SQL and bound parameters into public table API 500 bodies, and restores the lock field on 423 responses.

Adds shared orchestrationOutcomeErrorResponse so result-returning perform* failures use a safe fallback for internal/unclassified errors while keeping classified messages and including lock when present. Applied across v1 and internal table routes; also renames the throw-path alias rowWriteErrorResponse to orchestrationErrorResponse.

Pins the leak and lock behaviors with route and helper tests.

Reviewed by Cursor Bugbot for commit 5519d45. Configure here.

@greptile-apps

greptile-appsBot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR centralizes table orchestration-result error responses so internal failures use generic messages while classified errors retain their status and safe message.

  • Restores lock-kind metadata on rejected table mutations.
  • Applies the shared projection across v1 and internal table routes.
  • Consolidates row-write error classification under orchestrationErrorResponse.
  • Adds regression coverage for SQL-message concealment, lock responses, and classified failure statuses.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

FilenameOverview
apps/sim/app/api/table/utils.tsAdds the shared failure-result projection that conceals internal messages, preserves classified errors, and includes lock metadata.
apps/sim/app/api/v1/tables/[tableId]/route.tsRoutes delete failures through the shared projection to prevent SQL details from reaching v1 clients and restore the lock field.
apps/sim/app/api/v1/tables/[tableId]/rows/[rowId]/route.tsApplies the same safe projection to row deletion failures.
apps/sim/app/api/table/[tableId]/route.tsCentralizes lock, rename, move, and delete failure rendering while preserving the explicit move-table not-found message.
apps/sim/app/api/table/utils.test.tsCovers generic internal errors, classified failures, status mapping, and 423 lock metadata.
apps/sim/app/api/v1/tables/[tableId]/route.test.tsAdds route-level regression tests for SQL concealment and lock-kind responses.
apps/sim/app/api/v2/tables/utils.tsReplaces the removed row-write alias with its identical underlying classifier.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
A[Table route] --> B[perform mutation]
B -->|Success| C[Success response]
B -->|Failure result| D[orchestrationOutcomeErrorResponse]
D --> E{Error classification}
E -->|Internal or absent| F[Generic fallback and 500]
E -->|Locked| G[Specific error and lock field and 423]
E -->|Other classified| H[Specific safe error and mapped status]
Loading

Reviews (2): Last reviewed commit: "chore(tables): tidy v1 error projection ..." | Re-trigger Greptile

…ssage leak
The v1 table routes were rewritten to consume `lib/table/orchestration`
results, and two response behaviors drifted from what the live API returned.
Information disclosure: an unclassified failure's `outcome.error` carries
whatever text the fault happened to have. Drizzle wraps a throw raised inside
a transaction in an error whose own message is the failed statement and its
bound parameters, so `DELETE /api/v1/tables/{tableId}` and
`DELETE /api/v1/tables/{tableId}/rows/{rowId}` returned that verbatim in the
500 body to any API-key holder. Previously these returned a fixed generic
string.
Lost `lock` field: the 423 body used to be `{ error, lock }`. The delete,
row-delete, and column-update routes (v1 and internal) dropped the lock kind
the orchestration result already computes, leaving clients unable to tell
which lock to clear.
Both are fixed at one altitude: `orchestrationOutcomeErrorResponse` in
`app/api/table/utils.ts` is now the only way a table route projects an
orchestration failure onto the wire. It renders the route's fallback for an
unclassified failure and the real message for a classified one (validation,
not-found, conflict, locked keep their specific text), and carries `lock` on a
423. A future route cannot reintroduce either bug by hand-spelling the body.
Duplicate table names on `POST /api/v1/tables` keep answering 409 rather than
reverting to the previous 400. 409 is the correct semantic, and every other v1
duplicate-name surface (knowledge, files, workflow import) already answers 409;
the tables 400 was the outlier. v1 tables appears in no published OpenAPI
document and no in-repo client branches on the status, so the compatibility
cost is limited to a caller matching 400 specifically for a name collision.
@waleedlatif1
waleedlatif1force-pushed the chore/v2-dead-code-and-bounds branch from fe432f9 to 23b7792CompareAugust 11, 2026 23:25
@waleedlatif1
waleedlatif1force-pushed the fix/v1-response-parity branch from 12e43a6 to 5519d45CompareAugust 11, 2026 23:25
@waleedlatif1

Copy link
Copy Markdown
CollaboratorAuthor

@greptile-apps

@waleedlatif1

Copy link
Copy Markdown
CollaboratorAuthor

@cursor review

@cursorcursorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 5519d45. Configure here.

@waleedlatif1
waleedlatif1 changed the base branch from chore/v2-dead-code-and-bounds to improvement/v2-route-standardizationAugust 11, 2026 23:35
@waleedlatif1
waleedlatif1 merged commit 262ce32 into improvement/v2-route-standardizationAug 11, 2026
5 checks passed
@waleedlatif1
waleedlatif1 deleted the fix/v1-response-parity branch August 11, 2026 23:36
Sign up for freeto 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

@waleedlatif1