Skip to content

Fix Starlette 1.0 compatibility: convert on_startup/on_shutdown to lifespan#849

Open
namemartin wants to merge 2 commits intoAnswerDotAI:mainfrom
namemartin:fix/starlette-1.0-lifespan
Open

Fix Starlette 1.0 compatibility: convert on_startup/on_shutdown to lifespan#849
namemartin wants to merge 2 commits intoAnswerDotAI:mainfrom
namemartin:fix/starlette-1.0-lifespan

Conversation

@namemartin
Copy link

Related Issue
Fixes #847

Proposed Changes
Starlette 1.0 removed on_startup/on_shutdown params from Starlette.__init__(). This adds _wrap_lifespan() to convert these callbacks into a lifespan context manager. Unlike #848, this composes with any user-provided lifespan instead of silently dropping the callbacks.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)

Checklist

  • My code follows the code style of this project.
  • My change requires a change to the documentation.
  • I have added tests to cover my changes.
  • All new and existing tests passed.
  • I am aware that this is an nbdev project, and I have edited, cleaned, and synced the source notebooks instead of editing .py or .md files directly.

Additional Information
Closes #848's approach by composing on_startup/on_shutdown with lifespan rather than prioritizing one over the other. Also uses inspect.iscoroutinefunction instead of the deprecated asyncio.iscoroutinefunction.

…fespan

Starlette 1.0 removed on_startup/on_shutdown params from Starlette.__init__().
Add _wrap_lifespan() to convert these callbacks into a lifespan context manager,
composing with any user-provided lifespan. Fixes AnswerDotAI#847.
Copilot AI review requested due to automatic review settings March 23, 2026 13:20
@review-notebook-app
Copy link

Check out this pull request on  ReviewNB

See visual diffs & provide feedback on Jupyter Notebooks.


Powered by ReviewNB

@gitnotebooks
Copy link

gitnotebooks bot commented Mar 23, 2026

Found 1 changed notebook. Review the changes at https://app.gitnotebooks.com/AnswerDotAI/fasthtml/pull/849

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Updates FastHTML’s Starlette integration to stay compatible with Starlette 1.0 by translating legacy on_startup/on_shutdown callbacks into a lifespan context manager that composes with any user-provided lifespan.

Changes:

  • Add _wrap_lifespan() helper to compose on_startup/on_shutdown with an optional user lifespan.
  • Update FastHTML.__init__ to pass only lifespan= to Starlette.__init__ (removing on_startup=/on_shutdown= kwargs).
  • Add regression tests covering basic app creation and lifecycle ordering across sync/async handlers.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.

File Description
fasthtml/core.py Introduces _wrap_lifespan() and routes startup/shutdown through lifespan for Starlette 1.0 compatibility.
nbs/api/00_core.ipynb Source notebook changes for the same lifespan wrapping logic (nbdev sync).
tests/test_lifespan.py New tests validating startup/shutdown behavior and lifespan composition.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

fasthtml/core.py Outdated
Comment on lines +576 to +580
for h in on_startup: await h() if inspect.iscoroutinefunction(h) else h()
if lifespan:
async with lifespan(app) as state: yield state
else: yield
for h in on_shutdown: await h() if inspect.iscoroutinefunction(h) else h()
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using inspect.iscoroutinefunction(h) is not sufficient to detect async work here (e.g., functools.partial(async_fn), callable instances with async __call__, or sync wrappers returning an awaitable). This can lead to un-awaited coroutine objects. Call the handler first and then await the result if it is awaitable (e.g., via inspect.isawaitable).

Suggested change
for h in on_startup: await h() if inspect.iscoroutinefunction(h) else h()
if lifespan:
async with lifespan(app) as state: yield state
else: yield
for h in on_shutdown: await h() if inspect.iscoroutinefunction(h) else h()
for h in on_startup:
res = h()
if inspect.isawaitable(res):
await res
if lifespan:
async with lifespan(app) as state: yield state
else: yield
for h in on_shutdown:
res = h()
if inspect.isawaitable(res):
await res

Copilot uses AI. Check for mistakes.
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kept iscoroutinefunction for simplicity

Comment on lines +1732 to +1736
" for h in on_startup: await h() if inspect.iscoroutinefunction(h) else h()\n",
" if lifespan:\n",
" async with lifespan(app) as state: yield state\n",
" else: yield\n",
" for h in on_shutdown: await h() if inspect.iscoroutinefunction(h) else h()\n",
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

inspect.iscoroutinefunction won’t catch common async handler forms like partial(async_fn) or callable objects with async __call__, and can leave coroutine results un-awaited. Consider invoking each handler and awaiting the result when it’s awaitable (e.g., inspect.isawaitable(result)).

Suggested change
" for h in on_startup: await h() if inspect.iscoroutinefunction(h) else h()\n",
" if lifespan:\n",
" async with lifespan(app) as state: yield state\n",
" else: yield\n",
" for h in on_shutdown: await h() if inspect.iscoroutinefunction(h) else h()\n",
" for h in on_startup:\n",
" res = h()\n",
" if inspect.isawaitable(res): await res\n",
" if lifespan:\n",
" async with lifespan(app) as state: yield state\n",
" else: yield\n",
" for h in on_shutdown:\n",
" res = h()\n",
" if inspect.isawaitable(res): await res\n",

Copilot uses AI. Check for mistakes.
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kept iscoroutinefunction for simplicity

Wrap the lifespan yield in try/finally so on_shutdown callbacks
execute even when the lifespan context manager raises an exception.
Add test to verify this behavior.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Breaking changes in Starlette

2 participants