Skip to content

Commit 061ca82

Browse files
authored
feat: Add rate limiting middleware (#140)
* feat: Add pharaoh_rate_limit middleware package - Implement token bucket and sliding window rate limiting algorithms - Add configurable middleware with Express.js-like API - Include comprehensive test coverage (18 tests passing) - Add examples and documentation - Support custom key generation, skip functions, and headers - Compatible with existing Pharaoh middleware system * fix: Update pharaoh_examples rate limiting demo - Fix import path and API usage for rate limiting example - Add proper dependency configuration in pubspec.yaml - Create comprehensive test script demonstrating all features - Verify rate limiting works with token bucket and sliding window - Test custom key generation, skip functionality, and headers * Refactor rate limiting code for style and clarity This commit applies consistent formatting, improves code readability, and fixes minor whitespace issues across rate limiting middleware, algorithms, and example/test files. No functional changes were made; the update is purely stylistic to enhance maintainability. * fix: Remove unused variable in rate limiting test script - Fix dart analyze warning for unused 'body' variable - Consume response body without storing in unused variable - All analysis checks now pass * feat: Add HTTP integration tests for rate limiting middleware - Create comprehensive HTTP tests that verify rate limiting in action - Test 429 responses, rate limit headers, and middleware behavior - Address maintainer feedback for actual HTTP verification - Include tests for custom key generation and skip functionality - All existing unit tests continue to pass (18 tests) * feat: Add clean HTTP integration test using Spookie framework - Create focused HTTP test that verifies rate limiting in action - Test 429 responses, rate limit headers, and middleware behavior - Use Spookie framework following Pharaoh testing patterns - Remove complex HTTP client test attempts - All 22 tests now pass including HTTP integration tests - Addresses maintainer feedback for actual HTTP verification * Add coverage reports and remove example test Added new code coverage JSON reports for multiple test files across pharaoh, pharaoh_basic_auth, pharaoh_jwt_auth, pharaoh_rate_limit, spanner, and spookie packages. Removed the unused test_rate_limiting.dart from pharaoh_examples and updated rate_limit_http_test.dart in pharaoh_rate_limit. * Delete api_service_test.dart.vm.json * Remove generated test coverage JSON files Deleted multiple .vm.json files containing test coverage data from various package directories. This cleanup likely prepares for regeneration of coverage reports or reduces repository size by removing generated artifacts.
1 parent cc6bc58 commit 061ca82

File tree

14 files changed

+1167
-0
lines changed

14 files changed

+1167
-0
lines changed
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
# Pharaoh Rate Limit
2+
3+
Rate limiting middleware for the Pharaoh web framework. Provides token bucket and sliding window algorithms to protect your APIs from abuse and ensure fair usage.
4+
5+
## Features
6+
7+
- **Multiple algorithms**: Token bucket and sliding window rate limiting
8+
- **Flexible configuration**: Customizable limits, time windows, and key generation
9+
- **Standard headers**: Supports both modern and legacy rate limit headers
10+
- **Skip functionality**: Bypass rate limiting for specific requests
11+
- **Per-client tracking**: Automatic IP-based or custom key generation
12+
- **Production ready**: Comprehensive test coverage and error handling
13+
14+
## Installation
15+
16+
Add this to your package's `pubspec.yaml` file:
17+
18+
```yaml
19+
dependencies:
20+
pharaoh_rate_limit: ^1.0.0
21+
```
22+
23+
## Quick Start
24+
25+
```dart
26+
import 'package:pharaoh/pharaoh.dart';
27+
import 'package:pharaoh_rate_limit/pharaoh_rate_limit.dart';
28+
29+
final app = Pharaoh();
30+
31+
void main() async {
32+
// Basic rate limiting: 100 requests per 15 minutes
33+
app.use(rateLimit(
34+
max: 100,
35+
windowMs: Duration(minutes: 15),
36+
));
37+
38+
app.get('/api/data', (req, res) {
39+
return res.json({'message': 'Hello World!'});
40+
});
41+
42+
await app.listen(port: 3000);
43+
}
44+
```
45+
46+
## Configuration Options
47+
48+
### Basic Options
49+
50+
```dart
51+
app.use(rateLimit(
52+
max: 100, // Maximum requests per window
53+
windowMs: Duration(minutes: 15), // Time window
54+
message: 'Too many requests!', // Custom error message
55+
statusCode: 429, // HTTP status code for rate limited requests
56+
));
57+
```
58+
59+
### Advanced Options
60+
61+
```dart
62+
app.use(rateLimit(
63+
max: 50,
64+
windowMs: Duration(minutes: 1),
65+
66+
// Custom key generation (default: IP address)
67+
keyGenerator: (req) => req.headers['user-id']?.toString() ?? req.ipAddr,
68+
69+
// Skip rate limiting for certain requests
70+
skip: (req) => req.headers['x-api-key'] == 'admin-key',
71+
72+
// Response headers
73+
standardHeaders: true, // RateLimit-* headers (default: true)
74+
legacyHeaders: false, // X-RateLimit-* headers (default: false)
75+
76+
// Rate limiting algorithm
77+
algorithm: RateLimitAlgorithm.tokenBucket, // or slidingWindow
78+
));
79+
```
80+
81+
## Algorithms
82+
83+
### Token Bucket (Default)
84+
85+
Tokens are added to a bucket at a fixed rate. Each request consumes a token. When the bucket is empty, requests are rate limited.
86+
87+
```dart
88+
app.use(rateLimit(
89+
max: 10,
90+
windowMs: Duration(seconds: 60),
91+
algorithm: RateLimitAlgorithm.tokenBucket,
92+
));
93+
```
94+
95+
### Sliding Window
96+
97+
Tracks requests in a sliding time window. More memory intensive but provides smoother rate limiting.
98+
99+
```dart
100+
app.use(rateLimit(
101+
max: 10,
102+
windowMs: Duration(seconds: 60),
103+
algorithm: RateLimitAlgorithm.slidingWindow,
104+
));
105+
```
106+
107+
## Response Headers
108+
109+
When rate limiting is active, the following headers are added to responses:
110+
111+
### Standard Headers (enabled by default)
112+
- `RateLimit-Limit`: Request limit per window
113+
- `RateLimit-Remaining`: Remaining requests in current window
114+
- `RateLimit-Reset`: Unix timestamp when the window resets
115+
- `Retry-After`: Seconds to wait before retrying (when rate limited)
116+
117+
### Legacy Headers (optional)
118+
- `X-RateLimit-Limit`: Request limit per window
119+
- `X-RateLimit-Remaining`: Remaining requests in current window
120+
- `X-RateLimit-Reset`: Unix timestamp when the window resets
121+
122+
## Examples
123+
124+
### Per-Route Rate Limiting
125+
126+
```dart
127+
// Global rate limiting
128+
app.use(rateLimit(max: 1000, windowMs: Duration(hours: 1)));
129+
130+
// Stricter limits for auth endpoints
131+
app.use('/auth', rateLimit(
132+
max: 5,
133+
windowMs: Duration(minutes: 15),
134+
message: 'Too many login attempts',
135+
));
136+
137+
app.post('/auth/login', (req, res) {
138+
// Login logic
139+
});
140+
```
141+
142+
### User-Based Rate Limiting
143+
144+
```dart
145+
app.use(rateLimit(
146+
max: 100,
147+
windowMs: Duration(hours: 1),
148+
keyGenerator: (req) {
149+
// Rate limit by user ID instead of IP
150+
final userId = req.auth?['userId'];
151+
return userId?.toString() ?? req.ipAddr;
152+
},
153+
));
154+
```
155+
156+
### Skip Rate Limiting
157+
158+
```dart
159+
app.use(rateLimit(
160+
max: 50,
161+
windowMs: Duration(minutes: 1),
162+
skip: (req) {
163+
// Skip rate limiting for admin users
164+
return req.auth?['role'] == 'admin';
165+
},
166+
));
167+
```
168+
169+
## Testing
170+
171+
Run the test suite:
172+
173+
```bash
174+
cd packages/pharaoh_rate_limit
175+
dart test
176+
```
177+
178+
## Contributing
179+
180+
Contributions are welcome! Please read the [contributing guidelines](../../CONTRIBUTING.md) before submitting PRs.
181+
182+
## License
183+
184+
This project is licensed under the MIT License - see the [LICENSE](../../LICENSE) file for details.
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import 'package:pharaoh/pharaoh.dart';
2+
import 'package:pharaoh_rate_limit/pharaoh_rate_limit.dart';
3+
4+
final app = Pharaoh();
5+
6+
void main() async {
7+
// Basic rate limiting: 100 requests per 15 minutes
8+
app.use(rateLimit(
9+
max: 100,
10+
windowMs: Duration(minutes: 15),
11+
message: 'Too many requests from this IP, please try again later.',
12+
));
13+
14+
// API routes
15+
app.get('/api/users', (req, res) {
16+
return res.json([
17+
{'id': 1, 'name': 'John Doe'},
18+
{'id': 2, 'name': 'Jane Smith'},
19+
]);
20+
});
21+
22+
app.get('/api/posts', (req, res) {
23+
return res.json([
24+
{'id': 1, 'title': 'Hello World', 'author': 'John'},
25+
{'id': 2, 'title': 'Dart is Awesome', 'author': 'Jane'},
26+
]);
27+
});
28+
29+
// More restrictive rate limiting for auth endpoints
30+
final authLimiter = rateLimit(
31+
max: 5,
32+
windowMs: Duration(minutes: 15),
33+
message: 'Too many authentication attempts, please try again later.',
34+
statusCode: 429,
35+
);
36+
37+
app.use(authLimiter);
38+
39+
app.post('/auth/login', (req, res) {
40+
// Simulate login logic
41+
final body = req.body as Map<String, dynamic>?;
42+
final username = body?['username'];
43+
final password = body?['password'];
44+
45+
if (username == 'admin' && password == 'secret') {
46+
return res.json({'token': 'fake-jwt-token', 'user': username});
47+
}
48+
49+
return res.status(401).json({'error': 'Invalid credentials'});
50+
});
51+
52+
await app.listen(port: 3000);
53+
print('Server running on http://localhost:3000');
54+
print('Try making multiple requests to see rate limiting in action!');
55+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/// Rate limiting middleware for Pharaoh web framework.
2+
///
3+
/// Provides token bucket and sliding window rate limiting algorithms
4+
/// to protect APIs from abuse and ensure fair usage.
5+
library;
6+
7+
export 'src/rate_limiter.dart';
8+
export 'src/token_bucket.dart';
9+
export 'src/sliding_window.dart';
10+
export 'src/rate_limit_middleware.dart';

0 commit comments

Comments
 (0)