← oracle_full

fastapi_14978

resolved RESOLVED UNSUBMITTED PASS · None tool calls · 0 s · fastapi/fastapi

Task input

🔒️ Add `strict_content_type` checking for JSON requests

🔒️ Add `strict_content_type` checking for JSON requests

Tool calls (0)

#ToolArgumentsResult
No trace captured.

Patch

--- 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 [])

Test output

show
.....................................                                    [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