mirror of
https://github.com/fastapi/fastapi.git
synced 2026-09-11 21:07:50 -04:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
95f8322ee1 | ||
|
|
f137944c43 | ||
|
|
d62354434b | ||
|
|
1d211b9c10 | ||
|
|
8a1f876841 | ||
|
|
c7e7b651d6 | ||
|
|
6bceb84053 | ||
|
|
5429fed84e |
No files matched your search
@@ -119,6 +119,10 @@ Running `fastapi dev` initiates development mode.
|
||||
|
||||
By default, **auto-reload** is enabled, automatically reloading the server when you make changes to your code. This is resource-intensive and could be less stable than when it's disabled. You should only use it for development. It also listens on the IP address `127.0.0.1`, which is the IP for your machine to communicate with itself alone (`localhost`).
|
||||
|
||||
Before importing your app, `fastapi dev` sets the `FASTAPI_ENV` environment variable to `development`. If `FASTAPI_ENV` is already set, its existing value is preserved. This lets app startup code choose development-friendly behavior while allowing you to provide an app-specific environment such as `staging`.
|
||||
|
||||
The conventional `FASTAPI_ENV` values are `development` and `production`. `fastapi run` currently leaves `FASTAPI_ENV` unchanged, so set it explicitly if your app needs to detect production mode.
|
||||
|
||||
## `fastapi run` { #fastapi-run }
|
||||
|
||||
Executing `fastapi run` starts FastAPI in production mode.
|
||||
|
||||
@@ -7,6 +7,22 @@ hide:
|
||||
|
||||
## Latest Changes
|
||||
|
||||
## 0.141.1 (2026-07-29)
|
||||
|
||||
### Fixes
|
||||
|
||||
* 🐛 Fix support for background tasks and headers from dependencies in `app.frontend()`. PR [#16105](https://github.com/fastapi/fastapi/pull/16105) by [@tiangolo](https://github.com/tiangolo).
|
||||
|
||||
### Docs
|
||||
|
||||
* 📝 Document `FASTAPI_ENV` in FastAPI CLI guide. PR [#16104](https://github.com/fastapi/fastapi/pull/16104) by [@tiangolo](https://github.com/tiangolo).
|
||||
|
||||
## 0.141.0 (2026-07-29)
|
||||
|
||||
### Features
|
||||
|
||||
* ✨ Add `app.frontend(check_dir="auto")`, to make local development more convenient with `fastapi dev`. PR [#16102](https://github.com/fastapi/fastapi/pull/16102) by [@tiangolo](https://github.com/tiangolo).
|
||||
|
||||
## 0.140.13 (2026-07-28)
|
||||
|
||||
### Fixes
|
||||
|
||||
@@ -106,9 +106,13 @@ Then missing frontend paths return the normal `404`.
|
||||
|
||||
## Check Directory { #check-directory }
|
||||
|
||||
By default, `app.frontend()` checks that the directory exists when the app is created.
|
||||
By default, `app.frontend()` uses `check_dir="auto"`.
|
||||
|
||||
This helps catch configuration errors early. For example, if the frontend build output directory is missing, **FastAPI** will raise an error on startup.
|
||||
When the `FASTAPI_ENV` environment variable is set to `development`, **FastAPI** only shows a warning if the frontend build output directory is missing. The [`fastapi dev` command](https://github.com/fastapi/fastapi-cli#fastapi-dev) sets this environment variable for you if it is not already set. This lets you start the backend before building or starting the frontend during development.
|
||||
|
||||
In any other environment, **FastAPI** raises an error when the app is created. This helps catch configuration errors early before deploying an app without its frontend files.
|
||||
|
||||
You can also set `check_dir=True` to always check the directory when the app is created.
|
||||
|
||||
If your frontend files are created later, for example by a separate build step after the app object is created, set `check_dir=False`:
|
||||
|
||||
@@ -132,6 +136,8 @@ Frontend responses run inside the normal **FastAPI** application, so HTTP middle
|
||||
|
||||
Dependencies from the app, from an `APIRouter`, and from `include_router()` also apply to frontend responses. This can be useful for protecting a frontend with cookie authentication or similar.
|
||||
|
||||
Dependencies can also modify response headers and add background tasks, as with normal *path operations*.
|
||||
|
||||
## Static Build Output Only { #static-build-output-only }
|
||||
|
||||
`app.frontend()` serves files already generated by your frontend build.
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
"""FastAPI framework, high performance, easy to learn, fast to code, ready for production"""
|
||||
|
||||
__version__ = "0.140.13"
|
||||
__version__ = "0.141.1"
|
||||
|
||||
from starlette import status as status
|
||||
|
||||
|
||||
@@ -1247,13 +1247,16 @@ class FastAPI(Starlette):
|
||||
),
|
||||
] = "auto",
|
||||
check_dir: Annotated[
|
||||
bool,
|
||||
bool | Literal["auto"],
|
||||
Doc(
|
||||
"""
|
||||
Check that the frontend directory exists when the app is created.
|
||||
Check that the frontend directory exists when the app is created. When
|
||||
set to `"auto"`, skip the check with a warning when `FASTAPI_ENV` is
|
||||
`"development"`, and check it otherwise. The `fastapi dev` command
|
||||
sets `FASTAPI_ENV` to `"development"` if it is not already set.
|
||||
"""
|
||||
),
|
||||
] = True,
|
||||
] = "auto",
|
||||
) -> None:
|
||||
"""
|
||||
Serve a static frontend build as low-priority routes.
|
||||
@@ -1285,6 +1288,9 @@ class FastAPI(Starlette):
|
||||
app.frontend("/", directory="dist")
|
||||
```
|
||||
"""
|
||||
check_dir = routing._resolve_frontend_check_dir(
|
||||
directory=directory, check_dir=check_dir
|
||||
)
|
||||
self.router.frontend(
|
||||
path,
|
||||
directory=directory,
|
||||
|
||||
+48
-11
@@ -9,6 +9,7 @@ import os
|
||||
import stat
|
||||
import threading
|
||||
import types
|
||||
import warnings
|
||||
from collections.abc import (
|
||||
AsyncIterator,
|
||||
Awaitable,
|
||||
@@ -55,6 +56,7 @@ from fastapi.dependencies.models import (
|
||||
_is_gen_callable,
|
||||
)
|
||||
from fastapi.dependencies.utils import (
|
||||
SolvedDependency,
|
||||
_get_body_field,
|
||||
_get_flat_body_params,
|
||||
_should_embed_body_fields,
|
||||
@@ -1876,13 +1878,31 @@ def _get_resolved_absolute_path(path: str | os.PathLike[str]) -> str:
|
||||
return os.path.realpath(os.fspath(path))
|
||||
|
||||
|
||||
def _resolve_frontend_check_dir(
|
||||
*,
|
||||
directory: str | os.PathLike[str],
|
||||
check_dir: bool | Literal["auto"],
|
||||
) -> bool:
|
||||
if check_dir != "auto":
|
||||
return check_dir
|
||||
if os.environ.get("FASTAPI_ENV") != "development":
|
||||
return True
|
||||
if not os.path.isdir(directory):
|
||||
warnings.warn(
|
||||
f"Frontend directory '{directory}' does not exist. "
|
||||
f"Resolved absolute path: '{_get_resolved_absolute_path(directory)}'",
|
||||
stacklevel=3,
|
||||
)
|
||||
return False
|
||||
|
||||
|
||||
class _FrontendStaticFiles(StaticFiles):
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
directory: str | os.PathLike[str],
|
||||
fallback: Literal["auto", "index.html", "404.html"] | None,
|
||||
check_dir: bool = True,
|
||||
check_dir: bool,
|
||||
) -> None:
|
||||
self.fallback = fallback
|
||||
if check_dir and not os.path.isdir(directory):
|
||||
@@ -1917,6 +1937,12 @@ class _FrontendStaticFiles(StaticFiles):
|
||||
assert isinstance(path, str)
|
||||
return os.path.normpath(os.path.join(*path.split("/")))
|
||||
|
||||
async def get_response_for_scope(self, scope: Scope) -> Response:
|
||||
if not self.config_checked:
|
||||
await self.check_config()
|
||||
self.config_checked = True
|
||||
return await self.get_response(self.get_path(scope), scope)
|
||||
|
||||
async def get_response(self, path: str, scope: Scope) -> Response:
|
||||
if scope["method"] not in ("GET", "HEAD"):
|
||||
if await self._lookup_static_resource(path) is not None:
|
||||
@@ -2025,7 +2051,7 @@ class _FrontendRoute(BaseRoute):
|
||||
*,
|
||||
directory: str | os.PathLike[str],
|
||||
fallback: Literal["auto", "index.html", "404.html"] | None = "auto",
|
||||
check_dir: bool = True,
|
||||
check_dir: bool,
|
||||
) -> None:
|
||||
if fallback not in {"auto", "index.html", "404.html", None}:
|
||||
raise AssertionError(
|
||||
@@ -2067,7 +2093,8 @@ class _FrontendRoute(BaseRoute):
|
||||
return None
|
||||
|
||||
async def handle(self, scope: Scope, receive: Receive, send: Send) -> None:
|
||||
await self.app(scope, receive, send)
|
||||
response = await self.app.get_response_for_scope(scope)
|
||||
await response(scope, receive, send)
|
||||
|
||||
def url_path_for(self, name: str, /, **path_params: Any) -> URLPath:
|
||||
raise NoMatchFound(name, path_params)
|
||||
@@ -2099,7 +2126,7 @@ class _FrontendRouteGroup(BaseRoute):
|
||||
*,
|
||||
directory: str | os.PathLike[str],
|
||||
fallback: Literal["auto", "index.html", "404.html"] | None = "auto",
|
||||
check_dir: bool = True,
|
||||
check_dir: bool,
|
||||
) -> None:
|
||||
self.routes.append(
|
||||
_FrontendRoute(
|
||||
@@ -2170,8 +2197,12 @@ class _FrontendRouteGroup(BaseRoute):
|
||||
dependant=dependant,
|
||||
dependency_overrides_provider=dependency_overrides_provider,
|
||||
embed_body_fields=embed_body_fields,
|
||||
):
|
||||
await route.handle(scope, receive, send)
|
||||
) as solved_result:
|
||||
response = await route.app.get_response_for_scope(scope)
|
||||
if response.background is None:
|
||||
response.background = solved_result.background_tasks
|
||||
response.headers.raw.extend(solved_result.response.headers.raw)
|
||||
await response(scope, receive, send)
|
||||
return
|
||||
await route.handle(scope, receive, send)
|
||||
|
||||
@@ -2191,7 +2222,7 @@ class _FrontendRouteGroup(BaseRoute):
|
||||
dependant: Dependant,
|
||||
dependency_overrides_provider: Any | None,
|
||||
embed_body_fields: bool,
|
||||
) -> AsyncIterator[None]:
|
||||
) -> AsyncIterator[SolvedDependency]:
|
||||
request = Request(scope, receive, send)
|
||||
previous_inner_astack = scope.get("fastapi_inner_astack", _SCOPE_MISSING)
|
||||
previous_function_astack = scope.get("fastapi_function_astack", _SCOPE_MISSING)
|
||||
@@ -2209,7 +2240,7 @@ class _FrontendRouteGroup(BaseRoute):
|
||||
)
|
||||
if solved_result.errors:
|
||||
raise RequestValidationError(solved_result.errors)
|
||||
yield
|
||||
yield solved_result
|
||||
finally:
|
||||
if previous_inner_astack is _SCOPE_MISSING:
|
||||
scope.pop("fastapi_inner_astack", None)
|
||||
@@ -2624,13 +2655,16 @@ class APIRouter(routing.Router):
|
||||
),
|
||||
] = "auto",
|
||||
check_dir: Annotated[
|
||||
bool,
|
||||
bool | Literal["auto"],
|
||||
Doc(
|
||||
"""
|
||||
Check that the frontend directory exists when the app is created.
|
||||
Check that the frontend directory exists when the app is created. When
|
||||
set to `"auto"`, skip the check with a warning when `FASTAPI_ENV` is
|
||||
`"development"`, and check it otherwise. The `fastapi dev` command
|
||||
sets `FASTAPI_ENV` to `"development"` if it is not already set.
|
||||
"""
|
||||
),
|
||||
] = True,
|
||||
] = "auto",
|
||||
) -> None:
|
||||
"""
|
||||
Serve a static frontend build as low-priority routes.
|
||||
@@ -2664,6 +2698,9 @@ class APIRouter(routing.Router):
|
||||
app.include_router(router)
|
||||
```
|
||||
"""
|
||||
check_dir = _resolve_frontend_check_dir(
|
||||
directory=directory, check_dir=check_dir
|
||||
)
|
||||
normalized_path = _normalize_frontend_path(path)
|
||||
if self._frontend_routes is None:
|
||||
self._frontend_routes = _FrontendRouteGroup(
|
||||
|
||||
+3
-3
@@ -58,7 +58,7 @@ Changelog = "https://fastapi.tiangolo.com/release-notes/"
|
||||
|
||||
[project.optional-dependencies]
|
||||
standard = [
|
||||
"fastapi-cli[standard] >=0.0.8",
|
||||
"fastapi-cli[standard] >=0.0.32",
|
||||
"fastar >= 0.9.0",
|
||||
# For the test client
|
||||
"httpx >=0.23.0,<1.0.0",
|
||||
@@ -77,7 +77,7 @@ standard = [
|
||||
]
|
||||
|
||||
standard-no-fastapi-cloud-cli = [
|
||||
"fastapi-cli[standard-no-fastapi-cloud-cli] >=0.0.8",
|
||||
"fastapi-cli[standard-no-fastapi-cloud-cli] >=0.0.32",
|
||||
# For the test client
|
||||
"httpx >=0.23.0,<1.0.0",
|
||||
# For templates
|
||||
@@ -95,7 +95,7 @@ standard-no-fastapi-cloud-cli = [
|
||||
]
|
||||
|
||||
all = [
|
||||
"fastapi-cli[standard] >=0.0.8",
|
||||
"fastapi-cli[standard] >=0.0.32",
|
||||
# # For the test client
|
||||
"httpx >=0.23.0,<1.0.0",
|
||||
# For templates
|
||||
|
||||
+71
-2
@@ -7,7 +7,15 @@ from typing import Literal
|
||||
|
||||
import anyio
|
||||
import pytest
|
||||
from fastapi import APIRouter, Depends, FastAPI, HTTPException, Request, WebSocket
|
||||
from fastapi import (
|
||||
APIRouter,
|
||||
BackgroundTasks,
|
||||
Depends,
|
||||
FastAPI,
|
||||
HTTPException,
|
||||
Request,
|
||||
WebSocket,
|
||||
)
|
||||
from fastapi.testclient import TestClient
|
||||
from starlette.exceptions import HTTPException as StarletteHTTPException
|
||||
from starlette.responses import PlainTextResponse, Response
|
||||
@@ -491,6 +499,30 @@ def test_app_middleware_still_runs_for_frontend_dependencies(tmp_path: Path):
|
||||
assert calls == ["middleware-before", "dependency", "middleware-after"]
|
||||
|
||||
|
||||
def test_frontend_dependency_response_headers_and_background_tasks(tmp_path: Path):
|
||||
calls: list[str] = []
|
||||
|
||||
def frontend_dependency(
|
||||
response: Response, background_tasks: BackgroundTasks
|
||||
) -> None:
|
||||
response.headers["X-Frontend-Dependency"] = "applied"
|
||||
response.set_cookie("frontend", "dependency")
|
||||
background_tasks.add_task(calls.append, "background")
|
||||
|
||||
dist = tmp_path / "dist"
|
||||
write_file(dist / "index.html", "app")
|
||||
app = FastAPI(dependencies=[Depends(frontend_dependency)])
|
||||
app.frontend("/", directory=dist)
|
||||
|
||||
response = TestClient(app).get("/")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.text == "app"
|
||||
assert response.headers["X-Frontend-Dependency"] == "applied"
|
||||
assert response.cookies["frontend"] == "dependency"
|
||||
assert calls == ["background"]
|
||||
|
||||
|
||||
def test_frontend_dependency_validation_errors_return_422(tmp_path: Path):
|
||||
def require_token(token: str) -> None:
|
||||
pass # pragma: no cover
|
||||
@@ -1176,13 +1208,50 @@ def test_check_dir_true_fails_early_for_missing_directory(monkeypatch, tmp_path:
|
||||
monkeypatch.chdir(tmp_path)
|
||||
|
||||
with pytest.raises(RuntimeError, match="does not exist") as exc_info:
|
||||
app.frontend("/", directory="missing")
|
||||
app.frontend("/", directory="missing", check_dir=True)
|
||||
|
||||
message = str(exc_info.value)
|
||||
assert "'missing'" in message
|
||||
assert str(tmp_path / "missing") in message
|
||||
|
||||
|
||||
def test_check_dir_auto_warns_in_development(monkeypatch, tmp_path: Path):
|
||||
monkeypatch.setenv("FASTAPI_ENV", "development")
|
||||
app = FastAPI()
|
||||
|
||||
with pytest.warns(UserWarning, match="does not exist") as warnings:
|
||||
app.frontend("/", directory=tmp_path / "missing")
|
||||
|
||||
assert str(tmp_path / "missing") in str(warnings[0].message)
|
||||
assert warnings[0].filename == __file__
|
||||
|
||||
|
||||
def test_check_dir_auto_router_warning_points_to_user_code(monkeypatch, tmp_path: Path):
|
||||
monkeypatch.setenv("FASTAPI_ENV", "development")
|
||||
router = APIRouter()
|
||||
|
||||
with pytest.warns(UserWarning, match="does not exist") as warnings:
|
||||
router.frontend("/", directory=tmp_path / "missing")
|
||||
|
||||
assert warnings[0].filename == __file__
|
||||
|
||||
|
||||
def test_check_dir_true_fails_in_development(monkeypatch, tmp_path: Path):
|
||||
monkeypatch.setenv("FASTAPI_ENV", "development")
|
||||
app = FastAPI()
|
||||
|
||||
with pytest.raises(RuntimeError, match="does not exist"):
|
||||
app.frontend("/", directory=tmp_path / "missing", check_dir=True)
|
||||
|
||||
|
||||
def test_check_dir_auto_fails_outside_development(monkeypatch, tmp_path: Path):
|
||||
monkeypatch.setenv("FASTAPI_ENV", "production")
|
||||
router = APIRouter()
|
||||
|
||||
with pytest.raises(RuntimeError, match="does not exist"):
|
||||
router.frontend("/", directory=tmp_path / "missing")
|
||||
|
||||
|
||||
def test_check_dir_false_allows_missing_directory_and_fails_on_request(tmp_path: Path):
|
||||
app = FastAPI()
|
||||
app.frontend("/", directory=tmp_path / "missing", check_dir=False)
|
||||
|
||||
@@ -1005,9 +1005,9 @@ requires-dist = [
|
||||
{ name = "email-validator", marker = "extra == 'all'", specifier = ">=2.0.0" },
|
||||
{ name = "email-validator", marker = "extra == 'standard'", specifier = ">=2.0.0" },
|
||||
{ name = "email-validator", marker = "extra == 'standard-no-fastapi-cloud-cli'", specifier = ">=2.0.0" },
|
||||
{ name = "fastapi-cli", extras = ["standard"], marker = "extra == 'all'", specifier = ">=0.0.8" },
|
||||
{ name = "fastapi-cli", extras = ["standard"], marker = "extra == 'standard'", specifier = ">=0.0.8" },
|
||||
{ name = "fastapi-cli", extras = ["standard-no-fastapi-cloud-cli"], marker = "extra == 'standard-no-fastapi-cloud-cli'", specifier = ">=0.0.8" },
|
||||
{ name = "fastapi-cli", extras = ["standard"], marker = "extra == 'all'", specifier = ">=0.0.32" },
|
||||
{ name = "fastapi-cli", extras = ["standard"], marker = "extra == 'standard'", specifier = ">=0.0.32" },
|
||||
{ name = "fastapi-cli", extras = ["standard-no-fastapi-cloud-cli"], marker = "extra == 'standard-no-fastapi-cloud-cli'", specifier = ">=0.0.32" },
|
||||
{ name = "fastar", marker = "extra == 'standard'", specifier = ">=0.9.0" },
|
||||
{ name = "httpx", marker = "extra == 'all'", specifier = ">=0.23.0,<1.0.0" },
|
||||
{ name = "httpx", marker = "extra == 'standard'", specifier = ">=0.23.0,<1.0.0" },
|
||||
@@ -1144,7 +1144,7 @@ translations = [
|
||||
|
||||
[[package]]
|
||||
name = "fastapi-cli"
|
||||
version = "0.0.20"
|
||||
version = "0.0.32"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "rich-toolkit" },
|
||||
@@ -1152,9 +1152,9 @@ dependencies = [
|
||||
{ name = "typer" },
|
||||
{ name = "uvicorn", extra = ["standard"] },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d3/ca/d90fb3bfbcbd6e56c77afd9d114dd6ce8955d8bb90094399d1c70e659e40/fastapi_cli-0.0.20.tar.gz", hash = "sha256:d17c2634f7b96b6b560bc16b0035ed047d523c912011395f49f00a421692bc3a", size = 19786, upload-time = "2025-12-22T17:13:33.794Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/33/eb/3b534c6f8e157f9ddbf2a153512307c886cad0b258739c200dd8ff8c4452/fastapi_cli-0.0.32.tar.gz", hash = "sha256:38024d2345275e1b37ce8848727a580d84901b570e96b3256d9d36a9a5039424", size = 26636, upload-time = "2026-07-16T12:16:58.678Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/08/89/5c4eef60524d0fd704eb0706885b82cd5623a43396b94e4a5b17d3a3f516/fastapi_cli-0.0.20-py3-none-any.whl", hash = "sha256:e58b6a0038c0b1532b7a0af690656093dee666201b6b19d3c87175b358e9f783", size = 12390, upload-time = "2025-12-22T17:13:31.708Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/53/56ae5ae17bb0a5d89d1d31e5320eb1865553ebbfbde91cdc4c221245f2a8/fastapi_cli-0.0.32-py3-none-any.whl", hash = "sha256:8dcc286fa32f01bbd3f65dd09cfd5a2540ed5f2230b77db7fd30978d6165f3c4", size = 14670, upload-time = "2026-07-16T12:16:57.297Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
|
||||
Reference in new issue
Block a user