resolved RESOLVED UNSUBMITTED PASS · None tool calls · 0 s · fastapi/fastapi
🔒️ Add `strict_content_type` checking for JSON requests 🔒️ Add `strict_content_type` checking for JSON requests
| # | Tool | Arguments | Result |
|---|---|---|---|
| No trace captured. | |||
--- a/docs_src/strict_content_type/tutorial001_py310.py
+++ b/docs_src/strict_content_type/tutorial001_py310.py
@@ -0,0 +1,14 @@
+from fastapi import FastAPI
+from pydantic import BaseModel
+
+app = FastAPI(strict_content_type=False)
+
+
+class Item(BaseModel):
+ name: str
+ price: float
+
+
+@app.post("/items/")
+async def create_item(item: Item):
+ return item
--- a/fastapi/applications.py
+++ b/fastapi/applications.py
@@ -840,6 +840,29 @@ class Item(BaseModel):
"""
),
] = None,
+ strict_content_type: Annotated[
+ bool,
+ Doc(
+ """
+ Enable strict checking for request Content-Type headers.
+
+ When `True` (the default), requests with a body that do not include
+ a `Content-Type` header will **not** be parsed as JSON.
+
+ This prevents potential cross-site request forgery (CSRF) attacks
+ that exploit the browser's ability to send requests without a
+ Content-Type header, bypassing CORS preflight checks. In particular
+ applicable for apps that need to be run locally (in localhost).
+
+ When `False`, requests without a `Content-Type` header will have
+ their body parsed as JSON, which maintains compatibility with
+ certain clients that don't send `Content-Type` headers.
+
+ Read more about it in the
+ [FastAPI docs for Strict Content-Type](https://fastapi.tiangolo.com/advanced/strict-content-type/).
+ """
+ ),
+ ] = True,
**extra: Annotated[
Any,
Doc(
@@ -974,6 +997,7 @@ class Item(BaseModel):
include_in_schema=include_in_schema,
responses=responses,
generate_unique_id_function=generate_unique_id_function,
+ strict_content_type=strict_content_type,
)
self.exception_handlers: dict[
Any, Callable[[Request, Any], Response | Awaitable[Response]]
--- a/fastapi/routing.py
+++ b/fastapi/routing.py
@@ -329,6 +329,7 @@ def get_request_handler(
response_model_exclude_none: bool = False,
dependency_overrides_provider: Any | None = None,
embed_body_fields: bool = False,
+ strict_content_type: bool | DefaultPlaceholder = Default(True),
) -> Callable[[Request], Coroutine[Any, Any, Response]]:
assert dependant.call is not None, "dependant.call must be a function"
is_coroutine = dependant.is_coroutine_callable
@@ -337,6 +338,10 @@ def get_request_handler(
actual_response_class: type[Response] = response_class.value
else:
actual_response_class = response_class
+ if isinstance(strict_content_type, DefaultPlaceholder):
+ actual_strict_content_type: bool = strict_content_type.value
+ else:
+ actual_strict_content_type = strict_content_type
async def app(request: Request) -> Response:
response: Response | None = None
@@ -370,7 +375,8 @@ async def app(request: Request) -> Response:
json_body: Any = Undefined
content_type_value = request.headers.get("content-type")
if not content_type_value:
- json_body = await request.json()
+ if not actual_strict_content_type:
+ json_body = await request.json()
else:
message = email.message.Message()
message["content-type"] = content_type_value
@@ -599,6 +605,7 @@ def __init__(
openapi_extra: dict[str, Any] | None = None,
generate_unique_id_function: Callable[["APIRoute"], str]
| DefaultPlaceholder = Default(generate_unique_id),
+ strict_content_type: bool | DefaultPlaceholder = Default(True),
) -> None:
self.path = path
self.endpoint = endpoint
@@ -625,6 +632,7 @@ def __init__(
self.callbacks = callbacks
self.openapi_extra = openapi_extra
self.generate_unique_id_function = generate_unique_id_function
+ self.strict_content_type = strict_content_type
self.tags = tags or []
self.responses = responses or {}
self.name = get_name(endpoint) if name is None else name
@@ -713,6 +721,7 @@ def get_route_handler(self) -> Callable[[Request], Coroutine[Any, Any, Response]
response_model_exclude_none=self.response_model_exclude_none,
dependency_overrides_provider=self.dependency_overrides_provider,
embed_body_fields=self._embed_body_fields,
+ strict_content_type=self.strict_content_type,
)
def matches(self, scope: Scope) -> tuple[Match, Scope]:
@@ -963,6 +972,29 @@ def __init__(
"""
),
] = Default(generate_unique_id),
+ strict_content_type: Annotated[
+ bool,
+ Doc(
+ """
+ Enable strict checking for request Content-Type headers.
+
+ When `True` (the default), requests with a body that do not include
+ a `Content-Type` header will **not** be parsed as JSON.
+
+ This prevents potential cross-site request forgery (CSRF) attacks
+ that exploit the browser's ability to send requests without a
+ Content-Type header, bypassing CORS preflight checks. In particular
+ applicable for apps that need to be run locally (in localhost).
+
+ When `False`, requests without a `Content-Type` header will have
+ their body parsed as JSON, which maintains compatibility with
+ certain clients that don't send `Content-Type` headers.
+
+ Read more about it in the
+ [FastAPI docs for Strict Content-Type](https://fastapi.tiangolo.com/advanced/strict-content-type/).
+ """
+ ),
+ ] = Default(True),
) -> None:
# Determine the lifespan context to use
if lifespan is None:
@@ -1009,6 +1041,7 @@ def __init__(
self.route_class = route_class
self.default_response_class = default_response_class
self.generate_unique_id_function = generate_unique_id_function
+ self.strict_content_type = strict_content_type
def route(
self,
@@ -1059,6 +1092,7 @@ def add_api_route(
openapi_extra: dict[str, Any] | None = None,
generate_unique_id_function: Callable[[APIRoute], str]
| DefaultPlaceholder = Default(generate_unique_id),
+ strict_content_type: bool | DefaultPlaceholder = Default(True),
) -> None:
route_class = route_class_override or self.route_class
responses = responses or {}
@@ -1105,6 +1139,9 @@ def add_api_route(
callbacks=current_callbacks,
openapi_extra=openapi_extra,
generate_unique_id_function=current_generate_unique_id,
+ strict_content_type=get_value_or_default(
+ strict_content_type, self.strict_content_type
+ ),
)
self.routes.append(route)
@@ -1480,6 +1517,11 @@ def read_users():
callbacks=current_callbacks,
openapi_extra=route.openapi_extra,
generate_unique_id_function=current_generate_unique_id,
+ strict_content_type=get_value_or_default(
+ route.strict_content_type,
+ router.strict_content_type,
+ self.strict_content_type,
+ ),
)
elif isinstance(route, routing.Route):
methods = list(route.methods or [])..................................... [100%]
=============================== warnings summary ===============================
../../../../../../../Users/jp/repos/kaggle-gemini-coding-agent-post-training/.envs/overlays/starlette-0.52.1-py3-none-any/starlette/testclient.py:45
/Users/jp/repos/kaggle-gemini-coding-agent-post-training/.envs/overlays/starlette-0.52.1-py3-none-any/starlette/testclient.py:45: DeprecationWarning: The anyio.abc.BlockingPortal alias is deprecated, use anyio.from_thread.BlockingPortal instead.
_PortalFactoryType = Callable[[], AbstractContextManager[anyio.abc.BlockingPortal]]
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
37 passed, 1 warning in 0.64s