Write endpoints parse the body as JSON regardless of Content-Type

The insert, upsert, alter and set-column-type endpoints previously
required Content-Type: application/json while /-/create parsed the body
blind - and insert returned a 500 AttributeError when the header was
missing entirely. The lenient rule is now uniform: the body is always
parsed as JSON and invalid JSON is a 400. This makes curl -d and
requests data=json.dumps(...) invocations work without remembering the
header. Cross-site request forgery remains prevented by the Origin and
Sec-Fetch-Site checks in CrossOriginProtectionMiddleware, which is the
defense the strict content-type requirement was historically standing
in for.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GrHZSypDfMnym1tM5XJAFZ
This commit is contained in:
Claude 2026-07-06 23:20:47 +00:00
commit 87cd695ca3
No known key found for this signature in database
8 changed files with 69 additions and 39 deletions

View file

@ -662,3 +662,58 @@ async def test_debug_endpoints_use_size_and_page_parameters(ds_error_shape, endp
bad_page = await ds_error_shape.client.get(base + "&_page=0", actor={"id": "root"})
data = assert_canonical_error(bad_page, 400)
assert data["errors"] == ["_page must be a positive integer"]
# Write endpoints parse the body as JSON regardless of Content-Type
@pytest.mark.asyncio
async def test_insert_works_without_content_type_header(ds_error_shape):
# Previously a 500 AttributeError
response = await ds_error_shape.client.post(
"/data/docs/-/insert",
content='{"row": {"id": 1, "title": "One"}}',
actor={"id": "root"},
)
assert response.status_code == 201
assert response.json()["rows"][0]["title"] == "One"
@pytest.mark.asyncio
async def test_insert_works_with_form_content_type(ds_error_shape):
# Previously 400 "Invalid content-type, must be application/json"
response = await ds_error_shape.client.post(
"/data/docs/-/insert",
content='{"row": {"id": 2, "title": "Two"}}',
headers={"Content-Type": "application/x-www-form-urlencoded"},
actor={"id": "root"},
)
assert response.status_code == 201
@pytest.mark.asyncio
async def test_insert_form_encoded_body_is_invalid_json(ds_error_shape):
response = await ds_error_shape.client.post(
"/data/docs/-/insert",
content="title=Three",
headers={"Content-Type": "application/x-www-form-urlencoded"},
actor={"id": "root"},
)
data = assert_canonical_error(response, 400)
assert data["errors"][0].startswith("Invalid JSON:")
@pytest.mark.asyncio
async def test_alter_and_set_column_type_ignore_content_type(ds_error_shape):
alter = await ds_error_shape.client.post(
"/data/docs/-/alter",
content='{"operations": [{"op": "add_column", "args": {"name": "extra"}}]}',
actor={"id": "root"},
)
assert alter.status_code == 200, alter.text
sct = await ds_error_shape.client.post(
"/data/docs/-/set-column-type",
content='{"column": "title", "column_type": {"type": "textarea"}}',
actor={"id": "root"},
)
assert sct.status_code == 200, sct.text