diff --git a/apps/api/lib/helpers/rateLimitApiKey.ts b/apps/api/lib/helpers/rateLimitApiKey.ts index 7f1469ea7f..de2c71f4e2 100644 --- a/apps/api/lib/helpers/rateLimitApiKey.ts +++ b/apps/api/lib/helpers/rateLimitApiKey.ts @@ -1,6 +1,7 @@ import type { NextMiddleware } from "next-api-middleware"; import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError"; +import { API_KEY_RATE_LIMIT } from "@calcom/lib/rateLimit"; export const rateLimitApiKey: NextMiddleware = async (req, res, next) => { if (!req.query.apiKey) return res.status(401).json({ message: "No apiKey provided" }); @@ -9,6 +10,11 @@ export const rateLimitApiKey: NextMiddleware = async (req, res, next) => { await checkRateLimitAndThrowError({ identifier: req.query.apiKey as string, rateLimitingType: "api", + onRateLimiterResponse: (response) => { + res.setHeader("X-RateLimit-Limit", API_KEY_RATE_LIMIT); + res.setHeader("X-RateLimit-Remaining", response.remaining); + res.setHeader("X-RateLimit-Reset", response.reset); + }, }); await next(); diff --git a/packages/lib/checkRateLimitAndThrowError.ts b/packages/lib/checkRateLimitAndThrowError.ts index d510a16181..5a806e4135 100644 --- a/packages/lib/checkRateLimitAndThrowError.ts +++ b/packages/lib/checkRateLimitAndThrowError.ts @@ -6,8 +6,12 @@ import { rateLimiter } from "./rateLimit"; export async function checkRateLimitAndThrowError({ rateLimitingType = "core", identifier, + onRateLimiterResponse, }: RateLimitHelper) { - const { remaining, reset } = await rateLimiter()({ rateLimitingType, identifier }); + const response = await rateLimiter()({ rateLimitingType, identifier }); + const { remaining, reset } = response; + + if (onRateLimiterResponse) onRateLimiterResponse(response); if (remaining < 1) { const convertToSeconds = (ms: number) => Math.floor(ms / 1000); diff --git a/packages/lib/rateLimit.ts b/packages/lib/rateLimit.ts index be2c74308d..7a14d01ee7 100644 --- a/packages/lib/rateLimit.ts +++ b/packages/lib/rateLimit.ts @@ -9,6 +9,11 @@ const log = logger.getSubLogger({ prefix: ["RateLimit"] }); export type RateLimitHelper = { rateLimitingType?: "core" | "forcedSlowMode" | "common" | "api" | "ai"; identifier: string; + /** + * Using a callback instead of a regular return to provide headers even + * when the rate limit is reached and an error is thrown. + **/ + onRateLimiterResponse?: (response: RatelimitResponse) => void; }; export type RatelimitResponse = { @@ -41,6 +46,8 @@ function logOnce(message: string) { warningDisplayed = true; } +export const API_KEY_RATE_LIMIT = 10; + export function rateLimiter() { const UPSATCH_ENV_FOUND = process.env.UPSTASH_REDIS_REST_URL && process.env.UPSTASH_REDIS_REST_TOKEN; @@ -73,7 +80,7 @@ export function rateLimiter() { redis, analytics: true, prefix: "ratelimit:api", - limiter: Ratelimit.fixedWindow(10, "60s"), + limiter: Ratelimit.fixedWindow(API_KEY_RATE_LIMIT, "60s"), }), ai: new Ratelimit({ redis,