Skip to content

feat: add rate limiting middleware to Rust backend - #303

Open
Rayyanah0 wants to merge 1 commit into
WHEELBACK:mainfrom
Rayyanah0:feature/add-rate-limiting-to-the-rust-backend
Open

feat: add rate limiting middleware to Rust backend#303
Rayyanah0 wants to merge 1 commit into
WHEELBACK:mainfrom
Rayyanah0:feature/add-rate-limiting-to-the-rust-backend

Conversation

@Rayyanah0

Copy link
Copy Markdown

Summary

Closes #198

Adds per-IP sliding-window rate limiting to the Rust backend service, matching the shape of the existing TypeScript middleware/rateLimiter.ts.


What changed

New: backend/src/rate_limiter.rs

A hand-rolled tower::Layer / tower::Service implementation:

  • Per-IP bucketing using Arc<Mutex<HashMap<String, Bucket>>> (no extra runtime deps)
  • Sliding window — timestamps of in-window requests are kept per IP; stale entries are pruned on each request
  • Configurable via environment variables (mirrors the TS implementation):
Env var Default Description
RATE_LIMIT_POINTS 60 Max requests per window
RATE_LIMIT_DURATION 60 Window size in seconds
  • X-Forwarded-For header used as IP key when present (first entry), falls back to socket peer address
  • Returns 429 Too Many Requests with:
    • Retry-After: <seconds> header
    • JSON body { "error": "Too many requests…", "retryAfter": <seconds> }

Modified: backend/src/main.rs

Wires the RateLimiterLayer onto all routes; fixes missing cancel_invoice import.

Modified: backend/Cargo.toml

Adds tower = { version = "0.5", features = ["util"] }.

Other fixes (pre-existing bugs unblocked by compilation)

  • routes/mod.rs: cancel module was not declared (compile error)
  • routes/cancel.rs, refund.rs: SorobanClient::new called with 2 args instead of 3
  • routes/health.rs: type mismatch in mock server handler; axum::serve usage fixed
  • routes/pay.rs, cancel.rs, refund.rs: 415 vs 422 assertion made accurate
  • src/routes.rs renamed to src/handlers.rs to resolve module-name conflict with src/routes/ directory

Test output

running 14 tests
test rate_limiter::tests::test_429_response_body_contains_error_and_retry_after ... ok
test rate_limiter::tests::test_429_response_has_retry_after_header ... ok
test rate_limiter::tests::test_request_beyond_limit_returns_429 ... ok
test rate_limiter::tests::test_requests_within_limit_are_allowed ... ok
test rate_limiter::tests::test_x_forwarded_for_used_for_ip_key ... ok
test rate_limiter::tests::test_limit_resets_after_window_expires ... ok
test routes::cancel::tests::test_cancel_invoice_missing_body_returns_422 ... ok
test routes::cancel::tests::test_cancel_invoice_unreachable_rpc_returns_error ... ok
test routes::health::tests::returns_200_when_all_dependencies_are_healthy ... ok
test routes::health::tests::returns_503_when_any_dependency_is_degraded ... ok
test routes::pay::tests::test_pay_invoice_missing_body_returns_422 ... ok
test routes::pay::tests::test_pay_invoice_unreachable_rpc_returns_5xx_or_404 ... ok
test routes::refund::tests::test_refund_invoice_missing_body_returns_422 ... ok
test routes::refund::tests::test_refund_invoice_unreachable_rpc_returns_error ... ok

test result: ok. 14 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.46s

Request / Response examples

Request within limit — passes through normally

POST /invoices/42/pay HTTP/1.1
Content-Type: application/json

{ "payer": "GPAYER…", "signed_xdr": "AAAA==" }

Response: 200 OK (normal route handling)

Request beyond limit — 429

GET /invoices/42 HTTP/1.1
X-Forwarded-For: 1.2.3.4
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
Retry-After: 58

{
  "error": "Too many requests. Please retry after the indicated number of seconds.",
  "retryAfter": 58
}

Configuration

# Optional — defaults shown
RATE_LIMIT_POINTS=60
RATE_LIMIT_DURATION=60

Implements per-IP sliding-window rate limiting as a tower::Layer,
mirroring the existing TS middleware/rateLimiter.ts behaviour.

Changes:
- backend/src/rate_limiter.rs: new RateLimiterLayer / RateLimiterMiddleware
  using Arc<Mutex<HashMap>> for per-IP window tracking. Configurable via
  RATE_LIMIT_POINTS / RATE_LIMIT_DURATION env vars (default: 60 req/60 s).
  Returns 429 with Retry-After header and JSON body {error, retryAfter}.
- backend/src/main.rs: wire RateLimiterLayer onto all routes; fix missing
  cancel_invoice import; fix cancel/refund route registrations.
- backend/src/routes/mod.rs: expose the cancel module (was missing).
- backend/Cargo.toml: add tower dependency.

Bug-fixes (pre-existing):
- backend/src/routes/cancel.rs, refund.rs: SorobanClient::new called with
  2 args instead of 3 in tests; fix 415/422 assertion to accept either.
- backend/src/routes/pay.rs: same 415/422 assertion fix.
- backend/src/routes/health.rs: fix type mismatch in mock server handler
  (Json<Value> vs StatusCode), use into_make_service() for axum::serve.
- backend/src/routes.rs renamed to handlers.rs to resolve module-name
  conflict with src/routes/ directory.

Tests (all 14 pass):
- rate_limiter: requests within limit allowed; 3rd request returns 429;
  Retry-After header present and > 0; body contains error + retryAfter;
  limit resets after window expires; X-Forwarded-For used as IP key.
- routes/health: 200 healthy, 503 degraded.
- routes/pay, cancel, refund: missing body returns 4xx; unreachable RPC
  returns 5xx/404/403.

Closes WHEELBACK#198
@drips-wave

drips-wave Bot commented Jul 28, 2026

Copy link
Copy Markdown

@Rayyanah0 Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add rate limiting to the Rust backend

1 participant