# 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: 1. `POST /api/artifacts` with a 250-character `title` (limit 200) → `201`, title stored and returned in full, uncut. 2. `POST /api/artifacts` with a 150-character `source` (limit 80) → `201`, `source` stored in full. Because `source_name` defaults to `source` when omitted (`source_fields()` in `views.py`), this also silently blew past `source_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 ```bash curl -X POST http://127.0.0.1:8001/api/artifacts \ -H "Authorization: Bearer " \ -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.