Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ if(BUILD_EXAMPLES)

add_executable(todo_api examples/todo_api.cpp)
target_link_libraries(todo_api PRIVATE cpphttp)

add_executable(rate_limited_api examples/rate_limited_api.cpp)
target_link_libraries(rate_limited_api PRIVATE cpphttp)
endif()

# ── Tests (optional) ──────────────────────────────────────────────────────────
Expand Down
33 changes: 33 additions & 0 deletions examples/rate_limited_api.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
#include <cpphttp.hpp>
#include <cpphttp/middleware/rate_limiter.hpp>
#include <chrono>
#include <csignal>
#include <iostream>

using namespace cpphttp;

Server* g_server = nullptr;

void signal_handler(int) {
std::cout << "\nShutting down...\n";
if (g_server) g_server->stop();
}

int main() {
std::signal(SIGINT, signal_handler);

Server app;
g_server = &app;

// 5 requests per 10 seconds, per client IP.
static middleware::RateLimiter limiter(5, std::chrono::seconds(10));
app.use(limiter.handler());

app.get("/api/ping", [](const Request& /*req*/, Response& res) {
res.json(R"({"message":"pong"})");
});

// Try hammering this route more than 5 times in 10s — you'll get a 429.
std::cout << "Rate-limited server on :8080 (5 req / 10s per IP)\n";
app.listen(8080);
}
89 changes: 89 additions & 0 deletions include/cpphttp/middleware/rate_limiter.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
#pragma once

#include <algorithm>
#include <chrono>
#include <cmath>
#include <functional>
#include <mutex>
#include <string>
#include <unordered_map>

#include "../request.hpp"
#include "../response.hpp"

namespace cpphttp::middleware {

/**
* @brief Per-client token-bucket rate limiter.
*
* Usage:
* cpphttp::middleware::RateLimiter limiter(100, std::chrono::seconds(60));
* app.use(limiter.handler());
*
* Buckets are keyed by Request::remote_addr. Each bucket refills
* continuously at max_requests / window, capped at max_requests.
* Thread-safe: guarded by a single mutex, sized for thread-per-connection
* servers with moderate concurrency.
*/
class RateLimiter {
public:
RateLimiter(std::size_t max_requests, std::chrono::steady_clock::duration window)
: max_requests_(max_requests), window_(window) {}

using Handler = std::function<void(const Request&, Response&)>;

Handler handler() {
return [this](const Request& req, Response& res) { this->check(req, res); };
}

void check(const Request& req, Response& res) {
const auto& key = req.remote_addr;
const auto now = std::chrono::steady_clock::now();

std::lock_guard<std::mutex> lock(mutex_);
auto it = buckets_.find(key);
if (it == buckets_.end()) {
it = buckets_.emplace(key, Bucket{static_cast<double>(max_requests_), now}).first;
}
auto& bucket = it->second;

if (bucket.tokens < static_cast<double>(max_requests_)) {
const double elapsed_s =
std::chrono::duration<double>(now - bucket.last_refill).count();
const double refill_rate =
static_cast<double>(max_requests_) /
std::chrono::duration<double>(window_).count();
bucket.tokens =
std::min(static_cast<double>(max_requests_),
bucket.tokens + elapsed_s * refill_rate);
}
bucket.last_refill = now;

if (bucket.tokens < 1.0) {
const double refill_rate =
static_cast<double>(max_requests_) /
std::chrono::duration<double>(window_).count();
const int retry_after_s =
static_cast<int>(std::ceil((1.0 - bucket.tokens) / refill_rate));
res.set("Retry-After", std::to_string(retry_after_s))
.status(429)
.json(R"({"error":"Too Many Requests"})");
return;
}

bucket.tokens -= 1.0;
}

private:
struct Bucket {
double tokens = 0.0;
std::chrono::steady_clock::time_point last_refill = std::chrono::steady_clock::now();
};

std::size_t max_requests_;
std::chrono::steady_clock::duration window_;
std::unordered_map<std::string, Bucket> buckets_;
std::mutex mutex_;
};

} // namespace cpphttp::middleware
Loading