Volver a la wiki

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

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:

Archivos modificados:


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:


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

Subir