fix(api): report the real rate-limit ceiling on every v1 endpoint - #6011
Conversation
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.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
PR SummaryMedium Risk Overview 401/auth and checker-error paths no longer attach rate-limit headers or fake quotas ( Shared Zod id schemas ( Reviewed by Cursor Bugbot for commit d9f4e14. Configure here. |
Greptile SummaryFixes inconsistent v1 rate-limit headers and clearer shared-id validation messages.
Confidence Score: 5/5This 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.
|
| 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
Reviews (1): Last reviewed commit: "fix(api): report the real rate-limit cei..." | Re-trigger Greptile
Summary
Found while testing the v1 export/import endpoints against production — the response headers contradicted each other:
X-RateLimit-Limitreported the bucket's refill rate;X-RateLimit-Remainingreported tokens left in the bucket.createBucketConfigsetsmaxTokens = refillRate * burstMultiplier, so the two headers described different quantities andremainingroutinely exceededlimit. Any client computingused = limit - remaininggot a negative number. Now reports the bucket capacity, which is whatremainingcounts down from.checkRateLimit, so it affected every v1 endpoint — workflows, logs, tables, files, knowledge, audit-logs, copilot. One-line source, one-line fix.limit: 10, remaining: 0) advertised a quota that doesn't exist and told unauthenticated callers they'd been throttled.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'sInvalid input: expected string, received undefined. Adding the message to thez.string()constructor covers both. AffectsworkspaceId/organizationId/workflowId/fileIdacross every contract that builds on them.Retry-Afterwas.Type of Change
Testing
Adds the first tests for the v1 middleware (6 cases): limit is capacity not refill rate,
remaining <= limitalways, 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/andlib/core/rate-limiter/. Typecheck, lint,check:api-validation:strictand 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