UNPKG

@foundatiofx/fetchclient

Version:

A typed JSON fetch client with middleware support for Deno, Node and the browser.

116 lines (115 loc) 4.99 kB
import { ProblemDetails } from "./ProblemDetails.js"; import { buildRateLimitHeader, buildRateLimitPolicyHeader, RateLimiter, } from "./RateLimiter.js"; /** * Rate limiting error thrown when requests exceed the rate limit. */ export class RateLimitError extends Error { resetTime; remainingRequests; constructor(resetTime, remainingRequests, message) { super(message || `Rate limit exceeded. Try again after ${new Date(resetTime).toISOString()}`); this.name = "RateLimitError"; this.resetTime = resetTime; this.remainingRequests = remainingRequests; } } /** * Rate limiting middleware instance that can be shared across requests. */ export class RateLimitMiddleware { #rateLimiter; throwOnRateLimit; errorMessage; autoUpdateFromHeaders; constructor(options) { this.#rateLimiter = new RateLimiter(options); this.throwOnRateLimit = options.throwOnRateLimit ?? true; this.errorMessage = options.errorMessage; this.autoUpdateFromHeaders = options.autoUpdateFromHeaders ?? true; } /** * Gets the underlying rate limiter instance. */ get rateLimiter() { return this.#rateLimiter; } /** * Creates the middleware function. * @returns The middleware function */ middleware() { return async (context, next) => { const url = context.request.url; // Check if request is allowed if (!this.rateLimiter.isAllowed(url)) { const group = this.rateLimiter.getGroup(url); const resetTime = this.rateLimiter.getResetTime(url) ?? Date.now(); const remainingRequests = this.rateLimiter.getRemainingRequests(url); if (this.throwOnRateLimit) { throw new RateLimitError(resetTime, remainingRequests, this.errorMessage); } // Create a 429 Too Many Requests response const groupOptions = this.rateLimiter.getGroupOptions(group); const maxRequests = groupOptions.maxRequests; const windowSeconds = groupOptions.windowSeconds; // Create IETF standard rate limit headers const resetSeconds = Math.ceil((resetTime - Date.now()) / 1000); const rateLimitHeader = buildRateLimitHeader({ policy: group, remaining: remainingRequests, resetSeconds: resetSeconds, }); const rateLimitPolicyHeader = buildRateLimitPolicyHeader({ policy: group, limit: maxRequests, windowSeconds: Math.floor(windowSeconds), }); const headers = new Headers({ "Content-Type": "application/problem+json", "RateLimit": rateLimitHeader, "RateLimit-Policy": rateLimitPolicyHeader, // Legacy headers for backward compatibility "RateLimit-Limit": maxRequests.toString(), "RateLimit-Remaining": remainingRequests.toString(), "RateLimit-Reset": Math.ceil(resetTime / 1000).toString(), "Retry-After": resetSeconds.toString(), }); const problem = new ProblemDetails(); problem.status = 429; problem.title = "Too Many Requests"; problem.detail = this.errorMessage || `Rate limit exceeded. Try again after ${new Date(resetTime).toISOString()}`; context.response = { url: context.request.url, status: 429, statusText: "Too Many Requests", body: null, bodyUsed: true, ok: false, headers: headers, redirected: false, type: "basic", problem: problem, data: null, meta: { links: {} }, json: () => Promise.resolve(problem), text: () => Promise.resolve(JSON.stringify(problem)), arrayBuffer: () => Promise.resolve(new ArrayBuffer(0)), // @ts-ignore: New in Deno 1.44 bytes: () => Promise.resolve(new Uint8Array()), blob: () => Promise.resolve(new Blob()), formData: () => Promise.resolve(new FormData()), clone: () => { throw new Error("Not implemented"); }, }; return; } await next(); if (this.autoUpdateFromHeaders && context.response) { this.rateLimiter.updateFromHeadersForRequest(url, context.response.headers); } }; } }