Skip to content

Commit d69e59c

Browse files
mikeh-lagoLago Developerclaude
authored
feat: add HTTP 429 rate limit retry support (#83)
* feat: Add HTTP 429 rate limiting support with automatic retry - Add LagoRateLimitError class for 429 responses - Parse x-ratelimit-* headers from responses - Implement automatic retry on 429 with configurable max retries - Support header-based reset timing and exponential backoff - Create fetch wrapper to intercept and handle rate limiting - Add LagoClientConfig with rateLimitRetry options - Include comprehensive tests for all rate limiting features - Add RATE_LIMITING.md documentation - Add rate_limiting_example.ts with usage patterns - Maintain backward compatibility with existing API - Do not modify generated OpenAPI code * fix: correct rate limit test setup * fix: replace broken mock_fetch with simple fetch mock mock_fetch library's URLPattern is incompatible with current Deno. Replace with a lightweight createMockFetch helper. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore: gitignore ARCHITECTURE.md Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore: update deno.lock Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: implement real exponential backoff, remove dead code and generated docs - Fix getWaitTime to actually fall back to exponential backoff (1s, 2s, 4s...) when x-ratelimit-reset header is missing. Previously always used error.retryAfter which defaulted to 60s, meaning every retry without a header waited a full minute. - Change missing-header default from 60 to -1 so the backoff path is triggered correctly. - Remove RateLimitRetryHandler class (rate_limit_retry.ts) — both branches of handleResponse() threw unconditionally, making the retryOnRateLimit flag a no-op. The class was never used by createRateLimitFetch. Remove its export from mod.ts. - Remove RATE_LIMITING.md (394 lines) and examples/rate_limiting_example.ts (305 lines of commented-out code). - Add test verifying exponential backoff triggers ~1s wait (not 60s) when reset header is absent. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: remove deno.lock incompatible with CI Deno v1.x Local Deno generated lockfile v5 which is unsupported by CI's Deno 1.46.3. Removing so CI can regenerate. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore: add .claude to .gitignore Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add 20s max retry delay cap Prevents excessively long waits when the server returns a large x-ratelimit-reset value. Applies to both header-based and exponential backoff delays. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: remove deno.lock and add to .gitignore Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Lago Developer <dev@lago.io> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 5fbe72b commit d69e59c

7 files changed

Lines changed: 469 additions & 129 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,3 +102,6 @@ web_modules/
102102

103103
npm/
104104
openapi/
105+
ARCHITECTURE.md
106+
.claude
107+
deno.lock

deno.lock

Lines changed: 0 additions & 127 deletions
This file was deleted.

mod.ts

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,35 @@
11
// deno-lint-ignore-file no-explicit-any
22
import { Api, ApiConfig, HttpResponse } from "./openapi/client.ts";
3+
import { createRateLimitFetch } from "./rate_limit_fetch.ts";
4+
import type { RateLimitFetchConfig } from "./rate_limit_fetch.ts";
5+
6+
export interface LagoClientConfig extends ApiConfig {
7+
/**
8+
* Rate limit retry configuration
9+
*/
10+
rateLimitRetry?: RateLimitFetchConfig;
11+
}
12+
13+
export const Client = (apiKey: string, apiConfig?: LagoClientConfig) => {
14+
const { rateLimitRetry, ...restConfig } = apiConfig ?? {};
15+
16+
// Create rate-limit-aware fetch if configured
17+
const customFetch = rateLimitRetry
18+
? createRateLimitFetch(
19+
(restConfig?.customFetch ?? globalThis.fetch) as typeof fetch,
20+
rateLimitRetry,
21+
)
22+
: restConfig?.customFetch;
323

4-
export const Client = (apiKey: string, apiConfig?: ApiConfig) => {
524
const api = new Api({
625
securityWorker: (apiKey) =>
726
apiKey ? { headers: { Authorization: `Bearer ${apiKey}` } } : {},
827
// Cloudflare Workers doesn't support some options like credentials so need to override default
928
baseApiParams: {
1029
redirect: "follow",
1130
},
12-
...apiConfig,
31+
...restConfig,
32+
...(customFetch && { customFetch }),
1333
});
1434
api.setSecurityData(apiKey);
1535
return api;
@@ -42,4 +62,9 @@ export async function getLagoError<T>(error: any) {
4262
throw new Error(error);
4363
}
4464

65+
// Rate limit exports
66+
export { LagoRateLimitError } from "./rate_limit_error.ts";
67+
export { parseRateLimitHeaders, type RateLimitHeaders } from "./rate_limit_headers.ts";
68+
export { createRateLimitFetch, type RateLimitFetchConfig } from "./rate_limit_fetch.ts";
69+
4570
export * from "./openapi/client.ts";

rate_limit_error.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/**
2+
* Error class for rate limit (HTTP 429) responses
3+
*/
4+
export class LagoRateLimitError extends Error {
5+
public readonly limit: number;
6+
public readonly remaining: number;
7+
public readonly reset: number; // seconds until window resets
8+
public readonly retryAfter: number; // milliseconds to wait before retrying
9+
10+
constructor(
11+
limit: number,
12+
remaining: number,
13+
reset: number,
14+
) {
15+
super(
16+
`Rate limit exceeded. Limit: ${limit}, Remaining: ${remaining}, Reset in: ${reset}s`,
17+
);
18+
this.name = "LagoRateLimitError";
19+
this.limit = limit;
20+
this.remaining = remaining;
21+
this.reset = reset;
22+
this.retryAfter = reset * 1000; // Convert seconds to milliseconds
23+
24+
// Maintain proper prototype chain for instanceof checks
25+
Object.setPrototypeOf(this, LagoRateLimitError.prototype);
26+
}
27+
}

0 commit comments

Comments
 (0)