Skip to content

PerDiemInc/hapi-rate-limitor

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

732 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

@perdieminc/hapi-rate-limitor

NPM Version Node.js Version

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-limitor

Quick 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: 1639584000
  • X-Rate-Limit-Limit: Maximum number of requests allowed
  • X-Rate-Limit-Remaining: Number of requests remaining in the current window
  • X-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 test

License

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:

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:


Made with ❤️ for the hapi.js community

Development & Release

Creating a New Release

When you're ready to release a new version:

  1. 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.

  2. Push the tag (triggers automatic publish):

    git push origin master --follow-tags
  3. GitHub 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 public

Delete 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.2

Syncing with Upstream

To get the latest updates from the original repository:

git fetch upstream
git merge upstream/master
git push origin master

See the upstream repository for the latest changes.