This commit is contained in:
XU 2026-10-07 16:43:25 +08:00 • committed by GitHub
commit 60a5413dd2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 52 additions and 9 deletions

View file

@ -55,14 +55,19 @@ def handle_exception(datasette, request, exception):
headers = {}
if datasette.cors:
add_cors_headers(headers)
if request.path.split("?")[0].endswith(".json"):
body = dict(info)
body.update(error_body(plain_message or message, status))
return Response.json(body, status=status, headers=headers)
if request.path.split("?")[0].endswith(".csv"):
path = request.path.split("?")[0]
if path.endswith(".csv"):
return Response.text(
plain_message or message, status=status, headers=headers
)
if (
path.endswith(".json")
or "application/json" in (request.headers.get("accept") or "")
or request.headers.get("content-type") == "application/json"
):
body = dict(info)
body.update(error_body(plain_message or message, status))
return Response.json(body, status=status, headers=headers)
info.update(
{
"ok": False,

View file

@ -129,10 +129,11 @@ Some endpoints add extra context keys. For example, a SQL error from a
``"rows"`` and ``"truncated"`` keys of the response it was unable to
produce.
Permission errors use the same format: a request that fails a permission
check receives a ``403`` with this JSON error body when the URL ends in
``.json`` or the request sends an ``Accept: application/json`` or
``Content-Type: application/json`` header.
Errors handled by Datasette's exception and permission handlers use this
format when the URL ends in ``.json`` or the request sends an
``Accept: application/json`` or ``Content-Type: application/json`` header.
Without one of these signals, an error on an HTML page is rendered as HTML.
Errors on explicit ``.csv`` URLs remain plain text, regardless of headers.
.. _json_api_custom_sql:

View file

@ -63,6 +63,43 @@ async def test_not_found_error_shape(ds_client):
assert_canonical_error(response, 404)
@pytest.mark.asyncio
@pytest.mark.parametrize(
"headers",
(
{"Accept": "application/json"},
{"Content-Type": "application/json"},
),
)
async def test_not_found_error_shape_with_json_request_headers(ds_client, headers):
response = await ds_client.get("/fixtures/no_such_table", headers=headers)
assert_canonical_error(response, 404)
@pytest.mark.asyncio
async def test_not_found_without_json_request_headers_returns_html(ds_client):
response = await ds_client.get("/fixtures/no_such_table")
assert response.status_code == 404
assert response.headers["content-type"].startswith("text/html")
@pytest.mark.asyncio
async def test_csv_error_keeps_text_format_with_json_accept_header(ds_client):
response = await ds_client.get(
"/fixtures/no_such_table.csv", headers={"Accept": "application/json"}
)
assert response.status_code == 404
assert response.headers["content-type"].startswith("text/plain")
@pytest.mark.asyncio
async def test_write_api_missing_database_error_shape(ds_client):
response = await ds_client.post(
"/no_such/-/create", json={}, headers={"Content-Type": "application/json"}
)
assert_canonical_error(response, 404)
@pytest.mark.asyncio
async def test_datasette_error_with_title_omits_title_key(ds_client):
# DatasetteError(title="Invalid SQL") previously leaked a "title" key