Return 400 not 500 for wrong-arity composite-PK row URLs (#2815)

Fixes #2811

Co-authored-by: Zain Dana Harper <zain@aurora-framework.dev>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Zain Dana Harper 2026-07-07 14:19:08 -07:00 committed by GitHub
commit 211e70d4e1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 39 additions and 0 deletions

View file

@ -134,6 +134,7 @@ from .utils import (
from .tokens import TokenInvalid
from .utils.asgi import (
AsgiLifespan,
BadRequest,
Forbidden,
NotFound,
DatabaseNotFound,
@ -2820,6 +2821,10 @@ class Datasette:
db, table_name, _ = await self.resolve_table(request)
pk_values = urlsafe_components(request.url_vars["pks"])
sql, params, pks = await row_sql_params_pks(db, table_name, pk_values)
if len(pk_values) != len(pks):
raise BadRequest(
"URL row identifier does not match the primary key for this table"
)
results = await db.execute(sql, params, truncate=True)
row = results.first()
if row is None:

View file

@ -369,6 +369,40 @@ async def test_row(ds_client):
assert response.json()["rows"] == [{"id": 1, "content": "hello"}]
@pytest.mark.asyncio
@pytest.mark.parametrize("suffix", ("", ".json"))
@pytest.mark.parametrize(
"row_path",
(
"a", # too few components for a two-column primary key
"a,b,c", # too many components for a two-column primary key
),
)
async def test_row_pk_arity_mismatch_returns_400(ds_client, row_path, suffix):
# A row URL with the wrong number of comma-separated primary key
# components used to raise an uncaught sqlite3.ProgrammingError (HTTP 500)
# because the SQL had one bind placeholder per PK column but params were
# only bound for the supplied components. It should be a 400 instead,
# mirroring the existing guard in datasette/views/table.py.
response = await ds_client.get(
"/fixtures/compound_primary_key/{}{}".format(row_path, suffix)
)
assert response.status_code == 400
if suffix == ".json":
assert response.json()["ok"] is False
assert response.json()["status"] == 400
@pytest.mark.asyncio
async def test_row_compound_pk_correct_arity(ds_client):
# The valid two-component URL still resolves the row.
response = await ds_client.get(
"/fixtures/compound_primary_key/a,b.json?_shape=objects"
)
assert response.status_code == 200
assert response.json()["rows"] == [{"pk1": "a", "pk2": "b", "content": "c"}]
@pytest.mark.asyncio
async def test_row_strange_table_name(ds_client):
response = await ds_client.get(