Skip to content

fix(api): report the real rate-limit ceiling on every v1 endpoint - #6011

Merged
waleedlatif1 merged 1 commit into
stagingfrom
worktree-v1-ratelimit-headers
Jul 28, 2026
Merged

fix(api): report the real rate-limit ceiling on every v1 endpoint#6011
waleedlatif1 merged 1 commit into
stagingfrom
worktree-v1-ratelimit-headers

Conversation

@waleedlatif1

Copy link
Copy Markdown
Collaborator

Summary

Found while testing the v1 export/import endpoints against production — the response headers contradicted each other:

x-ratelimit-limit: 200
x-ratelimit-remaining: 399   ← remaining > limit
  • X-RateLimit-Limit reported the bucket's refill rate; X-RateLimit-Remaining reported tokens left in the bucket. createBucketConfig sets maxTokens = refillRate * burstMultiplier, so the two headers described different quantities and remaining routinely exceeded limit. Any client computing used = limit - remaining got a negative number. Now reports the bucket capacity, which is what remaining counts down from.
  • This is in the shared checkRateLimit, so it affected every v1 endpoint — workflows, logs, tables, files, knowledge, audit-logs, copilot. One-line source, one-line fix.
  • Stops publishing rate-limit headers on an authentication failure. That path never reaches the bucket, so the placeholder (limit: 10, remaining: 0) advertised a quota that doesn't exist and told unauthenticated callers they'd been throttled.
  • Missing required fields now name themselves. The shared id schemas used z.string().min(1, 'Workspace ID is required'), but .min(1) only fires for a present-but-empty string — an omitted field fell back to Zod's Invalid input: expected string, received undefined. Adding the message to the z.string() constructor covers both. Affects workspaceId / organizationId / workflowId / fileId across every contract that builds on them.
  • Documents all three headers in the OpenAPI spec as reusable components, including the burst-capacity semantics and their absence on auth failures. They were previously undocumented — only Retry-After was.

Type of Change

  • Bug fix

Testing

Adds the first tests for the v1 middleware (6 cases): limit is capacity not refill rate, remaining <= limit always, zero limit on auth failure and on checker error, consistent headers on 429, and no rate-limit headers on 401.

Extends the primitives tests (12 cases) covering omitted vs empty vs valid for all four shared id schemas, plus an assertion that Zod's default wording never leaks.

312 tests pass across app/api/v1/, lib/api/contracts/ and lib/core/rate-limiter/. Typecheck, lint, check:api-validation:strict and the monorepo boundary check all pass. Verified no consumer asserted the old behavior.

The header values were confirmed wrong against production before the fix; the corrected values have not yet been observed on a deployed build.

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)

X-RateLimit-Limit was the bucket's refill rate while X-RateLimit-Remaining
was tokens left in the bucket, and createBucketConfig sets
maxTokens = refillRate * burstMultiplier. The two headers described different
quantities, so remaining routinely exceeded limit — observed live on a team
plan as limit 200 alongside remaining 399 — and any client computing
used = limit - remaining got a negative number. Report the bucket capacity,
which is what remaining counts down from.

This lives in the shared checkRateLimit, so it applies to every v1 endpoint:
workflows, logs, tables, files, knowledge, audit-logs and copilot.

Also stops publishing rate-limit headers on an authentication failure. That
path never reaches the bucket, so the previous placeholder advertised a quota
that does not exist and told unauthenticated callers they had been throttled.

Separately, the shared id schemas reported Zod's default "expected string,
received undefined" when a required field was omitted, because .min(1) only
fires for a present-but-empty string. Adding the message to the z.string()
constructor makes an omitted workspaceId/organizationId/workflowId/fileId
name the field it is complaining about — the first thing an API consumer sees
on a malformed request.

Documents all three headers in the OpenAPI spec as reusable components,
including the burst-capacity semantics and the fact that they are absent on
authentication failures. They were previously undocumented.

Adds the first tests for the v1 middleware.
@vercel

vercel Bot commented Jul 28, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
docs Building Building Preview, Comment Jul 28, 2026 6:39pm

Request Review

@cursor

cursor Bot commented Jul 28, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Changes behavior of rate-limit headers on every v1 endpoint and strips them from 401 responses; clients or monitors that relied on the old (incorrect) limit or headers on auth failures may need to adjust, though the fix aligns headers with actual throttling semantics.

Overview
Fixes contradictory v1 rate-limit headers by setting X-RateLimit-Limit to the token bucket capacity (maxTokens) instead of the per-minute refill rate, so it matches what X-RateLimit-Remaining counts down from (e.g. no more limit: 200 with remaining: 399).

401/auth and checker-error paths no longer attach rate-limit headers or fake quotas (limit is 0 when the bucket was never consulted). OpenAPI documents the three X-RateLimit-* headers (burst semantics) and notes they apply on authenticated traffic, not failed auth.

Shared Zod id schemas (workspaceId, organizationId, workflowId, fileId) now use z.string({ error: '…' }) so omitted fields return the same human-readable “X is required” message as empty strings. Adds middleware tests for header consistency and extends primitives tests for those messages.

Reviewed by Cursor Bugbot for commit d9f4e14. Configure here.

@greptile-apps

greptile-apps Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Fixes inconsistent v1 rate-limit headers and clearer shared-id validation messages.

  • checkRateLimit reports bucket capacity (maxTokens) as X-RateLimit-Limit so it matches remaining.
  • Auth/checker-error paths use limit: 0 and no longer publish rate-limit headers on those responses.
  • Shared id Zod schemas pass the required message into z.string({ error }) so omitted fields name the field.
  • OpenAPI documents X-RateLimit-* header components; middleware and primitives tests cover the new contracts.

Confidence Score: 5/5

This PR appears safe to merge; the header and validation changes align limit with bucket capacity and do not introduce a blocking failure.

maxTokens is the storage ceiling remaining counts against; callers treat limit as the header quota; Zod 4 string error messages match existing contract patterns and tests.

Important Files Changed

Filename Overview
apps/sim/app/api/v1/middleware.ts Reports limit as maxTokens; omits rate-limit headers when result.error is set; auth/error paths use limit 0.
apps/sim/app/api/v1/middleware.test.ts New tests pin capacity-as-limit, remaining≤limit, zero limit on auth/checker failure, and no headers on 401.
apps/sim/lib/api/contracts/primitives.ts Id schemas use z.string({ error }) so omitted and empty values share the field-named message (Zod 4).
apps/sim/lib/api/contracts/primitives.test.ts Covers omitted/empty/valid for four shared id schemas and blocks Zod default undefined wording.
apps/docs/openapi.json Adds reusable RateLimit header components and attaches them to the RateLimited response description.

Sequence Diagram

sequenceDiagram
  participant Client
  participant MW as v1 checkRateLimit
  participant Auth as authenticateV1Request
  participant RL as RateLimiter / bucket
  Client->>MW: request
  MW->>Auth: authenticate
  alt auth fails
    Auth-->>MW: error
    MW-->>Client: "401, no X-RateLimit-* headers"
  else auth ok
    MW->>RL: consume token
    RL-->>MW: remaining, resetAt, config.maxTokens
    MW-->>Client: "success or 429 with Limit=maxTokens, Remaining, Reset"
  end
Loading

Reviews (1): Last reviewed commit: "fix(api): report the real rate-limit cei..." | Re-trigger Greptile

@waleedlatif1
waleedlatif1 merged commit 79b1ed1 into staging Jul 28, 2026
14 of 15 checks passed
@waleedlatif1
waleedlatif1 deleted the worktree-v1-ratelimit-headers branch July 28, 2026 18:42
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