feat: data-backed rate limits, concurrency limiter, copy button fix (BUG-025, BUG-022)

This commit is contained in:
OpenClaw 2026-02-15 08:14:39 +00:00
parent 922230c108
commit f5a85c6fc3
5 changed files with 222 additions and 17 deletions

View file

@ -0,0 +1,115 @@
import { Request, Response, NextFunction } from "express";
import { isProKey } from "../services/keys.js";
interface RateLimitEntry {
count: number;
resetTime: number;
}
// Per-key rate limits (requests per minute)
const FREE_RATE_LIMIT = 10;
const PRO_RATE_LIMIT = 30;
const RATE_WINDOW_MS = 60_000; // 1 minute
// Concurrency limits
const MAX_CONCURRENT_PDFS = 3;
const MAX_QUEUE_SIZE = 10;
const rateLimitStore = new Map<string, RateLimitEntry>();
let activePdfCount = 0;
const pdfQueue: Array<{ resolve: () => void; reject: (error: Error) => void }> = [];
function cleanupExpiredEntries(): void {
const now = Date.now();
for (const [key, entry] of rateLimitStore.entries()) {
if (now >= entry.resetTime) {
rateLimitStore.delete(key);
}
}
}
function getRateLimit(apiKey: string): number {
return isProKey(apiKey) ? PRO_RATE_LIMIT : FREE_RATE_LIMIT;
}
function checkRateLimit(apiKey: string): boolean {
cleanupExpiredEntries();
const now = Date.now();
const limit = getRateLimit(apiKey);
const entry = rateLimitStore.get(apiKey);
if (!entry || now >= entry.resetTime) {
// Create new window
rateLimitStore.set(apiKey, {
count: 1,
resetTime: now + RATE_WINDOW_MS
});
return true;
}
if (entry.count >= limit) {
return false;
}
entry.count++;
return true;
}
async function acquireConcurrencySlot(): Promise<void> {
if (activePdfCount < MAX_CONCURRENT_PDFS) {
activePdfCount++;
return;
}
if (pdfQueue.length >= MAX_QUEUE_SIZE) {
throw new Error("QUEUE_FULL");
}
return new Promise((resolve, reject) => {
pdfQueue.push({ resolve, reject });
});
}
function releaseConcurrencySlot(): void {
activePdfCount--;
const waiter = pdfQueue.shift();
if (waiter) {
activePdfCount++;
waiter.resolve();
}
}
export function pdfRateLimitMiddleware(req: Request & { apiKeyInfo?: any }, res: Response, next: NextFunction): void {
const keyInfo = req.apiKeyInfo;
const apiKey = keyInfo?.key || "unknown";
// Check rate limit first
if (!checkRateLimit(apiKey)) {
const limit = getRateLimit(apiKey);
const tier = isProKey(apiKey) ? "pro" : "free";
res.status(429).json({
error: "Rate limit exceeded",
limit: `${limit} PDFs per minute`,
tier,
retryAfter: "60 seconds"
});
return;
}
// Add concurrency control to the request
(req as any).acquirePdfSlot = acquireConcurrencySlot;
(req as any).releasePdfSlot = releaseConcurrencySlot;
next();
}
export function getConcurrencyStats() {
return {
activePdfCount,
queueSize: pdfQueue.length,
maxConcurrent: MAX_CONCURRENT_PDFS,
maxQueue: MAX_QUEUE_SIZE
};
}