Fix #015 (nested-JSON 500) and #016 (ragged CSV data loss)

#015 — deeply nested JSON raised RecursionError (a RuntimeError, not
ValueError) and became an unhandled 500 on two paths:
- json-kind content validation now catches RecursionError -> 400.
- Added a custom DRF EXCEPTION_HANDLER
  (artifacts/exception_handlers.py, wired via REST_FRAMEWORK) mapping
  otherwise-unhandled RecursionError -> 400, covering the parser-level
  crash on deeply nested request bodies (metadata etc.) too.

#016 — wrap_csv_viewer handed Grid.js rows without reconciling width to the
header, so over-long rows silently dropped cells. Now normalizes every row
to the max width: long rows widen the columns (auto-named "Column N"),
short rows pad with "". No cell is dropped.

Both verified live; 4 regression tests added. 106 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
peter 2026-09-03 10:57:07 +01:00
parent 5c53643349
commit 5f872b4b32
6 changed files with 110 additions and 7 deletions

View file

@ -866,8 +866,14 @@ def wrap_csv_viewer(content, title, request):
gridjs_js = request.build_absolute_uri("/static/vendor/gridjs/gridjs.umd.js")
gridjs_css = request.build_absolute_uri("/static/vendor/gridjs/mermaid.min.css")
rows = [row for row in csv.reader(io.StringIO(content)) if row]
columns = rows[0] if rows else []
data = rows[1:] if len(rows) > 1 else []
# Normalize ragged rows to a single width so no cell is ever dropped by
# Grid.js (which keys body cells by column). Widen the header for rows
# longer than it (auto-naming the extra columns), and pad short rows with
# empty strings. Issue #016.
width = max((len(row) for row in rows), default=0)
header = rows[0] if rows else []
columns = [header[i] if i < len(header) else f"Column {i + 1}" for i in range(width)]
data = [[row[i] if i < len(row) else "" for i in range(width)] for row in rows[1:]]
table_json = escape_script_body(json.dumps({"columns": columns, "data": data}))
return f"""<!doctype html>
<html>
@ -1036,7 +1042,9 @@ def validate_content_shape(kind, content):
if kind == Artifact.JSON:
try:
json.loads(content)
except (TypeError, ValueError) as exc:
# RecursionError (deeply nested JSON) is a RuntimeError, not a
# ValueError, so it must be caught explicitly or it becomes a 500.
except (TypeError, ValueError, RecursionError) as exc:
return Response({"detail": f"content must be valid JSON: {exc}"}, status=400)
return None