57 lines
1.9 KiB
Go
57 lines
1.9 KiB
Go
package middleware
|
|
|
|
import (
|
|
"net/http"
|
|
"strconv"
|
|
"time"
|
|
|
|
"agent-desk/internal/pkg/errorsx"
|
|
"agent-desk/internal/pkg/httpx"
|
|
"agent-desk/internal/pkg/ratelimit"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
// rateLimitMessageKey is the localized explanation returned with a 429. It
|
|
// deliberately does not disclose the limit or the remaining budget, which would
|
|
// only tell a caller how much room they have left.
|
|
const rateLimitMessageKey = "error.e0354"
|
|
|
|
// RateLimit bounds how often one client address may call a single public
|
|
// endpoint.
|
|
//
|
|
// The address comes from ctx.ClientIP(), which is only meaningful because
|
|
// NewServer configures Gin's trusted proxies and platform before any middleware
|
|
// is registered. Without that a caller picks their own bucket by setting
|
|
// X-Forwarded-For, and the limit is decoration.
|
|
//
|
|
// A nil limiter allows everything, so "disabled" is expressed by handing out nil
|
|
// limiters rather than by branching at every call site.
|
|
//
|
|
// Rejections are not logged here. requestLogMiddleware already records the path,
|
|
// status and client address of every request, so a 429 is visible there without
|
|
// giving a flood a second way to fill the log.
|
|
func RateLimit(limiter *ratelimit.Limiter) gin.HandlerFunc {
|
|
return func(ctx *gin.Context) {
|
|
allowed, retryAfter := limiter.Allow(ctx.ClientIP())
|
|
if allowed {
|
|
ctx.Next()
|
|
return
|
|
}
|
|
// Retry-After is a whole number of seconds, so round up: telling a caller
|
|
// to come back sooner than the window actually resets just earns another
|
|
// 429. Rounding down and adding one overshoots when the remaining time is
|
|
// already an exact number of seconds.
|
|
seconds := int64(retryAfter / time.Second)
|
|
if time.Duration(seconds)*time.Second < retryAfter {
|
|
seconds++
|
|
}
|
|
if seconds < 1 {
|
|
seconds = 1
|
|
}
|
|
ctx.Header("Retry-After", strconv.FormatInt(seconds, 10))
|
|
httpx.WriteHttpStatusJSON(ctx, http.StatusTooManyRequests, errorsx.InvalidParamI18n(rateLimitMessageKey))
|
|
ctx.Abort()
|
|
}
|
|
}
|