These accepted any JSON type on both POST and PATCH, silently mangled
into Python repr() strings at save time (a dict became the literal text
"{'a': 1}", unparseable as JSON) - and the immediate response echoed the
raw pre-save value, while a later GET returned the mangled one, an
internally inconsistent API.
Added validate_string_fields(), rejecting non-string values with 400
before they reach save(), matching #005's approach for tags. Verified live
and with new tests.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
3.2 KiB
008 — source_name/source_id/source_prompt accept any JSON type and get silently mangled into repr() strings
Status: Fixed
Follow-up to the #007 handover, which specifically asked to probe other
wrong-typed PATCH fields beyond tags now that #005 is fixed.
Description
Unlike tags, metadata, and tag_colors (all now type-checked, tags
since #005), source_name, source_id, and source_prompt accept any
JSON type — object, array, number — on both POST (source_fields() in
views.py) and PATCH (ArtifactDetailView.patch). Nothing validates
they're strings before assignment.
source_name/source_id are CharField, source_prompt is TextField.
Django's Field.get_prep_value() silently coerces non-string input via
plain str(value) at save time — which for a dict/list produces
Python repr() syntax, not JSON: {"a": 1} becomes the string
"{'a': 1}" (single quotes, unparseable as JSON). This is lossy and
irreversible — there's no way to recover the original structured value.
Worse: the API response is internally inconsistent. Immediately after
a POST/PATCH with a non-string value, the response echoes back the
raw, pre-save Python object (still a real JSON object/array in the
response body) — but a subsequent GET of the same artifact returns the
mangled repr() string instead. Same request, two different apparent
types for the same field depending only on when you read it.
Reproduced:
# PATCH source_name with an int, source_id with a list, source_prompt with a dict
curl -X PATCH .../api/artifacts/a74de286 -d '{"source_name": 12345}' ...
# -> 200, response shows "source_name": 12345 (int)
curl .../api/artifacts/a74de286 # fresh GET, session auth
# -> "source_name": "12345" (string)
# Actual DB values (confirmed via shell):
# source_name = '12345'
# source_id = "['a', 'b']" <- repr() of the list, not JSON
# source_prompt = "{'nested': 'object'}" <- repr() of the dict, not JSON
Also reproduced identically on POST (create), not just PATCH — a
freshly created artifact's immediate response shows
"source_name": {"a": 1} (real object) even though what's stored (and
what any later GET returns) is the mangled string form.
Suggested Fix
Add a type check for source_name, source_id, and source_prompt
(must be str or absent/null) alongside the existing tags/metadata/
tag_colors checks in validate_source_fields (create path) and
ArtifactDetailView.patch (update path) — return 400 naming the field,
consistent with #005's approach for tags. This also removes the
response/DB inconsistency, since invalid types would never reach save()
in the first place.
Resolution
Added validate_string_fields(data, fields) in artifacts/views.py,
rejecting any non-None, non-str value with 400. Called from
validate_source_fields (covers POST, both text and binary paths) and
directly in ArtifactDetailView.patch for source_name, source_id,
source_prompt — matching #005's approach for tags. Invalid values never
reach save(), so the response/DB inconsistency the issue described can no
longer occur. Verified live against the running dev instance and with new
tests in tests/api/test_artifacts.py.