- #006 (high, data loss): make_room_for() no longer deletes unkept artifacts on a doomed request - checks whether eviction can possibly succeed before deleting anything. Also fixed a related latent bug found while testing: the function returned evicted Artifact objects after calling .delete() on them, so their .id was always None (Django clears pk on delete); now returns ids captured beforehand. - #003: added server-side field-length validation (source/title/ source_name/source_id/theme) on POST and PATCH, matching the model's own max_length limits, which were previously unenforced on the ingest path entirely. - #004: ported daybook_post.py's content-shape validation (html_fragment/html_full/deck/markdown) to the server, closing the gap for any client that doesn't go through the helper script. - #005: PATCH now rejects wrong-typed `tags` (previously silently wiped existing tags) and rejects immutable fields (content/source/kind/day) with 400 instead of silently no-op'ing them. - #002: deleted the misleading skills/daybook-posting/.env.example (a zero-dependency script that never reads .env files) and clarified SKILL.md/docs/llm_posting_helper.md that these must be real process/shell env vars. Adds tests/unit/ for storage-layer tests. 46 tests passing (was 25). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
3.2 KiB
003 — CharField(max_length=...) limits are silently unenforced on ingest
Status: Fixed
Description
artifacts/models.py declares length caps on several Artifact fields —
source (80), title (200), source_name (120), source_id (120),
theme (40) — but nothing on the ingest path enforces them. views.py's
create_text_artifact/create_binary_artifact call
Artifact.objects.create(...) directly, which does not run Django's
full_clean()/validators. SQLite (the DB in both dev and production per
docs/deployment.md line 197) also doesn't enforce VARCHAR(n) length as
a hard constraint on INSERT, so oversized values are written and read
back verbatim with no error at any layer.
Confirmed against the local test account:
POST /api/artifactswith a 250-charactertitle(limit 200) →201, title stored and returned in full, uncut.POST /api/artifactswith a 150-charactersource(limit 80) →201,sourcestored in full. Becausesource_namedefaults tosourcewhen omitted (source_fields()inviews.py), this also silently blew pastsource_name's own 120-char limit in the same request.
For contrast, other validation on the same endpoint works correctly and
returns a clean 400 — e.g. an invalid tag_colors hex value
({"tag_colors": {"urgent": "notacolor"}} → 400 with a helpful
message). So the endpoint clearly has a validation convention; these
five fields just aren't part of it.
Practically: a title long enough could break feed-row layout in the UI;
source feeds directly into the on-disk storage path
(artifacts_store/{source}/{day}/...) and the sidebar/label system, so
an extreme value there has a larger blast radius than just cosmetics.
Reproduction
curl -X POST http://127.0.0.1:8001/api/artifacts \
-H "Authorization: Bearer <test-account-token>" \
-H "Content-Type: application/json" \
-d "{\"source\":\"edge-case-test\",\"title\":\"$(python3 -c 'print("A"*250)')\",\"kind\":\"markdown\",\"content\":\"x\"}"
# -> 201, title returned uncut at 250 chars
Suggested Fix
Add explicit length checks alongside the existing required-field/kind
checks in ArtifactListCreateView.post and ArtifactDetailView.patch
(and the two create_*_artifact helpers), returning 400 with a message
naming the field and its limit — consistent with how tag_colors and
source_type are already handled. A shared validate_field_lengths(data)
helper (mirroring validate_source_fields) covering source, title,
source_name, source_id, theme would keep it in one place.
Resolution
Added validate_field_lengths(data, fields) in artifacts/views.py, called
from validate_source_fields (covers source, title, source_name,
source_id, theme on both text and binary POST, since both paths already
routed through that function) and directly in ArtifactDetailView.patch
(covers title, source_name, source_id — the only length-limited fields
PATCH can touch). Returns 400 naming the field and its limit, matching
the existing tag_colors/source_type convention. Tests added in
tests/api/test_artifacts.py covering oversized title/source on create,
an exact-limit boundary case, and oversized title on patch.