#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:
parent
5c53643349
commit
5f872b4b32
6 changed files with 110 additions and 7 deletions
24
artifacts/exception_handlers.py
Normal file
24
artifacts/exception_handlers.py
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
from rest_framework.response import Response
|
||||
from rest_framework.views import exception_handler
|
||||
|
||||
|
||||
def daybook_exception_handler(exc, context):
|
||||
"""DRF exception handler that additionally maps pathological-input crashes
|
||||
to a clean 400 instead of an unhandled 500.
|
||||
|
||||
Deeply nested JSON in the request body raises RecursionError inside DRF's
|
||||
JSONParser (before any view validation runs). RecursionError subclasses
|
||||
RuntimeError, not ValueError, so DRF's default handler doesn't map it and
|
||||
it surfaces as a 500. Anything the default handler already knows about is
|
||||
left to it; anything still unhandled that isn't a RecursionError is
|
||||
returned as None so Django produces its normal 500.
|
||||
"""
|
||||
response = exception_handler(exc, context)
|
||||
if response is not None:
|
||||
return response
|
||||
if isinstance(exc, RecursionError):
|
||||
return Response(
|
||||
{"detail": "Request payload is nested too deeply to parse."},
|
||||
status=400,
|
||||
)
|
||||
return None
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue