Skip to content

Commit fa7e3c9

Browse files
🐛 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>
1 parent 90120dd commit fa7e3c9

File tree

3 files changed

+92
-12
lines changed

3 files changed

+92
-12
lines changed

fastapi/routing.py

+16-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import asyncio
2+
import email.message
23
import enum
34
import inspect
45
import json
@@ -36,7 +37,7 @@
3637
)
3738
from pydantic import BaseModel
3839
from pydantic.error_wrappers import ErrorWrapper, ValidationError
39-
from pydantic.fields import ModelField
40+
from pydantic.fields import ModelField, Undefined
4041
from starlette import routing
4142
from starlette.concurrency import run_in_threadpool
4243
from starlette.exceptions import HTTPException
@@ -174,14 +175,26 @@ def get_request_handler(
174175

175176
async def app(request: Request) -> Response:
176177
try:
177-
body = None
178+
body: Any = None
178179
if body_field:
179180
if is_body_form:
180181
body = await request.form()
181182
else:
182183
body_bytes = await request.body()
183184
if body_bytes:
184-
body = await request.json()
185+
json_body: Any = Undefined
186+
content_type_value = request.headers.get("content-type")
187+
if content_type_value:
188+
message = email.message.Message()
189+
message["content-type"] = content_type_value
190+
if message.get_content_maintype() == "application":
191+
subtype = message.get_content_subtype()
192+
if subtype == "json" or subtype.endswith("+json"):
193+
json_body = await request.json()
194+
if json_body != Undefined:
195+
body = json_body
196+
else:
197+
body = body_bytes
185198
except json.JSONDecodeError as e:
186199
raise RequestValidationError([ErrorWrapper(e, ("body", e.pos))], body=e.doc)
187200
except Exception as e:

tests/test_tutorial/test_body/test_tutorial001.py

+75-9
Original file line numberDiff line numberDiff line change
@@ -173,25 +173,91 @@ def test_post_body(path, body, expected_status, expected_response):
173173

174174

175175
def test_post_broken_body():
176-
response = client.post("/items/", data={"name": "Foo", "price": 50.5})
176+
response = client.post(
177+
"/items/",
178+
headers={"content-type": "application/json"},
179+
data="{some broken json}",
180+
)
177181
assert response.status_code == 422, response.text
178182
assert response.json() == {
179183
"detail": [
180184
{
185+
"loc": ["body", 1],
186+
"msg": "Expecting property name enclosed in double quotes: line 1 column 2 (char 1)",
187+
"type": "value_error.jsondecode",
181188
"ctx": {
182-
"colno": 1,
183-
"doc": "name=Foo&price=50.5",
189+
"msg": "Expecting property name enclosed in double quotes",
190+
"doc": "{some broken json}",
191+
"pos": 1,
184192
"lineno": 1,
185-
"msg": "Expecting value",
186-
"pos": 0,
193+
"colno": 2,
187194
},
188-
"loc": ["body", 0],
189-
"msg": "Expecting value: line 1 column 1 (char 0)",
190-
"type": "value_error.jsondecode",
191195
}
192196
]
193197
}
198+
199+
200+
def test_post_form_for_json():
201+
response = client.post("/items/", data={"name": "Foo", "price": 50.5})
202+
assert response.status_code == 422, response.text
203+
assert response.json() == {
204+
"detail": [
205+
{
206+
"loc": ["body"],
207+
"msg": "value is not a valid dict",
208+
"type": "type_error.dict",
209+
}
210+
]
211+
}
212+
213+
214+
def test_explicit_content_type():
215+
response = client.post(
216+
"/items/",
217+
data='{"name": "Foo", "price": 50.5}',
218+
headers={"Content-Type": "application/json"},
219+
)
220+
assert response.status_code == 200, response.text
221+
222+
223+
def test_geo_json():
224+
response = client.post(
225+
"/items/",
226+
data='{"name": "Foo", "price": 50.5}',
227+
headers={"Content-Type": "application/geo+json"},
228+
)
229+
assert response.status_code == 200, response.text
230+
231+
232+
def test_wrong_headers():
233+
data = '{"name": "Foo", "price": 50.5}'
234+
invalid_dict = {
235+
"detail": [
236+
{
237+
"loc": ["body"],
238+
"msg": "value is not a valid dict",
239+
"type": "type_error.dict",
240+
}
241+
]
242+
}
243+
244+
response = client.post("/items/", data=data, headers={"Content-Type": "text/plain"})
245+
assert response.status_code == 422, response.text
246+
assert response.json() == invalid_dict
247+
248+
response = client.post(
249+
"/items/", data=data, headers={"Content-Type": "application/geo+json-seq"}
250+
)
251+
assert response.status_code == 422, response.text
252+
assert response.json() == invalid_dict
253+
response = client.post(
254+
"/items/", data=data, headers={"Content-Type": "application/not-really-json"}
255+
)
256+
assert response.status_code == 422, response.text
257+
assert response.json() == invalid_dict
258+
259+
260+
def test_other_exceptions():
194261
with patch("json.loads", side_effect=Exception):
195262
response = client.post("/items/", json={"test": "test2"})
196263
assert response.status_code == 400, response.text
197-
assert response.json() == {"detail": "There was an error parsing the body"}

tests/test_tutorial/test_custom_request_and_route/test_tutorial001.py

+1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ def test_gzip_request(compress):
2525
if compress:
2626
data = gzip.compress(data)
2727
headers["Content-Encoding"] = "gzip"
28+
headers["Content-Type"] = "application/json"
2829
response = client.post("/sum", data=data, headers=headers)
2930
assert response.json() == {"sum": n}
3031

0 commit comments

Comments
 (0)