@hyperlimit/express
Version:
Express middleware for HyperLimit rate limiter
101 lines (84 loc) • 3.28 kB
JavaScript
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 function rateLimitMiddleware(req, res, next) {
try {
// Check bypass keys if configured
if (bypassHeader) {
const bypassKey = req.headers[bypassHeader.toLowerCase()];
if (bypassKeys.includes(bypassKey)) {
return next();
}
}
// Generate key if custom generator provided
const clientKey = keyGenerator ?
keyGenerator(req) :
req.ip;
// Get rate limit info before request
const info = limiter.getRateLimitInfo(key);
// Set rate limit headers
res.setHeader('X-RateLimit-Limit', String(info.limit));
res.setHeader('X-RateLimit-Remaining', String(Math.max(0, info.remaining)));
res.setHeader('X-RateLimit-Reset', String(Math.ceil(info.reset / 1000))); // Convert to seconds
// Attach limiter to request for potential use in route handlers
req.rateLimit = { limiter, key };
const allowed = limiter.tryRequest(key, clientKey);
if (allowed) {
return next();
}
// Return rejection info to be handled by custom handler
if (onRejected) {
return onRejected(req, res, {
error: 'Too many requests',
retryAfter: info.retryAfter || Math.ceil(windowMs / 1000)
});
}
// Default handling if no onRejected provided
return res.status(429).json({
error: 'Too many requests',
retryAfter: info.retryAfter || Math.ceil(windowMs / 1000)
});
} catch (error) {
return next(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;