From ed2512a5ec33d9d70f7a9ef2e5f9fe52d3b77ab2 Mon Sep 17 00:00:00 2001 From: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> Date: Mon, 9 Feb 2026 17:31:57 +0100 Subject: [PATCH] =?utf8?q?=F0=9F=90=9B=20Fix=20`on=5Fstartup`=20and=20`on?= =?utf8?q?=5Fshutdown`=20parameters=20of=20`APIRouter`=20(#14873)?= MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit Co-authored-by: github-actions[bot] --- fastapi/routing.py | 21 ++++++------- tests/test_router_events.py | 60 +++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 10 deletions(-) diff --git a/fastapi/routing.py b/fastapi/routing.py index 0b4d28873..16a89ef3e 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -952,16 +952,6 @@ class APIRouter(routing.Router): ), ] = Default(generate_unique_id), ) -> None: - # Handle on_startup/on_shutdown locally since Starlette removed support - # Ref: https://github.com/Kludex/starlette/pull/3117 - # TODO: deprecate this once the lifespan (or alternative) interface is improved - self.on_startup: list[Callable[[], Any]] = ( - [] if on_startup is None else list(on_startup) - ) - self.on_shutdown: list[Callable[[], Any]] = ( - [] if on_shutdown is None else list(on_shutdown) - ) - # Determine the lifespan context to use if lifespan is None: # Use the default lifespan that runs on_startup/on_shutdown handlers @@ -985,6 +975,17 @@ class APIRouter(routing.Router): assert not prefix.endswith("/"), ( "A path prefix must not end with '/', as the routes will start with '/'" ) + + # Handle on_startup/on_shutdown locally since Starlette removed support + # Ref: https://github.com/Kludex/starlette/pull/3117 + # TODO: deprecate this once the lifespan (or alternative) interface is improved + self.on_startup: list[Callable[[], Any]] = ( + [] if on_startup is None else list(on_startup) + ) + self.on_shutdown: list[Callable[[], Any]] = ( + [] if on_shutdown is None else list(on_shutdown) + ) + self.prefix = prefix self.tags: list[Union[str, Enum]] = tags or [] self.dependencies = list(dependencies or []) diff --git a/tests/test_router_events.py b/tests/test_router_events.py index 65f2f521c..a47d11913 100644 --- a/tests/test_router_events.py +++ b/tests/test_router_events.py @@ -317,3 +317,63 @@ def test_router_async_generator_lifespan(state: State) -> None: assert response.json() == {"message": "Hello World"} assert state.app_startup is True assert state.app_shutdown is True + + +def test_startup_shutdown_handlers_as_parameters(state: State) -> None: + """Test that startup/shutdown handlers passed as parameters to FastAPI are called correctly.""" + + def app_startup() -> None: + state.app_startup = True + + def app_shutdown() -> None: + state.app_shutdown = True + + app = FastAPI(on_startup=[app_startup], on_shutdown=[app_shutdown]) + + @app.get("/") + def main() -> dict[str, str]: + return {"message": "Hello World"} + + def router_startup() -> None: + state.router_startup = True + + def router_shutdown() -> None: + state.router_shutdown = True + + router = APIRouter(on_startup=[router_startup], on_shutdown=[router_shutdown]) + + def sub_router_startup() -> None: + state.sub_router_startup = True + + def sub_router_shutdown() -> None: + state.sub_router_shutdown = True + + sub_router = APIRouter( + on_startup=[sub_router_startup], on_shutdown=[sub_router_shutdown] + ) + + router.include_router(sub_router) + app.include_router(router) + + assert state.app_startup is False + assert state.router_startup is False + assert state.sub_router_startup is False + assert state.app_shutdown is False + assert state.router_shutdown is False + assert state.sub_router_shutdown is False + with TestClient(app) as client: + assert state.app_startup is True + assert state.router_startup is True + assert state.sub_router_startup is True + assert state.app_shutdown is False + assert state.router_shutdown is False + assert state.sub_router_shutdown is False + response = client.get("/") + assert response.status_code == 200, response.text + assert response.json() == {"message": "Hello World"} + assert state.app_startup is True + assert state.router_startup is True + assert state.sub_router_startup is True + assert state.app_shutdown is True + assert state.router_shutdown is True + assert state.sub_router_shutdown is True -- 2.47.3