Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🐛 Check Content-Type request header before assuming JSON #2118

Merged
merged 12 commits into from Jun 7, 2021
19 changes: 16 additions & 3 deletions fastapi/routing.py
@@ -1,4 +1,5 @@
import asyncio
import email.message
import enum
import inspect
import json
Expand Down Expand Up @@ -36,7 +37,7 @@
)
from pydantic import BaseModel
from pydantic.error_wrappers import ErrorWrapper, ValidationError
from pydantic.fields import ModelField
from pydantic.fields import ModelField, Undefined
from starlette import routing
from starlette.concurrency import run_in_threadpool
from starlette.exceptions import HTTPException
Expand Down Expand Up @@ -174,14 +175,26 @@ def get_request_handler(

async def app(request: Request) -> Response:
try:
body = None
body: Any = None
if body_field:
if is_body_form:
body = await request.form()
else:
body_bytes = await request.body()
if body_bytes:
body = await request.json()
json_body: Any = Undefined
content_type_value = request.headers.get("content-type")
if content_type_value:
message = email.message.Message()
message["content-type"] = content_type_value
if message.get_content_maintype() == "application":
subtype = message.get_content_subtype()
if subtype == "json" or subtype.endswith("+json"):
json_body = await request.json()
if json_body != Undefined:
body = json_body
else:
body = body_bytes
except json.JSONDecodeError as e:
raise RequestValidationError([ErrorWrapper(e, ("body", e.pos))], body=e.doc)
except Exception as e:
Expand Down
84 changes: 75 additions & 9 deletions tests/test_tutorial/test_body/test_tutorial001.py
Expand Up @@ -173,25 +173,91 @@ def test_post_body(path, body, expected_status, expected_response):


def test_post_broken_body():
response = client.post("/items/", data={"name": "Foo", "price": 50.5})
response = client.post(
"/items/",
headers={"content-type": "application/json"},
data="{some broken json}",
)
assert response.status_code == 422, response.text
assert response.json() == {
"detail": [
{
"loc": ["body", 1],
"msg": "Expecting property name enclosed in double quotes: line 1 column 2 (char 1)",
"type": "value_error.jsondecode",
"ctx": {
"colno": 1,
"doc": "name=Foo&price=50.5",
"msg": "Expecting property name enclosed in double quotes",
"doc": "{some broken json}",
"pos": 1,
"lineno": 1,
"msg": "Expecting value",
"pos": 0,
"colno": 2,
},
"loc": ["body", 0],
"msg": "Expecting value: line 1 column 1 (char 0)",
"type": "value_error.jsondecode",
}
]
}


def test_post_form_for_json():
response = client.post("/items/", data={"name": "Foo", "price": 50.5})
assert response.status_code == 422, response.text
assert response.json() == {
"detail": [
{
"loc": ["body"],
"msg": "value is not a valid dict",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be considered a breaking change. Think even my work API has a test similar to this.

"type": "type_error.dict",
}
]
}


def test_explicit_content_type():
response = client.post(
"/items/",
data='{"name": "Foo", "price": 50.5}',
headers={"Content-Type": "application/json"},
)
assert response.status_code == 200, response.text


def test_geo_json():
response = client.post(
"/items/",
data='{"name": "Foo", "price": 50.5}',
headers={"Content-Type": "application/geo+json"},
)
assert response.status_code == 200, response.text


def test_wrong_headers():
data = '{"name": "Foo", "price": 50.5}'
invalid_dict = {
"detail": [
{
"loc": ["body"],
"msg": "value is not a valid dict",
"type": "type_error.dict",
}
]
}

response = client.post("/items/", data=data, headers={"Content-Type": "text/plain"})
assert response.status_code == 422, response.text
assert response.json() == invalid_dict

response = client.post(
"/items/", data=data, headers={"Content-Type": "application/geo+json-seq"}
)
assert response.status_code == 422, response.text
assert response.json() == invalid_dict
response = client.post(
"/items/", data=data, headers={"Content-Type": "application/not-really-json"}
)
assert response.status_code == 422, response.text
assert response.json() == invalid_dict


def test_other_exceptions():
with patch("json.loads", side_effect=Exception):
response = client.post("/items/", json={"test": "test2"})
assert response.status_code == 400, response.text
assert response.json() == {"detail": "There was an error parsing the body"}
Expand Up @@ -25,6 +25,7 @@ def test_gzip_request(compress):
if compress:
data = gzip.compress(data)
headers["Content-Encoding"] = "gzip"
headers["Content-Type"] = "application/json"
response = client.post("/sum", data=data, headers=headers)
assert response.json() == {"sum": n}

Expand Down