Skip to content

fix(api): give every v1 endpoint quota headers and errors that name the field - #6012

Merged
waleedlatif1 merged 6 commits into
stagingfrom
v1-api-consistency
Jul 28, 2026
Merged

fix(api): give every v1 endpoint quota headers and errors that name the field#6012
waleedlatif1 merged 6 commits into
stagingfrom
v1-api-consistency

Conversation

@waleedlatif1

Copy link
Copy Markdown
Collaborator

Summary

Three consistency gaps found by probing the live v1 surface end to end after #6011.

1. Rate-limit headers were missing on tables / files / knowledge. Only routes built on createApiResponse (workflows, logs, audit-logs) published them:

/workflows   limit=200    remaining=391
/tables      ABSENT       ABSENT
/files       ABSENT       ABSENT
/knowledge   ABSENT       ABSENT

Those endpoints share the same bucket and will return 429 — clients just couldn't see the ceiling until they hit it. Adds a shared rateLimitHeaders() builder (reused by createRateLimitResponse) and attaches it to all 31 success responses across the three families.

2. #6011's error-message fix reached almost nothing. .min(1, '...') only fires for a present-but-empty string, so an omitted field fell through to Zod's default. I fixed the shared id schemas last time — but 22 of 23 v1 workspaceId declarations bypassed them, so /workflows without workspaceId still returned Invalid input: expected string, received undefined. Adds requiredFieldSchema(message) and routes every v1 request input through it.

Deliberately preserves each site's more specific wording ('workspaceId query parameter is required') rather than flattening to the generic message — and leaves response schemas alone, where "required" wording would be wrong.

3. tables/files/knowledge reported a bare "Validation error" and threw away the schema's message. Adds v1ValidationErrorResponse, wired into the 19 parseRequest calls that had no handler. Routes with intentionally specific messages keep theirs.

I did not change the global validationErrorResponse default — routes outside v1 (copilot, providers) assert that literal in their tests, so a blanket change would have been the wrong blast radius.

Not included

Two things I found and deliberately left alone: the two response envelopes ({data, limits} vs {data, success}) are inconsistent but correctly documented in OpenAPI, and unifying them is breaking; and /tables/{id} returning 400 rather than 404 for an unknown id follows from its workspaceId-scoped addressing.

Type of Change

  • Bug fix

Testing

472 tests pass across app/api/v1/, lib/api/, app/api/workflows/ and lib/core/rate-limiter/. New coverage for rateLimitHeaders (consistent pair, empty when no bucket was consulted) and v1ValidationErrorResponse (surfaces the message, keeps details).

Three route test files mocked @/app/api/v1/middleware and needed the new exports added to their mocks — caught by the suite, fixed.

Typecheck, lint, check:api-validation:strict and the monorepo boundary check all pass.

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

…he field

Three consistency gaps found by probing the live v1 surface end to end.

Rate-limit headers were only published by routes built on createApiResponse —
workflows, logs and audit-logs. Tables, files and knowledge are rate limited by
the same bucket and will return 429, but published no quota on success, so a
client discovered the ceiling only by hitting it. Adds a shared
rateLimitHeaders() builder, reused by createRateLimitResponse, and attaches it
to all 31 success responses on those three families.

Missing required fields did not name themselves. .min(1, '...') only fires for
a present-but-empty string, so an omitted field fell through to Zod's default
"Invalid input: expected string, received undefined". A previous pass fixed the
shared id schemas, but 22 of 23 v1 workspaceId declarations bypassed them, so
the fix reached almost nothing. Adds requiredFieldSchema(message) and routes
every v1 request input through it, preserving each site's existing, more
specific wording (for example "workspaceId query parameter is required")
instead of flattening them to the generic one. Response schemas are left alone
— "required" wording would be wrong there.

Validation failures on tables, files and knowledge reported the literal
"Validation error" and discarded the schema's message. Adds
v1ValidationErrorResponse, which surfaces the first issue while keeping
details, and wires it into the 19 parseRequest calls that had no handler.
Routes with deliberately specific wording keep theirs. The global default is
untouched, since routes outside v1 assert the current string.
@vercel

vercel Bot commented Jul 28, 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 Jul 28, 2026 7:50pm

Request Review

@cursor

cursor Bot commented Jul 28, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Touches many v1 route handlers and response shaping (headers on all exit paths); behavior is additive for clients but changes error text and header presence on some endpoints.

Overview
Aligns the public v1 API so clients get consistent quota visibility and clearer 400s.

Rate limits: Authenticated v1 handlers now record a per-request snapshot when checkRateLimit runs; withRouteHandler stamps X-RateLimit-* on every response (including validation and other 4xx), not only successes that used createApiResponse. Tables, files, and knowledge routes gain the same headers workflows/logs already had; 429 responses use a shared buildRateLimitHeaders helper. OpenAPI documents those headers on successful responses across workflows, logs, audit, tables, files, and knowledge.

Validation errors: Adds requiredFieldSchema so omitted required fields (especially workspaceId) return named messages instead of Zod’s generic “expected string, received undefined”. V1 contracts and table/knowledge/file schemas are updated to use it. Adds v1ValidationErrorResponse / v1ValidationErrorResponseFromError and wires them into v1 parseRequest calls that previously returned a bare "Validation error".

Reviewed by Cursor Bugbot for commit 3a22a75. Configure here.

@greptile-apps

greptile-apps Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Follow-up pass on v1 API consistency for quota headers and field-named validation errors.

  • Centralizes rate-limit snapshots and stamps X-RateLimit-* on v1 responses via withRouteHandler.
  • Adds v1ValidationErrorResponse / requiredFieldSchema and wires them through v1 contracts and routes (including table row mutations).
  • Documents rate-limit response headers in OpenAPI; tests cover the new helpers and mocks.

Confidence Score: 5/5

Safe to merge; prior incomplete validation-header and download rate-limit gaps are addressed and no blocking failures remain.

Table row mutations now surface schema field messages via v1ValidationErrorResponse, and file download responses receive X-RateLimit headers through checkRateLimit snapshots applied in withRouteHandler. No blocking failure remains.

Important Files Changed

Filename Overview
apps/sim/lib/core/utils/with-route-handler.ts Applies request-id and recorded rate-limit headers on success and unhandled-error responses.
apps/sim/lib/api/server/rate-limit-context.ts Request-scoped WeakMap snapshot plus shared X-RateLimit header builder.
apps/sim/app/api/v1/middleware.ts Records rate-limit snapshots on check; adds v1 validation error helpers.
apps/sim/app/api/v1/tables/[tableId]/rows/route.ts Row list/create/update/delete parseRequest paths use v1ValidationErrorResponse.
apps/sim/app/api/v1/tables/[tableId]/rows/[rowId]/route.ts PATCH (and other handlers) pass v1ValidationErrorResponse for contract failures.
apps/sim/app/api/v1/files/[fileId]/route.ts Download/delete validation uses v1 helper; download binary responses get quota headers from withRouteHandler.

Reviews (6): Last reviewed commit: "fix(api): stop the last v1 validation pa..." | Re-trigger Greptile

Comment thread apps/sim/app/api/v1/files/[fileId]/route.ts
Review found three places the first pass missed, all from filters that worked
on whole files instead of individual call sites.

- GET /api/v1/files/{fileId} returns the file bytes via `new Response`, not a
  `success: true` JSON body, so the header pass skipped it. The download now
  carries the same quota headers as the DELETE beside it.
- Four parseRequest calls in the table-row routes still reported the generic
  "Validation error". The first pass skipped any file that already had a
  handler anywhere in it, which excluded these two files wholesale. The check
  is now per call site, and no bare call remains.
- POST /api/v1/tables takes its body from the shared tables contract, which
  still used the bare `.min(1)` form, so an omitted workspaceId did not name
  itself. Converted there and in the other v1-reachable contracts.

Scope note: roughly a thousand `.min(1, '... is required')` declarations remain
under contracts/tools/**. Those are block and tool definitions rather than the
public REST surface, and converting them belongs in its own change.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

@cursor cursor Bot 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 51f0104. Configure here.

… sites

A quality pass found the previous commit only made the happy path consistent.
Those three route families have 117 response sites; 32 got headers. The other
85 are the error paths — 400/403/404/500 — which are exactly the responses a
client is deciding whether to retry, and they published no quota at all.

`checkRateLimit` now records the bucket snapshot against the request, and
`withRouteHandler` attaches the headers next to the `x-request-id` it already
sets, on both the success and the unhandled-error branch. Every v1 response
carries the quota now, and a new v1 route gets it without remembering to. The
carrier is a WeakMap keyed by the request, so it needs no cleanup and routes
that never record a snapshot — everything outside v1 — are untouched.

This deletes more than it adds: the 32 decorations are gone, and so is the
`rateLimit` parameter that had been threaded into `handleBatchInsert` purely so
a business-logic helper could decorate its own response.

Also from the same pass:
- One definition of the header trio. `createApiResponse` had its own copy, so
  after the last commit there were two; both now build from
  `buildRateLimitHeaders`.
- `v1ValidationErrorResponse` delegates to the shared `validationErrorResponse`
  instead of hand-rolling the same body, and takes a fallback message, which
  collapses four route closures that differed only in that string.
- 36 sites wrote `requiredFieldSchema('Workspace ID is required')` — the
  verbatim definition of the exported `workspaceIdSchema`. Using the primitive
  is the whole point of having it; they now import it.
- Dropped TSDoc that had gone stale or contradicted the call sites it advised.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

@cursor cursor Bot 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 607ea4a. Configure here.

The comment pass caught a real casualty of the previous commit: inserting
`applyResponseHeaders` put it between `withRouteHandler`'s docblock and the
function itself, so the file's most-used export lost its documentation to the
new private helper. Reattached, and its header bullet now mentions the
rate-limit trio it also emits.

Remaining edits are wording only. The WeakMap rationale moved off
`RateLimitSnapshot` — three self-evident fields — onto the `snapshots`
declaration it actually describes. The record site no longer restates that
rationale; it keeps only the part unique to it. And three id-schema docs claimed
"same constraint as nonEmptyIdSchema", which stopped being true when that
schema was documented as deliberately message-less.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

@cursor cursor Bot 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 b912fd9. Configure here.

The spec already asserted, in the RateLimited description, that the
X-RateLimit-* trio accompanies every authenticated response. Before this branch
that was false for tables, files and knowledge; it is true now, but no operation
documented it — only 1 of 40 v1 success responses carried the headers.

All 40 now reference the shared header components. The shared BadRequest,
Forbidden and NotFound components are deliberately left alone: they are also
$ref-ed by non-v1 operations that publish no quota, so annotating them there
would over-claim. The RateLimited description carries the general rule instead,
now stating explicitly that the only responses without the headers are the ones
that failed authentication.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread apps/sim/app/api/v1/tables/[tableId]/rows/route.ts
Bugbot found GET /api/v1/tables/{tableId}/rows still answering with the bare
"Validation error". Its handler special-cases malformed filter/sort JSON and
then falls back to the shared helper — so the site looked handled to a check
that only asked whether a handler existed, which is why the earlier call-level
sweep passed over it.

Auditing the whole class turned up more of the same shape:
- The optional-body parses on deploy and rollback, where a bad `version` lost
  "version must be a positive integer".
- Eleven catch-block `validationErrorResponseFromError` handlers across the
  table routes, which discard the message of any ZodError thrown deeper.

Adds `v1ValidationErrorResponseFromError` as the v1 counterpart for unknown
caught values, and routes every remaining v1 validation path through the v1
helpers. No call to the generic helpers survives under app/api/v1 outside
admin, which keeps its own error envelope.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

@cursor cursor Bot 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 3a22a75. Configure here.

@waleedlatif1
waleedlatif1 merged commit b69fdbd into staging Jul 28, 2026
15 checks passed
@waleedlatif1
waleedlatif1 deleted the v1-api-consistency branch July 28, 2026 19:56
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