pythonasyncioapiasyncfastapiframeworkjsonjson-schemaopenapiopenapi3pydanticpython-typespython3redocreststarletteswaggerswagger-uiuvicornweb
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
112 lines
2.9 KiB
112 lines
2.9 KiB
"""
|
|
Test that ClientDisconnect during request body reading is not
|
|
misreported as HTTP 400 "There was an error parsing the body".
|
|
|
|
Ref: https://github.com/fastapi/fastapi/issues/XXXXX
|
|
"""
|
|
|
|
import pytest
|
|
from fastapi import Body, FastAPI, Form
|
|
from fastapi.testclient import TestClient
|
|
from starlette.requests import ClientDisconnect
|
|
|
|
pytestmark = pytest.mark.anyio
|
|
|
|
app = FastAPI()
|
|
|
|
|
|
@app.post("/json")
|
|
async def json_endpoint(data: dict = Body(...)):
|
|
return data
|
|
|
|
|
|
@app.post("/form")
|
|
async def form_endpoint(field: str = Form(...)):
|
|
return {"field": field}
|
|
|
|
|
|
def _make_scope(path: str) -> dict:
|
|
return {
|
|
"type": "http",
|
|
"asgi": {"version": "3.0"},
|
|
"http_version": "1.1",
|
|
"method": "POST",
|
|
"scheme": "http",
|
|
"path": path,
|
|
"raw_path": path.encode(),
|
|
"query_string": b"",
|
|
"headers": [(b"content-type", b"application/json")],
|
|
"client": ("127.0.0.1", 12345),
|
|
"server": ("127.0.0.1", 8000),
|
|
}
|
|
|
|
|
|
async def test_client_disconnect_json_body():
|
|
"""ClientDisconnect during JSON body reading must not become HTTP 400."""
|
|
messages = [
|
|
{"type": "http.request", "body": b'{"incomplete": ', "more_body": True},
|
|
{"type": "http.disconnect"},
|
|
]
|
|
|
|
async def receive():
|
|
return messages.pop(0)
|
|
|
|
sent: list[dict] = []
|
|
|
|
async def send(message):
|
|
sent.append(message)
|
|
|
|
scope = _make_scope("/json")
|
|
with pytest.raises(ClientDisconnect):
|
|
await app(scope, receive, send)
|
|
|
|
# Ensure no HTTP 400 response was sent
|
|
for msg in sent:
|
|
if msg.get("type") == "http.response.start":
|
|
assert msg.get("status") != 400, (
|
|
"ClientDisconnect should not produce HTTP 400"
|
|
)
|
|
|
|
|
|
async def test_client_disconnect_form_body():
|
|
"""ClientDisconnect during form body reading must not become HTTP 400."""
|
|
messages = [
|
|
{
|
|
"type": "http.request",
|
|
"body": b"field=partial",
|
|
"more_body": True,
|
|
},
|
|
{"type": "http.disconnect"},
|
|
]
|
|
|
|
async def receive():
|
|
return messages.pop(0)
|
|
|
|
sent: list[dict] = []
|
|
|
|
async def send(message):
|
|
sent.append(message)
|
|
|
|
scope = _make_scope("/form")
|
|
scope["headers"] = [
|
|
(b"content-type", b"application/x-www-form-urlencoded"),
|
|
]
|
|
with pytest.raises(ClientDisconnect):
|
|
await app(scope, receive, send)
|
|
|
|
for msg in sent:
|
|
if msg.get("type") == "http.response.start":
|
|
assert msg.get("status") != 400, (
|
|
"ClientDisconnect should not produce HTTP 400"
|
|
)
|
|
|
|
|
|
def test_body_parse_error_still_returns_400():
|
|
"""Generic body parse errors must still produce HTTP 400."""
|
|
client = TestClient(app, raise_server_exceptions=False)
|
|
response = client.post(
|
|
"/json",
|
|
content=b"not valid json",
|
|
headers={"content-type": "application/json"},
|
|
)
|
|
assert response.status_code == 422
|
|
|