feat(server): add per-IP rate limit middleware on /api/v1/greet #22

Merged
arcodange merged 1 commits from feat/rate-limit-middleware into main 2026-05-03 13:16:30 +02:00
Owner

Summary

Implements Phase 1 of ADR-0022 (Rate Limiting and Cache Strategy): in-memory per-IP rate limiting middleware. Wired onto /api/v1/greet as proof of integration. Closes #13 (Phase 1 only).

Spec

  • Library: golang.org/x/time/rate (golang.org/x/time v0.15.0)
  • Config: 3 fields under ratelimit.* (Enabled/RequestsPerMinute/BurstSize), default 60 RPM / burst 10 / enabled
  • Behavior: per-IP limiter, HTTP 429 with JSON body {"error":"rate_limited",...} and Retry-After header when exceeded
  • Disabled config = no-op middleware (always allows)
  • Per-IP map TTL eviction: entries unused > 10 min are evicted on access

Out of scope

  • Phase 2 (Redis-compatible shared cache via Dragonfly/KeyDB) — separate PR
  • Caching service (Cache.Get/Set) — separate PR
  • Rate limiting on /api/v2 or other routes — separate PR

Changes

  • pkg/middleware/ratelimit.go (NEW, 153 lines) : middleware implementation
  • pkg/middleware/ratelimit_test.go (NEW, 310 lines) : 7 unit tests
  • pkg/config/config.go : RateLimit struct + defaults + env binds + getters
  • pkg/server/server.go : wire on /api/v1/greet
  • pkg/bdd/testserver/server.go : env-var support for rate limit config (BDD plumbing)
  • pkg/bdd/steps/ratelimit_steps.go (NEW) : step definitions
  • features/greet/greet.feature : new scenario (currently @skip @bdd-deferred — see commit message)

Test plan

  • go build ./pkg/middleware/... PASS
  • go build ./pkg/config/... PASS
  • go build ./pkg/server/... PASS
  • go test ./pkg/middleware/... 7/7 PASS
  • go test ./pkg/config/... PASS
  • go test ./pkg/server/... PASS
  • go test ./features/greet/... PASS (ratelimit scenario @skip until testserver rework)
  • CI passes on the branch

Migration context

ARCODANGE Phase 1 — autonomous run via Mistral Vibe with ICM workspace at ~/Work/Vibe/workspaces/rate-limit-middleware/. ~95% autonomy : Mistral wrote the entire feature including unit tests, BDD scenario, and ratelimit_steps.go. Trainer finalized commit/PR (Mistral hit max-turns).

🤖 Co-Authored-By: Mistral Vibe + Claude Opus 4.7

## Summary Implements Phase 1 of [ADR-0022](adr/0022-rate-limiting-and-cache-strategy.md) (Rate Limiting and Cache Strategy): in-memory per-IP rate limiting middleware. Wired onto `/api/v1/greet` as proof of integration. Closes #13 (Phase 1 only). ## Spec - Library: `golang.org/x/time/rate` (golang.org/x/time v0.15.0) - Config: 3 fields under `ratelimit.*` (Enabled/RequestsPerMinute/BurstSize), default 60 RPM / burst 10 / enabled - Behavior: per-IP limiter, HTTP 429 with JSON body `{"error":"rate_limited",...}` and `Retry-After` header when exceeded - Disabled config = no-op middleware (always allows) - Per-IP map TTL eviction: entries unused > 10 min are evicted on access ## Out of scope - Phase 2 (Redis-compatible shared cache via Dragonfly/KeyDB) — separate PR - Caching service (Cache.Get/Set) — separate PR - Rate limiting on /api/v2 or other routes — separate PR ## Changes - `pkg/middleware/ratelimit.go` (NEW, 153 lines) : middleware implementation - `pkg/middleware/ratelimit_test.go` (NEW, 310 lines) : 7 unit tests - `pkg/config/config.go` : RateLimit struct + defaults + env binds + getters - `pkg/server/server.go` : wire on /api/v1/greet - `pkg/bdd/testserver/server.go` : env-var support for rate limit config (BDD plumbing) - `pkg/bdd/steps/ratelimit_steps.go` (NEW) : step definitions - `features/greet/greet.feature` : new scenario (currently `@skip @bdd-deferred` — see commit message) ## Test plan - [x] `go build ./pkg/middleware/...` PASS - [x] `go build ./pkg/config/...` PASS - [x] `go build ./pkg/server/...` PASS - [x] `go test ./pkg/middleware/...` 7/7 PASS - [x] `go test ./pkg/config/...` PASS - [x] `go test ./pkg/server/...` PASS - [x] `go test ./features/greet/...` PASS (ratelimit scenario @skip until testserver rework) - [ ] CI passes on the branch ## Migration context ARCODANGE Phase 1 — autonomous run via Mistral Vibe with ICM workspace at `~/Work/Vibe/workspaces/rate-limit-middleware/`. ~95% autonomy : Mistral wrote the entire feature including unit tests, BDD scenario, and ratelimit_steps.go. Trainer finalized commit/PR (Mistral hit max-turns). 🤖 Co-Authored-By: Mistral Vibe + Claude Opus 4.7
arcodange added 1 commit 2026-05-03 13:16:17 +02:00
Implements Phase 1 of ADR-0022 (Rate Limiting and Cache Strategy):
in-memory per-IP rate limiter using golang.org/x/time/rate. Returns
HTTP 429 with JSON body and Retry-After header when exceeded.

Changes:
- New: pkg/middleware/ratelimit.go (153 lines, 7 unit tests in ratelimit_test.go)
- Modified: pkg/config/config.go (RateLimit struct + 3 SetDefaults + 3 BindEnv + 3 getters)
- Modified: pkg/server/server.go (wire on /api/v1/greet, conditional on Enabled)
- Modified: pkg/bdd/testserver/server.go (env-var support for rate limit config)
- New: pkg/bdd/steps/ratelimit_steps.go (step definitions)
- Added: features/greet/greet.feature scenario (currently @skip @bdd-deferred — see note below)

Known limitation:
The BDD scenario is tagged @skip @bdd-deferred because the testserver
loads its config once at startup; env vars set inside a step do not
reach the already-running server. The middleware itself is fully
covered by unit tests. To re-enable BDD, the testserver needs either
an admin endpoint or a per-scenario fresh-server pattern.

Closes #13 (Phase 1 only — Phase 2 Redis + cache service deferred).

Generated ~95% in autonomy by Mistral Vibe via ICM workspace
~/Work/Vibe/workspaces/rate-limit-middleware/.
Trainer (Claude) finalized the commit/PR step (Mistral hit max-turns).

🤖 Co-Authored-By: Mistral Vibe (devstral-2 / mistral-medium-3.5)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
arcodange merged commit 54dd0cc80f into main 2026-05-03 13:16:30 +02:00
arcodange deleted branch feat/rate-limit-middleware 2026-05-03 13:16:31 +02:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: arcodange/dance-lessons-coach#22