Skip to content

feat(api): IP/key rate limit on /api routes with configurable thresholds#19

Merged
BlindMaster24 merged 3 commits into
devin/1777023056-health-metricsfrom
devin/1777024936-rate-limit
Apr 27, 2026
Merged

feat(api): IP/key rate limit on /api routes with configurable thresholds#19
BlindMaster24 merged 3 commits into
devin/1777023056-health-metricsfrom
devin/1777024936-rate-limit

Conversation

@devin-ai-integration
Copy link
Copy Markdown

@devin-ai-integration devin-ai-integration Bot commented Apr 24, 2026

Summary

Adds express-rate-limit in front of ${config.api.url}/:method to cap abuse from unauthenticated (and authenticated) callers. Complements the existing per-API-key quota check inside the handler, which continues to enforce each key's configured limitPerSec.

Why: until now anonymous hits on /api/* could loop indefinitely before the auth check — trivial DoS surface. IP-only limiting wasn't enough either (shared NATs), so the limiter keys by authorization token when present and falls back to IP otherwise.

What's new:

  • express-rate-limit@latest (v8). Exposed factory createApiRateLimiter from src/services/api/index.ts so unit tests don't have to bootstrap the whole service.
  • Limiter is installed only when config.api.rateLimit.enabled === true (default true).
  • Keyed by auth token first, then ip — a cheap anon scraper can't drain the pool for legitimate keyed clients.
  • Standard IETF RateLimit headers (standardHeaders: 'draft-7', legacyHeaders: false).
  • 429 body matches the existing per-key error shape: {"error":"Превышен лимит запросов"}.
  • config.api.rateLimit: { enabled, windowMs, max, trustProxy } with sane defaults (120 req/min). Zod schema + example config + AGENTS.md updated.
  • HttpService.run() honors trustProxy — required when running behind nginx/traefik/Cloudflare so the IP fallback key is the real client, not the proxy.
  • /api/health and /api/metrics (registered on HttpService in PR feat(http): unauthenticated /api/health and /api/metrics on http service #18) are NOT behind this limiter — they're monitoring endpoints and need to stay open.

Tests (tests/api/rate-limit.test.ts):

  • returns 429 after the configured max for anonymous IP
  • counts per-token independently from per-IP
  • emits the RateLimit standard header on successful responses

Checks:

  • pnpm run test:all — 31 files / 207 tests pass
  • pnpm run ts-check / pnpm run lint / pnpm run format:check — clean
  • pnpm audit --audit-level=low — no known vulnerabilities

Run the new tests with: pnpm exec vitest run tests/api/rate-limit.test.ts

Stacked on PR #18.

Review & Testing Checklist for Human

  • Pick a config with api.rateLimit.max: 5, run docker compose up, hit any /api/... endpoint 6 times from the same IP → the 6th should return 429 with the Russian error payload.
  • Hit the same endpoint with two different Authorization: Bearer tokens concurrently — each token should have its own bucket.
  • If deploying behind a reverse proxy, set api.rateLimit.trustProxy: true and confirm X-Forwarded-For is honored (otherwise everybody shares one bucket).
  • /api/health and /api/metrics stay unthrottled — verify by hammering them under the limit.

Notes

  • Per-API-key quotas (ApiKeyModel.limitPerSec) remain in place inside the handler; this PR adds a second, broader layer that also catches anonymous abuse.
  • The limiter uses in-memory store (per process). If you later run multi-instance, swap to rate-limit-redis — the factory makes that a one-line change.
  • Next: PR #L (graceful shutdown) → PR #M (grammY smoke tests).

Link to Devin session: https://app.devin.ai/sessions/7732f5fd16e9448295cbabeb8b5f471a
Requested by: @BlindMaster24


Open in Devin Review

@devin-ai-integration
Copy link
Copy Markdown
Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

devin-ai-integration[bot]

This comment was marked as resolved.

@BlindMaster24 BlindMaster24 merged commit e228e84 into devin/1777023056-health-metrics Apr 27, 2026
8 checks passed
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.

1 participant