A write fuzz over all 113 unguarded write operations (6,465 requests)
produced 36 unhandled 500s. Every one was the same shape: a value
pydantic accepted that Postgres then refused while binding or writing,
surfacing as an unhandled DBAPIError.
Three kinds appeared — a NUL byte in any text field (project, org, tag
and engine names, MCP token names, interest rule queries, the `q` of
all six search grammars, endpoint verify host and path), a string
longer than its column (project label against varchar(50)), and an
integer past bigint (a body-supplied page number, interest rule
weight).
All three are SQLSTATE class 22, the SQL standard's data exception
class, which by definition means the supplied value cannot be
represented in the target column. That is always a client error, so a
handler now maps class 22 to 400 with a message naming the kind of
problem, and re-raises everything else. Class 23 (integrity) is
deliberately left alone: it can indicate a server fault.
The one class 23 failure was a genuine defect rather than bad input.
InterestService.update wrote an explicit null straight through to a
NOT NULL column, so PATCH {"enabled": null} was a 500. It now stores a
null only where the column accepts one, taking that from the column
itself, which keeps `weight: null` clearing the weight while `enabled:
null` means unchanged — the convention every other update service here
already follows.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K6LcH8uUddjJbKrahf34ys