fad4006f60
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
190 lines
7.2 KiB
Markdown
190 lines
7.2 KiB
Markdown
---
|
|
id: "0016"
|
|
title: "Rate Limiting"
|
|
status: completado
|
|
type: feature
|
|
domain: []
|
|
scope: multi-app
|
|
priority: media
|
|
depends: []
|
|
blocks: []
|
|
related: []
|
|
created: 2026-05-17
|
|
updated: 2026-05-17
|
|
tags: []
|
|
---
|
|
# 0016 — Rate Limiting
|
|
|
|
## Metadata
|
|
|
|
| Campo | Valor |
|
|
|-------|-------|
|
|
| **ID** | 0016 |
|
|
| **Estado** | pendiente |
|
|
| **Prioridad** | media |
|
|
| **Tipo** | feature |
|
|
|
|
## Dependencias
|
|
|
|
- **0009** (HTTP Server Foundation) — el middleware de rate limiting se integra via `http_middleware_chain`.
|
|
|
|
---
|
|
|
|
## Objetivo
|
|
|
|
Proteger cualquier API del registry contra abuso con rate limiting in-memory basado en token bucket, sin dependencias externas (no Redis). Funciones componibles que se enchufan al stack de middlewares de 0009.
|
|
|
|
## Contexto
|
|
|
|
- `sqlite_api` y futuras apps HTTP no tienen ninguna proteccion contra abuso. Un cliente puede hacer miles de requests por segundo sin limite.
|
|
- Con las funciones de HTTP server de 0009, integrar rate limiting es cuestion de componer un middleware mas en la chain.
|
|
- Token bucket es el algoritmo estandar para rate limiting HTTP: permite rafagas controladas (`burst`) mientras mantiene una tasa sostenida (`rate`).
|
|
- Go stdlib incluye `golang.org/x/time/rate` pero crear funciones propias permite control total sobre cleanup, headers y key extraction.
|
|
|
|
## Arquitectura
|
|
|
|
```
|
|
functions/infra/
|
|
rate_limiter_new.go — NEW: crea rate limiter in-memory
|
|
rate_limiter_new.md — NEW
|
|
rate_limiter_check.go — NEW: consulta si un request esta permitido
|
|
rate_limiter_check.md — NEW
|
|
rate_limit_middleware.go — NEW: middleware HTTP por IP
|
|
rate_limit_middleware.md — NEW
|
|
rate_limiter_by_key.go — NEW: rate limit por clave custom
|
|
rate_limiter_by_key.md — NEW
|
|
rate_limiter_cleanup.go — NEW: GC de entries stale
|
|
rate_limiter_cleanup.md — NEW
|
|
rate_limit_headers.go — NEW: construye headers estandar
|
|
rate_limit_headers.md — NEW
|
|
|
|
types/infra/
|
|
rate_limiter.md — NEW
|
|
rate_limit_config.md — NEW
|
|
rate_limit_result.md — NEW
|
|
```
|
|
|
|
## Diseno
|
|
|
|
### Tipos
|
|
|
|
```go
|
|
// RateLimiter mantiene estado de todos los clientes
|
|
type RateLimiter struct {
|
|
rate float64 // tokens por segundo
|
|
burst int // capacidad maxima del bucket
|
|
mu sync.Mutex
|
|
clients map[string]*clientEntry // key -> bucket state
|
|
}
|
|
|
|
type clientEntry struct {
|
|
tokens float64
|
|
lastSeen time.Time
|
|
}
|
|
|
|
// RateLimitConfig configura el middleware
|
|
type RateLimitConfig struct {
|
|
RequestsPerSecond float64 // tasa sostenida
|
|
BurstSize int // rafaga maxima
|
|
KeyFunc func(r *http.Request) string // extractor de clave (nil = IP)
|
|
CleanupInterval time.Duration // frecuencia de GC
|
|
}
|
|
|
|
// RateLimitResult es el resultado de un check
|
|
type RateLimitResult struct {
|
|
Allowed bool // request permitido
|
|
Remaining int // tokens restantes
|
|
ResetAt time.Time // cuando se rellena el bucket
|
|
RetryAfter float64 // segundos hasta que se pueda reintentar (0 si allowed)
|
|
}
|
|
```
|
|
|
|
### Funciones
|
|
|
|
| Funcion | Purity | Firma (simplificada) |
|
|
|---------|--------|---------------------|
|
|
| `rate_limiter_new` | impure | `(rate float64, burst int) *RateLimiter` |
|
|
| `rate_limiter_check` | impure | `(rl *RateLimiter, key string) RateLimitResult` |
|
|
| `rate_limit_middleware` | impure | `(rl *RateLimiter) Middleware` |
|
|
| `rate_limiter_by_key` | impure | `(rl *RateLimiter, keyFunc func(*http.Request) string) Middleware` |
|
|
| `rate_limiter_cleanup` | impure | `(rl *RateLimiter, maxAge time.Duration, interval time.Duration) func()` |
|
|
| `rate_limit_headers` | pure | `(result RateLimitResult, limit int) http.Header` |
|
|
|
|
### Token bucket
|
|
|
|
Cada key tiene un bucket con `burst` tokens. Se recargan a `rate` tokens/segundo. Un request consume 1 token. Si no quedan tokens, se rechaza con 429.
|
|
|
|
---
|
|
|
|
## Tareas
|
|
|
|
### Fase 1: Core + tipos
|
|
|
|
- [ ] **1.1** Crear tipos `RateLimiter`, `RateLimitConfig`, `RateLimitResult` en `functions/infra/` con `.md` en `types/infra/`
|
|
- [ ] **1.2** `rate_limiter_new` — inicializa `RateLimiter` con rate y burst
|
|
- [ ] **1.3** `rate_limiter_check` — evalua token bucket para una key, retorna `RateLimitResult`
|
|
- [ ] **1.4** `rate_limit_headers` — construye `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`, `Retry-After` a partir de `RateLimitResult`
|
|
- [ ] **1.5** `rate_limiter_cleanup` — goroutine que borra entries sin actividad reciente, retorna `func()` para cancelar
|
|
|
|
### Fase 2: Middlewares + tests
|
|
|
|
- [ ] **2.1** `rate_limit_middleware` — middleware que limita por IP del cliente (extrae de `RemoteAddr` / `X-Forwarded-For`)
|
|
- [ ] **2.2** `rate_limiter_by_key` — middleware configurable con `keyFunc` para limitar por API key, user ID, etc.
|
|
- [ ] **2.3** Tests de cada funcion con `httptest.NewRecorder`
|
|
- [ ] **2.4** `fn index` y verificar con `fn show`
|
|
|
|
---
|
|
|
|
## Ejemplo de uso
|
|
|
|
```go
|
|
// Crear limiter: 10 req/s con burst de 20
|
|
rl := infra.RateLimiterNew(10, 20)
|
|
|
|
// Arrancar cleanup cada 5 minutos, borra entries sin actividad en 10 min
|
|
stopCleanup := infra.RateLimiterCleanup(rl, 10*time.Minute, 5*time.Minute)
|
|
defer stopCleanup()
|
|
|
|
// Componer con otros middlewares de 0009
|
|
middleware := infra.HttpMiddlewareChain(
|
|
infra.HttpCorsMiddleware([]string{"*"}, []string{"GET", "POST"}),
|
|
infra.RateLimitMiddleware(rl), // por IP
|
|
infra.HttpLoggerMiddleware(os.Stdout),
|
|
)
|
|
|
|
mux := infra.HttpRouter(routes)
|
|
infra.HttpServe(":8484", middleware(mux), ctx)
|
|
```
|
|
|
|
```go
|
|
// Rate limit por API key en vez de IP
|
|
keyMiddleware := infra.RateLimiterByKey(rl, func(r *http.Request) string {
|
|
return r.Header.Get("X-API-Key")
|
|
})
|
|
```
|
|
|
|
```go
|
|
// Respuesta 429 automatica del middleware:
|
|
// HTTP/1.1 429 Too Many Requests
|
|
// X-RateLimit-Limit: 10
|
|
// X-RateLimit-Remaining: 0
|
|
// X-RateLimit-Reset: 1713045600
|
|
// Retry-After: 1
|
|
// Content-Type: application/json
|
|
//
|
|
// {"status":429,"code":"rate_limited","message":"too many requests"}
|
|
```
|
|
|
|
## Decisiones de diseno
|
|
|
|
- **In-memory, no Redis:** para el scope del registry (single-process, pocas apps) un `sync.Mutex` + `map` es suficiente y evita una dependencia de infraestructura.
|
|
- **Token bucket sobre sliding window:** permite rafagas legitimas (burst) sin penalizar al cliente por picos puntuales, y es trivial de implementar.
|
|
- **Headers IETF draft:** sigue `draft-ietf-httpapi-ratelimit-headers` (`X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`). Los clientes pueden adaptar su ritmo sin adivinar.
|
|
- **`rate_limit_headers` como funcion pura:** construir headers no requiere I/O, solo formateo. El middleware la usa internamente pero queda disponible para otros usos.
|
|
- **Cleanup explicito:** el GC goroutine se arranca con parametros configurables y se para con la funcion retornada, sin goroutine leaks.
|
|
|
|
## Riesgos
|
|
|
|
- **Memoria con muchas IPs unicas:** Mitigado con `rate_limiter_cleanup` que purga entries inactivas periodicamente. Para APIs con millones de IPs distintas habria que migrar a Redis, pero ese no es el caso del registry.
|
|
- **IP detras de proxy:** `X-Forwarded-For` puede ser spoofed. Para uso interno es aceptable; para exposicion publica real habria que validar el header contra trusted proxies.
|