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:
FileResponseutilizastreaming_content(generador por chunks) en lugar decontent.PayloadBudgetMiddlewarecompruebahasattr(response, "content")— en unStreamingHttpResponse/FileResponseeste 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 bloquefinallymantiene la inode viva mientrasFileResponsetenga el filehandle abierto; Django cierra el handle al terminar de servir, el SO libera el inodo.
Archivos modificados:
racks/api/export/backup.py— funciónbackup_full- Import cambiado:
HttpResponse→FileResponse(dedjango.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
-
PayloadBudgetMiddlewarees incompatible con respuestas binarias grandes — cualquier endpoint que devuelva archivos de más de unos pocos KB debe usarFileResponse(streaming), nuncaHttpResponsecon el contenido completo en memoria. Ver [[concept—core—payload-budget-middleware]] para la lista de endpoints de exportación afectados. -
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. -
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]]