@perdieminc/hapi-rate-limitor
v4.0.2
Published
Rate limiting for hapi/hapi.js to prevent brute-force attacks
Readme
@perdieminc/hapi-rate-limitor
A powerful rate limiting plugin for hapi.js that helps prevent brute-force attacks and API abuse.
Features
- 🚀 Custom Redis Support: Works with both standard Redis instances and Redis Cluster
- 🔒 Flexible Rate Limiting: Global, route-specific, and user-specific limits
- 🎯 IP-based & User-based: Rate limit by IP address or authenticated user
- 📊 Response Headers: Automatic rate limit headers (
X-Rate-Limit-*) - 🎨 Custom Views: Render custom views when rate limit is exceeded
- ⚡ Event Emitters: Hook into rate limiting events
- 🔧 Highly Configurable: Whitelist IPs, skip routes, custom extension points
Requirements
- Node.js: >= 18
- hapi: >= 21
- Redis: A running Redis instance or Redis Cluster
Installation
npm install @perdieminc/hapi-rate-limitorQuick Start
const Hapi = require('@hapi/hapi');
const server = new Hapi.Server({
port: 3000
});
await server.register({
plugin: require('@perdieminc/hapi-rate-limitor'),
options: {
redis: 'redis://localhost:6379',
max: 60, // 60 requests
duration: 60 * 1000, // per 60 seconds
namespace: 'my-app-limiter'
}
});
await server.start();Redis Configuration
Standard Redis Instance
The plugin supports multiple ways to configure a standard Redis connection:
Connection String
await server.register({
plugin: require('@perdieminc/hapi-rate-limitor'),
options: {
redis: 'redis://localhost:6379'
}
});Configuration Object
await server.register({
plugin: require('@perdieminc/hapi-rate-limitor'),
options: {
redis: {
host: 'localhost',
port: 6379,
password: 'your-password',
db: 0
}
}
});Custom Redis Instance
You can pass your own pre-configured Redis instance (useful for connection pooling or custom configurations):
const Redis = require('ioredis');
const redis = new Redis({
host: 'localhost',
port: 6379,
password: 'your-password',
retryStrategy: (times) => {
return Math.min(times * 50, 2000);
}
});
await server.register({
plugin: require('@perdieminc/hapi-rate-limitor'),
options: {
redis: redis // Pass your custom Redis instance
}
});Redis Cluster Support
The plugin fully supports Redis Cluster configurations:
const Redis = require('ioredis');
const cluster = new Redis.Cluster([
{ host: 'localhost', port: 7000 },
{ host: 'localhost', port: 7001 },
{ host: 'localhost', port: 7002 }
], {
redisOptions: {
password: 'your-cluster-password'
}
});
await server.register({
plugin: require('@perdieminc/hapi-rate-limitor'),
options: {
redis: cluster // Pass your Redis Cluster instance
}
});Note: When passing a custom Redis instance or cluster, the plugin will not automatically connect or disconnect. You are responsible for managing the connection lifecycle.
Configuration Options
Plugin Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| enabled | Boolean | true | Enable/disable rate limiting globally |
| redis | String/Object/Redis | required | Redis connection string, config object, or Redis instance |
| max | Number | 60 | Maximum number of requests allowed |
| duration | Number | 60000 | Time window in milliseconds (default: 60 seconds) |
| namespace | String | 'hapi-rate-limitor' | Redis key namespace |
| userAttribute | String | 'id' | User identifier attribute in request.auth.credentials |
| userLimitAttribute | String | 'rateLimit' | User-specific rate limit attribute |
| ipWhitelist | Array | [] | Array of whitelisted IP addresses |
| extensionPoint | String | 'onPostAuth' | Hapi extension point for rate limiting |
| view | String | undefined | Path to custom view for rate limit exceeded |
| skip | Function | () => false | Function to skip rate limiting for specific requests |
| getIp | Function | undefined | Custom function to extract IP address |
Usage Examples
Basic Rate Limiting
await server.register({
plugin: require('@perdieminc/hapi-rate-limitor'),
options: {
redis: 'redis://localhost:6379',
max: 100, // 100 requests
duration: 60 * 1000, // per minute
namespace: 'my-api'
}
});Route-Specific Limits
You can set different rate limits for specific routes:
server.route({
method: 'POST',
path: '/login',
options: {
plugins: {
'hapi-rate-limitor': {
max: 5, // Only 5 login attempts
duration: 5 * 60 * 1000 // per 5 minutes
}
},
handler: async (request, h) => {
return { success: true };
}
}
});Disable Rate Limiting for Specific Routes
server.route({
method: 'GET',
path: '/health',
options: {
plugins: {
'hapi-rate-limitor': {
enabled: false // No rate limiting for health checks
}
},
handler: async (request, h) => {
return { status: 'ok' };
}
}
});IP Whitelist
await server.register({
plugin: require('@perdieminc/hapi-rate-limitor'),
options: {
redis: 'redis://localhost:6379',
ipWhitelist: [
'127.0.0.1',
'::1',
'10.0.0.0/8' // Internal network
]
}
});User-Specific Rate Limits
For authenticated users, you can set per-user rate limits:
// In your authentication handler
request.auth.credentials = {
id: 'user-123',
rateLimit: 1000 // This user gets 1000 requests per duration
};Custom View for Rate Limit Exceeded
await server.register([
{
plugin: require('@hapi/vision')
},
{
plugin: require('@perdieminc/hapi-rate-limitor'),
options: {
redis: 'redis://localhost:6379',
view: 'rate-limit-exceeded' // Render this view when exceeded
}
}
]);
server.views({
engines: { html: require('handlebars') },
path: 'views'
});Skip Rate Limiting Conditionally
await server.register({
plugin: require('@perdieminc/hapi-rate-limitor'),
options: {
redis: 'redis://localhost:6379',
skip: (request) => {
// Skip rate limiting for admin users
return request.auth.credentials?.role === 'admin';
}
}
});Custom IP Detection
await server.register({
plugin: require('@perdieminc/hapi-rate-limitor'),
options: {
redis: 'redis://localhost:6379',
getIp: (request) => {
// Custom IP extraction (e.g., behind a proxy)
return request.headers['x-real-ip'] ||
request.headers['x-forwarded-for']?.split(',')[0] ||
request.info.remoteAddress;
}
}
});Custom Extension Point
await server.register({
plugin: require('@perdieminc/hapi-rate-limitor'),
options: {
redis: 'redis://localhost:6379',
extensionPoint: 'onPreAuth' // Rate limit before authentication
}
});Rate Limit Events
The plugin emits events that you can listen to:
await server.register({
plugin: require('@perdieminc/hapi-rate-limitor'),
options: {
redis: 'redis://localhost:6379',
emitter: {
on: (event, callback) => {
if (event === 'rate-limit:exceeded') {
callback((request) => {
console.log(`Rate limit exceeded for ${request.info.remoteAddress}`);
});
}
if (event === 'rate-limit:in-quota') {
callback((request) => {
console.log(`Request within quota: ${request.path}`);
});
}
}
}
}
});Response Headers
The plugin automatically adds rate limit headers to all responses:
X-Rate-Limit-Limit: 60
X-Rate-Limit-Remaining: 59
X-Rate-Limit-Reset: 1639584000X-Rate-Limit-Limit: Maximum number of requests allowedX-Rate-Limit-Remaining: Number of requests remaining in the current windowX-Rate-Limit-Reset: Unix timestamp (in seconds) when the rate limit resets
Error Response
When the rate limit is exceeded, the plugin returns a 429 Too Many Requests response:
{
"statusCode": 429,
"error": "Too Many Requests",
"message": "You have exceeded the request limit"
}Advanced Configuration
Complete Example with All Options
const Redis = require('ioredis');
const redis = new Redis.Cluster([
{ host: 'redis-node-1', port: 7000 },
{ host: 'redis-node-2', port: 7001 },
{ host: 'redis-node-3', port: 7002 }
]);
await server.register({
plugin: require('@perdieminc/hapi-rate-limitor'),
options: {
enabled: true,
redis: redis,
max: 100,
duration: 60 * 1000,
namespace: 'my-app-rate-limiter',
userAttribute: 'userId',
userLimitAttribute: 'maxRequests',
ipWhitelist: ['127.0.0.1', '::1'],
extensionPoint: 'onPostAuth',
view: 'rate-limit-exceeded',
skip: (request) => {
return request.path.startsWith('/public');
},
getIp: (request) => {
return request.headers['x-forwarded-for']?.split(',')[0] ||
request.info.remoteAddress;
}
}
});TypeScript Support
The plugin includes TypeScript definitions:
import { Server } from '@hapi/hapi';
import * as RateLimitor from '@perdieminc/hapi-rate-limitor';
const server = new Server({ port: 3000 });
await server.register({
plugin: RateLimitor,
options: {
redis: 'redis://localhost:6379',
max: 60,
duration: 60000
}
});Testing
npm testLicense
MIT © Marcus Pöhls
Acknowledgments
This package is a fork of the hapi-rate-limitor created by Marcus Pöhls and the Future Studio team.
Original Repository: futurestudio/hapi-rate-limitor
We are grateful for their work and contributions to the hapi.js ecosystem. This fork maintains compatibility with the original while adding enhanced documentation and examples, particularly around Redis Cluster support.
If you find this package useful, please consider:
- ⭐ Starring the original repository
- 💖 Supporting Future Studio
- 🤝 Contributing back improvements to the upstream project
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
For changes that could benefit the broader community, consider contributing to the original repository as well.
Credits
This plugin uses:
- ioredis for Redis connectivity
- async-ratelimiter for rate limiting logic
- @supercharge/request-ip for IP detection
Made with ❤️ for the hapi.js community
Development & Release
Creating a New Release
When you're ready to release a new version:
Update version and create tag (automatically):
npm version patch # For bug fixes (4.0.1 -> 4.0.2) npm version minor # For new features (4.0.1 -> 4.1.0) npm version major # For breaking changes (4.0.1 -> 5.0.0)This automatically updates
package.json, creates a commit, and creates a git tag.Push the tag (triggers automatic publish):
git push origin master --follow-tagsGitHub Actions automatically:
- ✅ Runs all tests
- ✅ Publishes to npm
- ✅ Creates a GitHub Release with auto-generated notes
Manual Release (if needed)
If you need to publish manually without the automation:
npm publish --access publicDelete a Tag (if you made a mistake)
# Delete local tag
git tag -d v4.0.2
# Delete remote tag
git push origin --delete v4.0.2Syncing with Upstream
To get the latest updates from the original repository:
git fetch upstream
git merge upstream/master
git push origin masterSee the upstream repository for the latest changes.
