UNPKG

@hyperlimit/fastify

Version:

Fastify plugin for HyperLimit rate limiter

102 lines (85 loc) 3.32 kB
const { HyperLimit } = require('@hyperlimit/core'); function rateLimit(options = {}) { const { maxTokens = 100, window = '1m', maxPenalty = 0, block = '', sliding = true, key = 'default', bypassHeader, bypassKeys = [], keyGenerator, onRejected, redis } = options; // Convert time strings to milliseconds const windowMs = parseDuration(window); const blockMs = block ? parseDuration(block) : 0; // Create limiter instance with Redis support if configured const limiter = new HyperLimit(redis ? { redis } : undefined); // Create limiter for this route limiter.createLimiter(key, maxTokens, windowMs, sliding, blockMs, maxPenalty); return async function rateLimitMiddleware(request, reply) { try { // Check bypass keys if configured if (bypassHeader) { const bypassKey = request.headers[bypassHeader.toLowerCase()]; if (bypassKeys.includes(bypassKey)) { return; } } // Generate key if custom generator provided const clientKey = keyGenerator ? keyGenerator(request) : request.ip; // Get rate limit info before request const info = limiter.getRateLimitInfo(key); // Set rate limit headers reply.header('X-RateLimit-Limit', String(info.limit)); reply.header('X-RateLimit-Remaining', String(Math.max(0, info.remaining))); reply.header('X-RateLimit-Reset', String(Math.ceil(info.reset / 1000))); // Convert to seconds // Attach limiter to request for potential use in route handlers request.rateLimit = { limiter, key }; const allowed = limiter.tryRequest(key, clientKey); if (allowed) { return; } // Return rejection info to be handled by custom handler if (onRejected) { return onRejected(request, reply, { error: 'Too many requests', retryAfter: info.retryAfter || Math.ceil(windowMs / 1000) }); } // Default handling if no onRejected provided reply.code(429).send({ error: 'Too many requests', retryAfter: info.retryAfter || Math.ceil(windowMs / 1000) }); } catch (error) { request.log.error(error); throw error; } }; } function parseDuration(duration) { if (typeof duration === 'number') { return duration; } const match = duration.match(/^(\d+)(ms|s|m|h|d)$/); if (!match) { throw new Error('Invalid duration format. Use number or string (e.g., "500ms", "1s", "5m", "2h", "1d")'); } const value = parseInt(match[1], 10); const unit = match[2]; switch (unit) { case 'ms': return value; case 's': return value * 1000; case 'm': return value * 60 * 1000; case 'h': return value * 60 * 60 * 1000; case 'd': return value * 24 * 60 * 60 * 1000; default: throw new Error('Invalid time unit'); } } module.exports = rateLimit;