CreaRack-SL

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 MBRespuestas pequeñas (JSON, HTML parcial)
JsonResponse(...)✅ Sí✅ SíAPIs JSON con payloads acotados
StreamingHttpResponse(gen)❌ No❌ No afectadoStreams generados en memoria
FileResponse(filehandle)❌ No❌ No afectadoArchivos 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 racks
  • GET /api/racks/{rack_id}/pdf — PDF individual
  • GET /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

FechaEndpoint afectadoDescripción
2026-05-03GET /api/racks/backup/fullZIP 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 en core/middleware/ o en settings.py → MIDDLEWARE. Esta página documenta el comportamiento observado en producción.


Véase también

  • [[incident—20260503—full-backup-http500]]