A GET for a row page on a table with a composite (multi-column) primary key returns HTTP 500 (sqlite3.ProgrammingError, unbound bind parameter) when the URL has the wrong number of comma-separated primary-key components — most commonly when the separating comma arrives percent-encoded as %2C (proxies and crawlers routinely re-encode reserved characters in paths).
Single-PK tables don't hit this: a non-canonical single-PK URL 404s, and handle_404's %→tilde redirect recovers it to the canonical form. The identical pk-count mismatch is already guarded with BadRequest in _fragment_request_for_row, but the row-page resolution path has no such guard — so it raises an uncaught ProgrammingError (500) before the 404 handler can run.
Reproduce (confirmed on 0.65.2 and 1.0a35)
sqlite3 demo.db "create table t (a text, b text, primary key (a, b)); insert into t values ('x', 'y');"
datasette demo.db
curl -s -o /dev/null -w '%{http_code}\n' 'http://127.0.0.1:8001/demo/t/x,y' # 200
curl -s -o /dev/null -w '%{http_code}\n' 'http://127.0.0.1:8001/demo/t/x%2Cy' # 500 <-- bug
Server log for the %2C request:
ERROR: ... sql = 'select * from t where "a"=:p0 AND "b"=:p1', params = {'p0': 'x%2Cy'}: You did not supply a value for binding parameter :p1.
sqlite3.ProgrammingError: You did not supply a value for binding parameter :p1.
Root cause
The row resolver builds one :pN placeholder per primary-key column but binds only the supplied components. When %2C collapses the comma-separated arity (one value where the table has two PK columns), the trailing :p1 is left unbound → ProgrammingError (a 500) at execute time, before any 404 — so the existing handle_404 recovery never runs.
Suggested fix
Guard the pk-value count against the pk-column count in the row resolver, raising BadRequest, mirroring the existing _fragment_request_for_row check:
if len(pk_values) != len(pks):
raise BadRequest("Invalid primary key value count for this table")
A malformed-arity row URL is a client error, so 400 is the appropriate status — consistent with _fragment_request_for_row and with the ?_sort errors → 400 change (#1950).
A
GETfor a row page on a table with a composite (multi-column) primary key returns HTTP 500 (sqlite3.ProgrammingError, unbound bind parameter) when the URL has the wrong number of comma-separated primary-key components — most commonly when the separating comma arrives percent-encoded as%2C(proxies and crawlers routinely re-encode reserved characters in paths).Single-PK tables don't hit this: a non-canonical single-PK URL 404s, and
handle_404's%→tilde redirect recovers it to the canonical form. The identical pk-count mismatch is already guarded withBadRequestin_fragment_request_for_row, but the row-page resolution path has no such guard — so it raises an uncaughtProgrammingError(500) before the 404 handler can run.Reproduce (confirmed on 0.65.2 and 1.0a35)
Server log for the
%2Crequest:Root cause
The row resolver builds one
:pNplaceholder per primary-key column but binds only the supplied components. When%2Ccollapses the comma-separated arity (one value where the table has two PK columns), the trailing:p1is left unbound →ProgrammingError(a 500) at execute time, before any 404 — so the existinghandle_404recovery never runs.Suggested fix
Guard the pk-value count against the pk-column count in the row resolver, raising
BadRequest, mirroring the existing_fragment_request_for_rowcheck:A malformed-arity row URL is a client error, so 400 is the appropriate status — consistent with
_fragment_request_for_rowand with the?_sorterrors → 400 change (#1950).