html_url,issue_url,id,node_id,user,created_at,updated_at,author_association,body,reactions,issue,performed_via_github_app https://github.com/simonw/datasette/issues/1949#issuecomment-1352411327,https://api.github.com/repos/simonw/datasette/issues/1949,1352411327,IC_kwDOBm6k_c5QnCi_,9599,2022-12-15T00:46:27Z,2022-12-15T00:46:27Z,OWNER,"I got this far: ```diff diff --git a/datasette/handle_exception.py b/datasette/handle_exception.py index 8b7e83e3..31d41e00 100644 --- a/datasette/handle_exception.py +++ b/datasette/handle_exception.py @@ -54,7 +54,17 @@ def handle_exception(datasette, request, exception): headers = {} if datasette.cors: add_cors_headers(headers) - if request.path.split(""?"")[0].endswith("".json""): + # Return JSON error under certain conditions + should_return_json = ( + # URL ends in .json + request.path.split(""?"")[0].endswith("".json"") + or + # Hints from incoming request headers + request.headers.get(""content-type"") == ""application/json"" + or ""application/json"" in request.headers.get(""accept"", """") + ) + breakpoint() + if should_return_json: return Response.json(info, status=status, headers=headers) else: template = datasette.jinja_env.select_template(templates) diff --git a/tests/test_api_write.py b/tests/test_api_write.py index f27d143f..982543a6 100644 --- a/tests/test_api_write.py +++ b/tests/test_api_write.py @@ -1140,6 +1140,38 @@ async def test_create_table_permissions( assert data[""errors""] == expected_errors +@pytest.mark.asyncio +@pytest.mark.parametrize( + ""headers,expect_json"", + ( + ({}, False), + ({""Accept"": ""text/html""}, True), + ({""Accept"": ""application/json""}, True), + ({""Content-Type"": ""application/json""}, True), + ({""Accept"": ""application/json, text/plain, */*""}, True), + ({""Content-Type"": ""application/json""}, True), + ({""accept"": ""application/json, text/plain, */*""}, True), + ({""content-type"": ""application/json""}, True), + ), +) +async def test_permission_errors_html_and_json(ds_write, headers, expect_json): + request_headers = {""Authorization"": ""Bearer bad_token""} + request_headers.update(headers) + response = await ds_write.client.post( + ""/data/-/create"", + json={}, + headers=request_headers, + ) + assert response.status_code == 403 + if expect_json: + data = response.json() + assert data[""ok""] is False + assert data[""errors""] == [""Permission denied""] + else: + assert response.headers[""Content-Type""] == ""text/html; charset=utf-8"" + assert ""Permission denied"" in response.text + + @pytest.mark.asyncio @pytest.mark.parametrize( ""input,expected_rows_after"", ``` Then decided I would punt this until the next milestone.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1493471221, https://github.com/simonw/datasette/issues/1949#issuecomment-1352378370,https://api.github.com/repos/simonw/datasette/issues/1949,1352378370,IC_kwDOBm6k_c5Qm6gC,9599,2022-12-15T00:02:08Z,2022-12-15T00:04:54Z,OWNER,"I fixed this issue to help research this further: - https://github.com/simonw/datasette-ripgrep/issues/26 Now this search works: I wish I had this feature! - https://github.com/simonw/datasette-ripgrep/issues/24 Looks like I have both `_error()` and `_errors()` functions in there! ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1493471221, https://github.com/simonw/datasette/issues/1949#issuecomment-1352356356,https://api.github.com/repos/simonw/datasette/issues/1949,1352356356,IC_kwDOBm6k_c5Qm1IE,9599,2022-12-14T23:27:25Z,2022-12-14T23:28:16Z,OWNER,"Also weird: errors returned by that mechanism look like this: ```json { ""ok"": false, ""errors"": [""list of error messages""] } ``` While errors returned by the rest of Datasette look like this: https://latest.datasette.io/fixtures/no_table.json ```json { ""ok"": false, ""error"": ""Table not found: no_table"", ""status"": 404, ""title"": null } ``` Related: - #1875","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1493471221, https://github.com/simonw/datasette/issues/1949#issuecomment-1352354927,https://api.github.com/repos/simonw/datasette/issues/1949,1352354927,IC_kwDOBm6k_c5Qm0xv,9599,2022-12-14T23:25:06Z,2022-12-14T23:25:14Z,OWNER,"Looks like the code I've written for permission checking on `TableCreateView` and friends doesn't use the regular `raise Forbidden` or `raise DatasetteError` mechanisms - it does its own thing here: https://github.com/simonw/datasette/blob/9ad76d279e2c3874ca5070626a25458ce129f126/datasette/views/database.py#L580-L584 Which uses this: https://github.com/simonw/datasette/blob/9ad76d279e2c3874ca5070626a25458ce129f126/datasette/views/base.py#L547-L548 Having two different patterns to return errors is bad, I should fix that.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1493471221, https://github.com/simonw/datasette/issues/1949#issuecomment-1352340518,https://api.github.com/repos/simonw/datasette/issues/1949,1352340518,IC_kwDOBm6k_c5QmxQm,9599,2022-12-14T23:07:01Z,2022-12-14T23:07:01Z,OWNER,"Easiest fix would be to look for `accept: application/json` and/or `content-type: application/json` headers. Not bullet-proof, so people might occasionally make JSON requests and get back an HTML error - but the documentation can tell people that they need to send those headers if they want to reliably get back JSON error messages. I'm happy with this as a solution.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1493471221, https://github.com/simonw/datasette/issues/1949#issuecomment-1352338620,https://api.github.com/repos/simonw/datasette/issues/1949,1352338620,IC_kwDOBm6k_c5Qmwy8,9599,2022-12-14T23:05:17Z,2022-12-14T23:05:17Z,OWNER,"Sniffing for a `{` is a little bit tricky though, as the post body is lazily loaded on request here: https://github.com/simonw/datasette/blob/9ad76d279e2c3874ca5070626a25458ce129f126/datasette/utils/asgi.py#L127-L135","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1493471221, https://github.com/simonw/datasette/issues/1949#issuecomment-1352335503,https://api.github.com/repos/simonw/datasette/issues/1949,1352335503,IC_kwDOBm6k_c5QmwCP,9599,2022-12-14T23:03:28Z,2022-12-14T23:03:28Z,OWNER,"This raises a more complicated issue At some point I'm likely to want to add an HTML interface for creating tables and inserting and updating rows. The obvious URLs for that are the same as for the JSON API: `/db/table/-/insert` and suchlike. Those endpoints are currently POST only - and can return JSON all the time. If they start accepting form POSTs too they'll need to be able to accept form-encoded data and return HTML instead. That's OK - they can detect incoming JSON thanks to the `content-type` header an the fact that the request body starts with `{` - but the `should_return_json` fix described above could intefere with how errors are returned if I'm not careful. I think it can still work though: I'll only set `should_return_json = True` if the endpoint gets a POST with a body starting `{`, or a content-type JSON header.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1493471221, https://github.com/simonw/datasette/issues/1949#issuecomment-1352331314,https://api.github.com/repos/simonw/datasette/issues/1949,1352331314,IC_kwDOBm6k_c5QmvAy,9599,2022-12-14T22:59:36Z,2022-12-14T22:59:36Z,OWNER,I'm going to prototype that up to see what it looks like.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1493471221, https://github.com/simonw/datasette/issues/1949#issuecomment-1352330825,https://api.github.com/repos/simonw/datasette/issues/1949,1352330825,IC_kwDOBm6k_c5Qmu5J,9599,2022-12-14T22:58:51Z,2022-12-14T22:59:27Z,OWNER,"I need a way for those JSON endpoints to communicate back to the `handle_exception` handler that they are returning JSON, so it knows to behave differently. Since it gets the `request` object, one way could be to have view code set `request.should_return_json = True` so that the handler knows to do something different. It's a bit of a cludge though!","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1493471221, https://github.com/simonw/datasette/issues/1949#issuecomment-1352329027,https://api.github.com/repos/simonw/datasette/issues/1949,1352329027,IC_kwDOBm6k_c5QmudD,9599,2022-12-14T22:56:24Z,2022-12-14T22:57:19Z,OWNER,"Most `.json` errors DO return as JSON, thanks to this: https://github.com/simonw/datasette/blob/c094dde3ff2bae030f261e6440d4fb082eb860a9/datasette/handle_exception.py#L19-L24 https://github.com/simonw/datasette/blob/c094dde3ff2bae030f261e6440d4fb082eb860a9/datasette/handle_exception.py#L57-L58 But that code triggers when the URL ends with `.json` - and none of the JSON write API endpoints (things like `/db/-/create` and `/db/table/-/insert`) follow that convention.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1493471221,