PayloadBudgetMiddleware — guard de tamaño de respuesta HTTP
PayloadBudgetMiddleware — guard de tamaño de respuesta HTTP
¿Qué es?
PayloadBudgetMiddleware es un middleware Django de CreaRack Pro que inspecciona cada respuesta HTTP saliente y aborta con HTTP 500 aquellas cuyo content supere el hard_limit configurado (5 MB en producción).
Su propósito original es proteger al worker web de respuestas accidentalmente grandes (p.ej. volcados JSON sin paginación, exports mal acotados) que podrían agotar la RAM del container.
Mecanismo de funcionamiento
# Lógica conceptual (pseudocódigo)
class PayloadBudgetMiddleware:
hard_limit = 5 * 1024 * 1024 # 5 MB
def process_response(self, request, response):
if hasattr(response, "content"): # solo responses no-streaming
if len(response.content) > self.hard_limit:
raise Http500("Response exceeds payload budget")
return response
Punto clave: el guard usa hasattr(response, "content") como discriminador. Los StreamingHttpResponse y FileResponse de Django exponen streaming_content (un generador), no content — por tanto el middleware no los mide y los deja pasar sin restricción.
Tabla de compatibilidad
| Tipo de respuesta Django | ¿Tiene .content? | ¿Afectado por el guard? | Usar cuando… |
|---|---|---|---|
HttpResponse(data) | ✅ Sí | ✅ Sí — bloqueado si > 5 MB | Respuestas pequeñas (JSON, HTML parcial) |
JsonResponse(...) | ✅ Sí | ✅ Sí | APIs JSON con payloads acotados |
StreamingHttpResponse(gen) | ❌ No | ❌ No afectado | Streams generados en memoria |
FileResponse(filehandle) | ❌ No | ❌ No afectado | Archivos desde disco — obligatorio para exports |
Regla de uso: cuándo usar FileResponse
Todo endpoint que pueda devolver más de ~1 MB debe usar FileResponse, no HttpResponse. Esto incluye, como mínimo:
GET /api/racks/backup/full— ZIP completo (~250 MB en prod) ✅ ya corregido en v1.0.69+GET /api/racks/export/csv— CSV de todos los racksGET /api/racks/{rack_id}/pdf— PDF individualGET /api/logs/export— CSV de logs del sistema- Cualquier endpoint futuro que genere archivos ZIP, PDF, XLSX o binarios
Patrón correcto para exports desde archivo temporal
from django.http import FileResponse
import os, tempfile
def my_export_endpoint(request):
# 1. Generar el archivo en disco (ruta temporal)
tmp = tempfile.NamedTemporaryFile(suffix=".zip", delete=False)
zip_path = tmp.name
tmp.close()
try:
# 2. Construir el ZIP / PDF / CSV en zip_path
_build_export(zip_path, ...)
# 3. Devolver como FileResponse — el guard no mide streaming_content
response = FileResponse(
open(zip_path, "rb"), # noqa: SIM115 — FileResponse cierra al servir
as_attachment=True,
filename="export.zip",
content_type="application/zip",
)
return response
finally:
# En POSIX: unlink antes de que FileResponse cierre el filehandle
# mantiene la inode viva hasta que Django termina de servir.
# El SO libera el inodo cuando el último filehandle se cierra.
os.unlink(zip_path)
⚠️ No hacer
zip_data = f.read()+HttpResponse(zip_data)— eso carga el archivo completo en RAM y activa el guard si supera 5 MB.
Historial de incidentes relacionados
| Fecha | Endpoint afectado | Descripción |
|---|---|---|
| 2026-05-03 | GET /api/racks/backup/full | ZIP de producción (~250 MB) bloqueado → HTTP 500. Fix: FileResponse. Ver [[incident—20260503—full-backup-http500]] |
Configuración
La ubicación exacta del middleware y los parámetros configurables (
hard_limit,soft_limit, exclusiones por path) deben buscarse encore/middleware/o ensettings.py→MIDDLEWARE. Esta página documenta el comportamiento observado en producción.
Véase también
- [[incident—20260503—full-backup-http500]]