✨ 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:
@@ -13,12 +13,13 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
chimiddleware "github.com/go-chi/chi/v5/middleware"
|
||||
"github.com/rs/zerolog/log"
|
||||
httpSwagger "github.com/swaggo/http-swagger"
|
||||
|
||||
"dance-lessons-coach/pkg/config"
|
||||
"dance-lessons-coach/pkg/greet"
|
||||
"dance-lessons-coach/pkg/middleware"
|
||||
"dance-lessons-coach/pkg/telemetry"
|
||||
"dance-lessons-coach/pkg/user"
|
||||
userapi "dance-lessons-coach/pkg/user/api"
|
||||
@@ -125,7 +126,7 @@ func initializeUserServices(cfg *config.Config) (user.UserRepository, user.UserS
|
||||
|
||||
func (s *Server) setupRoutes() {
|
||||
// Use Zerolog middleware instead of Chi's default logger
|
||||
s.router.Use(middleware.RequestLogger(&middleware.DefaultLogFormatter{
|
||||
s.router.Use(chimiddleware.RequestLogger(&chimiddleware.DefaultLogFormatter{
|
||||
Logger: &log.Logger,
|
||||
NoColor: false,
|
||||
}))
|
||||
@@ -177,6 +178,13 @@ func (s *Server) registerApiV1Routes(r chi.Router) {
|
||||
greetService := greet.NewService()
|
||||
greetHandler := greet.NewApiV1GreetHandler(greetService)
|
||||
|
||||
// Create rate limit middleware
|
||||
rateLimitMiddleware := middleware.NewRateLimiter(middleware.RateLimitConfig{
|
||||
Enabled: s.config.GetRateLimitEnabled(),
|
||||
RequestsPerMinute: s.config.GetRateLimitRequestsPerMinute(),
|
||||
BurstSize: s.config.GetRateLimitBurstSize(),
|
||||
})
|
||||
|
||||
// Create auth middleware if available
|
||||
var authMiddleware *AuthMiddleware
|
||||
if s.userService != nil {
|
||||
@@ -184,6 +192,8 @@ func (s *Server) registerApiV1Routes(r chi.Router) {
|
||||
}
|
||||
|
||||
r.Route("/greet", func(r chi.Router) {
|
||||
// Add rate limiting middleware for greet endpoint
|
||||
r.Use(rateLimitMiddleware.Middleware)
|
||||
// Add optional authentication middleware
|
||||
if authMiddleware != nil {
|
||||
r.Use(authMiddleware.Middleware)
|
||||
@@ -220,8 +230,8 @@ func (s *Server) registerApiV2Routes(r chi.Router) {
|
||||
// getAllMiddlewares returns all middleware including OpenTelemetry if enabled
|
||||
func (s *Server) getAllMiddlewares() []func(http.Handler) http.Handler {
|
||||
middlewares := []func(http.Handler) http.Handler{
|
||||
middleware.StripSlashes,
|
||||
middleware.Recoverer,
|
||||
chimiddleware.StripSlashes,
|
||||
chimiddleware.Recoverer,
|
||||
}
|
||||
|
||||
if s.withOTEL {
|
||||
|
||||
Reference in New Issue
Block a user