Skip to content

Commit 484d7db

Browse files
committed
feat(rate-limit): implement MongoDB-backed rate limit service
- Add MongoDbRateLimitService class implementing RateLimitService interface - Use MongoDB TTL index for efficient automatic purging of old records - Implement checkRequest method with counting and limiting logic - Add error handling and logging
1 parent adcc0eb commit 484d7db

File tree

1 file changed

+87
-0
lines changed

1 file changed

+87
-0
lines changed
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import 'package:core/core.dart';
2+
import 'package:data_mongodb/data_mongodb.dart';
3+
import 'package:flutter_news_app_api_server_full_source_code/src/services/rate_limit_service.dart';
4+
import 'package:logging/logging.dart';
5+
import 'package:mongo_dart/mongo_dart.dart';
6+
7+
/// The name of the MongoDB collection for storing rate limit attempts.
8+
const String kRateLimitAttemptsCollection = 'rate_limit_attempts';
9+
10+
/// {@template mongodb_rate_limit_service}
11+
/// A MongoDB-backed implementation of [RateLimitService].
12+
///
13+
/// This service tracks request attempts in a dedicated MongoDB collection.
14+
/// It relies on a TTL (Time-To-Live) index on the `createdAt` field to
15+
/// ensure that old request records are automatically purged by the database,
16+
/// which is highly efficient.
17+
/// {@endtemplate}
18+
class MongoDbRateLimitService implements RateLimitService {
19+
/// {@macro mongodb_rate_limit_service}
20+
MongoDbRateLimitService({
21+
required MongoDbConnectionManager connectionManager,
22+
required Logger log,
23+
}) : _connectionManager = connectionManager,
24+
_log = log;
25+
26+
final MongoDbConnectionManager _connectionManager;
27+
final Logger _log;
28+
29+
DbCollection get _collection =>
30+
_connectionManager.db.collection(kRateLimitAttemptsCollection);
31+
32+
@override
33+
Future<void> checkRequest({
34+
required String key,
35+
required int limit,
36+
required Duration window,
37+
}) async {
38+
try {
39+
final now = DateTime.now();
40+
final windowStart = now.subtract(window);
41+
42+
// 1. Count recent requests for the given key within the time window.
43+
final recentRequestsCount = await _collection.count(
44+
where.eq('key', key).and(where.gte('createdAt', windowStart)),
45+
);
46+
47+
_log.finer(
48+
'Rate limit check for key "$key": Found $recentRequestsCount '
49+
'requests in the last ${window.inMinutes} minutes (limit is $limit).',
50+
);
51+
52+
// 2. If the limit is reached or exceeded, throw an exception.
53+
if (recentRequestsCount >= limit) {
54+
_log.warning(
55+
'Rate limit exceeded for key "$key". '
56+
'($recentRequestsCount >= $limit)',
57+
);
58+
throw ForbiddenException(
59+
'You have made too many requests. Please try again later.',
60+
);
61+
}
62+
63+
// 3. If the limit is not reached, record the new request.
64+
await _collection.insertOne({
65+
'_id': ObjectId(),
66+
'key': key,
67+
'createdAt': now,
68+
});
69+
_log.finer('Recorded new request for key "$key".');
70+
} on HttpException {
71+
// Re-throw exceptions that we've thrown intentionally.
72+
rethrow;
73+
} catch (e, s) {
74+
_log.severe('Error during rate limit check for key "$key"', e, s);
75+
throw OperationFailedException(
76+
'An unexpected error occurred while checking request rate limits.',
77+
);
78+
}
79+
}
80+
81+
@override
82+
void dispose() {
83+
// This is a no-op because the underlying database connection is managed
84+
// by the injected MongoDbConnectionManager, which has its own lifecycle.
85+
_log.finer('dispose() called, no action needed.');
86+
}
87+
}

0 commit comments

Comments
 (0)