# 005 — `PATCH /api/artifacts/{id}` silently mishandles invalid/immutable input instead of rejecting it **Status:** Fixed ## Description `ArtifactDetailView.patch` (`artifacts/views.py`) applies whatever recognized keys are present in the request body without validating their type or rejecting fields it can't act on. Two confirmed effects: **1. A wrong-typed `tags` value silently wipes existing tags (data loss).** `normalize_tags()` returns `[]` for anything that isn't a `str` or `list`. `ArtifactDetailView.patch` calls it unconditionally whenever `"tags"` is a key in the request body, with no type check first. Reproduced: ```bash # Artifact has tags ["important", "keep-me"] curl -X PATCH .../api/artifacts/a74de286 -d '{"tags": 12345}' ... # -> 200, tags silently become [] ``` An agent that sends a malformed `tags` value (e.g. a stray int from a bad template substitution) gets a `200` and loses the artifact's existing tags with no indication anything went wrong. **2. Immutable fields (`content`, `source`, `kind`, `day`) are silently no-ops instead of rejected.** `docs/api.md`'s "Not editable yet" table lists these fields and why, but doesn't say what happens if a client sends them anyway. In practice: `PATCH` with `{"content": "...", "source": "...", "kind": "...", "day": "..."}` returns `200` with the artifact **unchanged** — the fields are silently dropped, not rejected. An agent attempting to fix a typo in `content` via `PATCH` (a very natural thing to try, since `PATCH` is the only mutate-after-create endpoint) gets a success response that looks like it worked but did nothing. Both are instances of the same root cause: `patch()` doesn't validate before applying, unlike `POST`'s `validate_source_fields()`. ## Suggested Fix - Type-check `tags` (must be `str` or `list`) before calling `normalize_tags`; return `400` on other types instead of silently emptying it. Same principle likely applies to `metadata` (already checked) and could apply to `source_prompt`/`source_name`/`source_id` (currently accept anything via implicit str coercion via Django, but worth a pass). - Either make sending an immutable field (`content`/`source`/`kind`/`day`) in a `PATCH` body a `400` ("field is not editable"), or explicitly document that unknown/immutable keys are ignored — right now the behavior is undocumented and easy to mistake for success. ## Resolution Went with explicit rejection for both, over silent-ignore-and-document: - `tags` is now type-checked (`str` or `list`) before `normalize_tags` runs; any other type returns `400` and leaves existing tags untouched. - Sending `content`, `source`, `kind`, or `day` in a `PATCH` body now returns `400` naming the offending field(s), via a new `IMMUTABLE_PATCH_FIELDS` check at the top of `ArtifactDetailView.patch`, before any other processing — chosen over documenting the no-op because a `200` that does nothing is easy to mistake for success. `docs/api.md`'s "Not editable" table now says so explicitly instead of just listing reasons. Tests added: wrong-typed `tags` is rejected and leaves existing tags intact, and each of the four immutable fields is rejected individually.