← mined_oracle

fastapi_15863

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

Task input

(not found in data/tasks.jsonl)

Tool calls (0)

#ToolArgumentsResult
No trace captured.

Patch

--- a/docs/en/docs/tutorial/frontend.md
+++ b/docs/en/docs/tutorial/frontend.md
@@ -52,7 +52,9 @@ For that, use `fallback="index.html"`:
 
 {* ../../docs_src/frontend/tutorial002_py310.py hl[5] *}
 
-**FastAPI** uses this fallback only for requests that look like browser navigation. Missing files like JavaScript, CSS, and images still return `404`.
+**FastAPI** uses this fallback only for `GET` and `HEAD` requests that look like browser navigation. Missing files like JavaScript, CSS, and images still return `404`.
+
+Requests with other methods, like `POST` or `PUT`, to paths that only match the frontend fallback also return `404`. Regular **FastAPI** *path operations* still have higher priority than frontend routes.
 
 /// tip
 
--- a/fastapi/routing.py
+++ b/fastapi/routing.py
@@ -1841,34 +1841,19 @@ class _FrontendStaticFiles(StaticFiles):
 
     async def get_response(self, path: str, scope: Scope) -> Response:
         if scope["method"] not in ("GET", "HEAD"):
-            raise HTTPException(status_code=405)
-
-        try:
-            full_path, stat_result = await run_in_threadpool(self.lookup_path, path)
-        except PermissionError:
-            raise HTTPException(status_code=401) from None
-        except OSError as exc:
-            if exc.errno == errno.ENAMETOOLONG:
-                raise HTTPException(status_code=404) from None
-            raise exc
-        except ValueError:
-            raise HTTPException(status_code=404) from None
+            if await self._lookup_static_resource(path) is not None:
+                raise HTTPException(status_code=405)
+            raise HTTPException(status_code=404)
 
-        if stat_result and stat.S_ISREG(stat_result.st_mode):
+        static_resource = await self._lookup_static_resource(path)
+        if static_resource is not None:
+            full_path, stat_result, is_directory_index = static_resource
+            if is_directory_index and not scope["path"].endswith("/"):
+                url = URL(scope=scope)
+                url = url.replace(path=url.path + "/")
+                return RedirectResponse(url=url)
             return self.file_response(full_path, stat_result, scope)
 
-        if stat_result and stat.S_ISDIR(stat_result.st_mode):
-            index_path = os.path.join(path, "index.html")
-            full_path, stat_result = await run_in_threadpool(
-                self.lookup_path, index_path
-            )
-            if stat_result is not None and stat.S_ISREG(stat_result.st_mode):
-                if not scope["path"].endswith("/"):
-                    url = URL(scope=scope)
-                    url = url.replace(path=url.path + "/")
-                    return RedirectResponse(url=url)
-                return self.file_response(full_path, stat_result, scope)
-
         if self.fallback == "404.html" or (
             self.fallback == "auto" and self._fallback_file_exists("404.html")
         ):
@@ -1882,6 +1867,33 @@ class _FrontendStaticFiles(StaticFiles):
 
         raise HTTPException(status_code=404)
 
+    async def _lookup_path(self, path: str) -> tuple[str, os.stat_result | None]:
+        try:
+            return await run_in_threadpool(self.lookup_path, path)
+        except PermissionError:
+            raise HTTPException(status_code=401) from None
+        except OSError as exc:
+            if exc.errno == errno.ENAMETOOLONG:
+                raise HTTPException(status_code=404) from None
+            raise exc
+        except ValueError:
+            raise HTTPException(status_code=404) from None
+
+    async def _lookup_static_resource(
+        self, path: str
+    ) -> tuple[str, os.stat_result, bool] | None:
+        full_path, stat_result = await self._lookup_path(path)
+        if stat_result is None:
+            return None
+        if stat.S_ISREG(stat_result.st_mode):
+            return full_path, stat_result, False
+        if stat.S_ISDIR(stat_result.st_mode):
+            index_path = os.path.join(path, "index.html")
+            full_path, stat_result = await self._lookup_path(index_path)
+            if stat_result is not None and stat.S_ISREG(stat_result.st_mode):
+                return full_path, stat_result, True
+        return None
+
     def _fallback_file_exists(self, fallback: str) -> bool:
         _, stat_result = self.lookup_path(fallback)
         return stat_result is not None and stat.S_ISREG(stat_result.st_mode)

Test output

show
........................................................................ [ 97%]
..                                                                       [100%]
=============================== warnings summary ===============================
../../../../../../../Users/jp/repos/kaggle-gemini-coding-agent-post-training/.envs/overlays/starlette-1.6.0-py3-none-any/starlette/testclient.py:53
  /Users/jp/repos/kaggle-gemini-coding-agent-post-training/.envs/overlays/starlette-1.6.0-py3-none-any/starlette/testclient.py:53: 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
74 passed, 1 warning in 0.66s