CreaRack-SL

Incident: Full Backup HTTP 500 + Trash modal mostraba comentario interno (v1.0.69+)

Incident: Full Backup HTTP 500 + Trash modal comentario interno

Fecha detectada: 2026-05-03 (sesión 48 cont.) Versión afectada: ≤ v1.0.68+ Versión corregida: v1.0.69+ Autor fix: @Esquembri Commit: 8074d3cbb8e9147b83bb9cef8b4b1acff1bedf7d Severidad: Alta (Bug 1) / Baja (Bug 2)


Resumen

Dos bugs encontrados en producción simultáneamente. El más grave inutilizaba la descarga de Full System Backup para cualquier organización con datos reales. El segundo exponía documentación interna del template al usuario final en el modal “Trash” del dashboard.


Bug 1 — GET /api/racks/backup/full → HTTP 500

Síntoma

El endpoint GET /api/racks/backup/full respondía HTTP 500 para cualquier organización con un volumen de datos habitual en producción (~250 MB de ZIP con todos los modelos + uploads/).

Causa raíz

PayloadBudgetMiddleware inspecciona len(response.content) en cada respuesta saliente y bloquea con HTTP 500 toda respuesta que supere el hard_limit de 5 MB. La implementación original de backup_full usaba HttpResponse(zip_data, ...), que lee el ZIP entero en memoria antes de enviarlo:

# ANTES (roto)
with open(zip_path, "rb") as f:
    zip_data = f.read()          # 250 MB cargados en RAM del worker

response = HttpResponse(zip_data, content_type="application/zip")
response["Content-Disposition"] = f'attachment; filename="..."'
return response

Al leer response.content, el middleware detectaba un payload de ~250 MB > 5 MB y devolvía 500 antes de que el cliente recibiera nada.

Impacto

  • Todas las organizaciones con >50 racks y/o uploads generaban un ZIP > 5 MB → backup 100% inutilizable en producción.
  • No había workaround desde el frontend; el endpoint fallaba siempre de forma silenciosa desde el punto de vista del usuario.
  • El container web cargaba hasta 250 MB por intento fallido — presión innecesaria sobre la RAM del worker.

Fix aplicado

Reemplazar HttpResponse por FileResponse (streaming):

# DESPUÉS (correcto)
filename = f"crearack_backup_{org.name}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.zip"
response = FileResponse(
    open(zip_path, "rb"),        # noqa: SIM115 — FileResponse cierra al servir
    as_attachment=True,
    filename=filename,
    content_type="application/zip",
)
return response

Por qué funciona:

  • FileResponse utiliza streaming_content (generador por chunks) en lugar de content.
  • PayloadBudgetMiddleware comprueba hasattr(response, "content") — en un StreamingHttpResponse/FileResponse este atributo no existe → el guard no se dispara.
  • El archivo se sirve en chunks sin cargar los 250 MB en RAM.
  • En POSIX, el os.unlink(zip_path) del bloque finally mantiene la inode viva mientras FileResponse tenga el filehandle abierto; Django cierra el handle al terminar de servir, el SO libera el inodo.

Archivos modificados:

  • racks/api/export/backup.py — función backup_full
  • Import cambiado: HttpResponse → FileResponse (de django.http)

Bug 2 — Modal “Trash” mostraba comentario interno del template

Síntoma

El modal “Trash” del dashboard mostraba un bloque de texto con la documentación interna del partial _item.html directamente visible para el usuario (nombres de parámetros, URLs de ejemplo, descripción del componente).

Causa raíz

El comentario multilínea en templates/htmx/trash/_item.html estaba escrito con la sintaxis {# ... #}, que en Django Template Language solo funciona en una línea. En bloques multilínea, DTL renderiza el contenido literal en lugar de eliminarlo.

<!-- ANTES (roto) -->
{# Generic trash item row. Params:
   - item: object with .id, .name, .deleted_at
   - item_type: slug used for DOM id + hx-target ("blueprint", "rack")
   ...
#}

Esta sintaxis es válida en Jinja2 para comentarios multilínea, pero DTL no la interpreta igual.

Fix aplicado

Usar {% comment %}...{% endcomment %}, la sintaxis oficial DTL para comentarios multilínea:

<!-- DESPUÉS (correcto) -->
{% comment %}
Generic trash item row. Params:
- item: object with .id, .name, .deleted_at
- item_type: slug used for DOM id + hx-target ("blueprint", "rack")
...
{% endcomment %}

Archivos modificados:

  • templates/htmx/trash/_item.html

Lecciones aprendidas

  1. PayloadBudgetMiddleware es incompatible con respuestas binarias grandes — cualquier endpoint que devuelva archivos de más de unos pocos KB debe usar FileResponse (streaming), nunca HttpResponse con el contenido completo en memoria. Ver [[concept—core—payload-budget-middleware]] para la lista de endpoints de exportación afectados.

  2. DTL ≠ Jinja2 en comentarios multilínea — {# ... #} solo comenta una línea en DTL. Para comentarios de bloque usar siempre {% comment %}...{% endcomment %}. Si el proyecto mezcla templates DTL y Jinja2, es fácil importar inconscientemente sintaxis del otro motor.

  3. Cubrir endpoints de exportación con tests de tamaño real — el bug del backup solo era reproducible con datos de producción; un fixture de test con 1 rack nunca lo hubiera detectado.


Véase también

  • [[concept—core—payload-budget-middleware]]