mirror of https://github.com/tiangolo/fastapi.git
🐛 Check Content-Type request header before assuming JSON (#2118)
Co-authored-by: Patrick Wang <patrickkwang@users.noreply.github.com> Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com>
This commit is contained in:
parent
90120dd6e8
commit
fa7e3c996e
|
|
@ -1,4 +1,5 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import email.message
|
||||||
import enum
|
import enum
|
||||||
import inspect
|
import inspect
|
||||||
import json
|
import json
|
||||||
|
|
@ -36,7 +37,7 @@ from fastapi.utils import (
|
||||||
)
|
)
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from pydantic.error_wrappers import ErrorWrapper, ValidationError
|
from pydantic.error_wrappers import ErrorWrapper, ValidationError
|
||||||
from pydantic.fields import ModelField
|
from pydantic.fields import ModelField, Undefined
|
||||||
from starlette import routing
|
from starlette import routing
|
||||||
from starlette.concurrency import run_in_threadpool
|
from starlette.concurrency import run_in_threadpool
|
||||||
from starlette.exceptions import HTTPException
|
from starlette.exceptions import HTTPException
|
||||||
|
|
@ -174,14 +175,26 @@ def get_request_handler(
|
||||||
|
|
||||||
async def app(request: Request) -> Response:
|
async def app(request: Request) -> Response:
|
||||||
try:
|
try:
|
||||||
body = None
|
body: Any = None
|
||||||
if body_field:
|
if body_field:
|
||||||
if is_body_form:
|
if is_body_form:
|
||||||
body = await request.form()
|
body = await request.form()
|
||||||
else:
|
else:
|
||||||
body_bytes = await request.body()
|
body_bytes = await request.body()
|
||||||
if body_bytes:
|
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:
|
except json.JSONDecodeError as e:
|
||||||
raise RequestValidationError([ErrorWrapper(e, ("body", e.pos))], body=e.doc)
|
raise RequestValidationError([ErrorWrapper(e, ("body", e.pos))], body=e.doc)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
|
||||||
|
|
@ -173,25 +173,91 @@ def test_post_body(path, body, expected_status, expected_response):
|
||||||
|
|
||||||
|
|
||||||
def test_post_broken_body():
|
def test_post_broken_body():
|
||||||
|
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": {
|
||||||
|
"msg": "Expecting property name enclosed in double quotes",
|
||||||
|
"doc": "{some broken json}",
|
||||||
|
"pos": 1,
|
||||||
|
"lineno": 1,
|
||||||
|
"colno": 2,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_post_form_for_json():
|
||||||
response = client.post("/items/", data={"name": "Foo", "price": 50.5})
|
response = client.post("/items/", data={"name": "Foo", "price": 50.5})
|
||||||
assert response.status_code == 422, response.text
|
assert response.status_code == 422, response.text
|
||||||
assert response.json() == {
|
assert response.json() == {
|
||||||
"detail": [
|
"detail": [
|
||||||
{
|
{
|
||||||
"ctx": {
|
"loc": ["body"],
|
||||||
"colno": 1,
|
"msg": "value is not a valid dict",
|
||||||
"doc": "name=Foo&price=50.5",
|
"type": "type_error.dict",
|
||||||
"lineno": 1,
|
|
||||||
"msg": "Expecting value",
|
|
||||||
"pos": 0,
|
|
||||||
},
|
|
||||||
"loc": ["body", 0],
|
|
||||||
"msg": "Expecting value: line 1 column 1 (char 0)",
|
|
||||||
"type": "value_error.jsondecode",
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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):
|
with patch("json.loads", side_effect=Exception):
|
||||||
response = client.post("/items/", json={"test": "test2"})
|
response = client.post("/items/", json={"test": "test2"})
|
||||||
assert response.status_code == 400, response.text
|
assert response.status_code == 400, response.text
|
||||||
assert response.json() == {"detail": "There was an error parsing the body"}
|
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ def test_gzip_request(compress):
|
||||||
if compress:
|
if compress:
|
||||||
data = gzip.compress(data)
|
data = gzip.compress(data)
|
||||||
headers["Content-Encoding"] = "gzip"
|
headers["Content-Encoding"] = "gzip"
|
||||||
|
headers["Content-Type"] = "application/json"
|
||||||
response = client.post("/sum", data=data, headers=headers)
|
response = client.post("/sum", data=data, headers=headers)
|
||||||
assert response.json() == {"sum": n}
|
assert response.json() == {"sum": n}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue