Skip to content

Commit 0ffeed3

Browse files
authored
Merge pull request #1 from codcreater1/feat/rate-limiter-middleware
Add token-bucket rate limiter middleware + example
2 parents 3b6b8e7 + 092bae3 commit 0ffeed3

3 files changed

Lines changed: 125 additions & 0 deletions

File tree

‎CMakeLists.txt‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ if(BUILD_EXAMPLES)
4040

4141
add_executable(todo_api examples/todo_api.cpp)
4242
target_link_libraries(todo_api PRIVATE cpphttp)
43+
44+
add_executable(rate_limited_api examples/rate_limited_api.cpp)
45+
target_link_libraries(rate_limited_api PRIVATE cpphttp)
4346
endif()
4447

4548
# ── Tests (optional) ──────────────────────────────────────────────────────────

‎examples/rate_limited_api.cpp‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
#include <cpphttp.hpp>
2+
#include <cpphttp/middleware/rate_limiter.hpp>
3+
#include <chrono>
4+
#include <csignal>
5+
#include <iostream>
6+
7+
using namespace cpphttp;
8+
9+
Server* g_server = nullptr;
10+
11+
void signal_handler(int) {
12+
std::cout << "\nShutting down...\n";
13+
if (g_server) g_server->stop();
14+
}
15+
16+
int main() {
17+
std::signal(SIGINT, signal_handler);
18+
19+
Server app;
20+
g_server = &app;
21+
22+
// 5 requests per 10 seconds, per client IP.
23+
static middleware::RateLimiter limiter(5, std::chrono::seconds(10));
24+
app.use(limiter.handler());
25+
26+
app.get("/api/ping", [](const Request& /*req*/, Response& res) {
27+
res.json(R"({"message":"pong"})");
28+
});
29+
30+
// Try hammering this route more than 5 times in 10s — you'll get a 429.
31+
std::cout << "Rate-limited server on :8080 (5 req / 10s per IP)\n";
32+
app.listen(8080);
33+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
#pragma once
2+
3+
#include <algorithm>
4+
#include <chrono>
5+
#include <cmath>
6+
#include <functional>
7+
#include <mutex>
8+
#include <string>
9+
#include <unordered_map>
10+
11+
#include "../request.hpp"
12+
#include "../response.hpp"
13+
14+
namespace cpphttp::middleware {
15+
16+
/**
17+
* @brief Per-client token-bucket rate limiter.
18+
*
19+
* Usage:
20+
* cpphttp::middleware::RateLimiter limiter(100, std::chrono::seconds(60));
21+
* app.use(limiter.handler());
22+
*
23+
* Buckets are keyed by Request::remote_addr. Each bucket refills
24+
* continuously at max_requests / window, capped at max_requests.
25+
* Thread-safe: guarded by a single mutex, sized for thread-per-connection
26+
* servers with moderate concurrency.
27+
*/
28+
class RateLimiter {
29+
public:
30+
RateLimiter(std::size_t max_requests, std::chrono::steady_clock::duration window)
31+
: max_requests_(max_requests), window_(window) {}
32+
33+
using Handler = std::function<void(const Request&, Response&)>;
34+
35+
Handler handler() {
36+
return [this](const Request& req, Response& res) { this->check(req, res); };
37+
}
38+
39+
void check(const Request& req, Response& res) {
40+
const auto& key = req.remote_addr;
41+
const auto now = std::chrono::steady_clock::now();
42+
43+
std::lock_guard<std::mutex> lock(mutex_);
44+
auto it = buckets_.find(key);
45+
if (it == buckets_.end()) {
46+
it = buckets_.emplace(key, Bucket{static_cast<double>(max_requests_), now}).first;
47+
}
48+
auto& bucket = it->second;
49+
50+
if (bucket.tokens < static_cast<double>(max_requests_)) {
51+
const double elapsed_s =
52+
std::chrono::duration<double>(now - bucket.last_refill).count();
53+
const double refill_rate =
54+
static_cast<double>(max_requests_) /
55+
std::chrono::duration<double>(window_).count();
56+
bucket.tokens =
57+
std::min(static_cast<double>(max_requests_),
58+
bucket.tokens + elapsed_s * refill_rate);
59+
}
60+
bucket.last_refill = now;
61+
62+
if (bucket.tokens < 1.0) {
63+
const double refill_rate =
64+
static_cast<double>(max_requests_) /
65+
std::chrono::duration<double>(window_).count();
66+
const int retry_after_s =
67+
static_cast<int>(std::ceil((1.0 - bucket.tokens) / refill_rate));
68+
res.set("Retry-After", std::to_string(retry_after_s))
69+
.status(429)
70+
.json(R"({"error":"Too Many Requests"})");
71+
return;
72+
}
73+
74+
bucket.tokens -= 1.0;
75+
}
76+
77+
private:
78+
struct Bucket {
79+
double tokens = 0.0;
80+
std::chrono::steady_clock::time_point last_refill = std::chrono::steady_clock::now();
81+
};
82+
83+
std::size_t max_requests_;
84+
std::chrono::steady_clock::duration window_;
85+
std::unordered_map<std::string, Bucket> buckets_;
86+
std::mutex mutex_;
87+
};
88+
89+
} // namespace cpphttp::middleware

0 commit comments

Comments
 (0)