✨ feat(server): add per-IP rate limit middleware on /api/v1/greet
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>
This commit is contained in:
@@ -676,6 +676,25 @@ func (s *Server) shouldEnableV2() bool {
|
||||
// createTestConfig creates a test configuration
|
||||
// Pass v2Enabled explicitly to avoid reading env vars deep in the stack
|
||||
func createTestConfig(port int, v2Enabled bool) *config.Config {
|
||||
// Check for rate limit env vars, use defaults if not set
|
||||
rateLimitEnabled := true
|
||||
rateLimitRPM := 60
|
||||
rateLimitBurst := 10
|
||||
|
||||
if env := os.Getenv("DLC_RATE_LIMIT_ENABLED"); env != "" {
|
||||
rateLimitEnabled = strings.EqualFold(env, "true") || env == "1"
|
||||
}
|
||||
if env := os.Getenv("DLC_RATE_LIMIT_REQUESTS_PER_MINUTE"); env != "" {
|
||||
if val, err := strconv.Atoi(env); err == nil {
|
||||
rateLimitRPM = val
|
||||
}
|
||||
}
|
||||
if env := os.Getenv("DLC_RATE_LIMIT_BURST_SIZE"); env != "" {
|
||||
if val, err := strconv.Atoi(env); err == nil {
|
||||
rateLimitBurst = val
|
||||
}
|
||||
}
|
||||
|
||||
return &config.Config{
|
||||
Server: config.ServerConfig{
|
||||
Host: "0.0.0.0",
|
||||
@@ -702,5 +721,10 @@ func createTestConfig(port int, v2Enabled bool) *config.Config {
|
||||
Logging: config.LoggingConfig{
|
||||
Level: "debug",
|
||||
},
|
||||
RateLimit: config.RateLimitConfig{
|
||||
Enabled: rateLimitEnabled,
|
||||
RequestsPerMinute: rateLimitRPM,
|
||||
BurstSize: rateLimitBurst,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user