Skip to content

[BUG] Live reload enters infinite page reload loop on uvicorn >= 0.39 #817

@nambaaa-pt

Description

@nambaaa-pt

Describe the bug
When running FastHTML with fast_app(live=True), the browser gets stuck in a high-frequency, infinite reload loop. The /live-reload WebSocket is accepted and then immediately closed, which triggers the client reconnection logic and causes the page to reload over and over even when no files change.

This issue only occurs on uvicorn >= 0.39.0. It does not reproduce on uvicorn 0.38.0.

Workaround: fast_app(live=False) or pin uvicorn to < 0.39.0.

Minimal Reproducible Example

from fasthtml.common import *

app, rt = fast_app(live=True)

@rt("/")
def home():
    return Titled("Test", P("hello"))

serve()

Run the app and open / in a browser. The page will continually reload without any code changes.

Server logs show repeated connect/disconnect:

INFO: ... - "GET / HTTP/1.1" 200 OK
INFO: ... - "WebSocket /live-reload" [accepted]
INFO: connection open
INFO: connection closed
(repeats)

Expected behavior

  • The /live-reload websocket should remain open under normal operation.
  • The page should reload only when the server actually restarts (e.g. due to hot reload), not continuously while the server is healthy.

Environment Information

  • fastlite version: 0.2.3
  • fastcore version: 1.9.4
  • fasthtml version: 0.12.36
  • uvicorn version: >= 0.39.0 (issue does not occur on 0.38.0)
  • Python version: 3.13

Confirmation

  • I have read the FAQ (https://docs.fastht.ml/explains/faq.html)
  • I have provided a minimal reproducible example
  • I have included the versions of fastlite, fastcore, and fasthtml
  • I understand that this is a volunteer open source project with no commercial support.

Additional context

Root Cause

In fasthtml/live_reload.py, the websocket handler accepts and then returns immediately:

async def live_reload_ws(websocket): await websocket.accept()

On uvicorn >= 0.39.0 (released December 21, 2025), returning from the ASGI websocket handler causes uvicorn to send a close frame. From the uvicorn release notes:

Fixed: Send close frame on ASGI return for WebSockets

Before this fix, uvicorn did not send a close frame when the handler returned, so the WebSocket connection remained open at the TCP level even though the handler had exited. This allowed live-reload to work correctly — the onclose event only fired when the server actually restarted.

With uvicorn >= 0.39.0, the close frame is now correctly sent (per ASGI spec), which exposes the issue in FastHTML's handler.

The reload loop works as follows:

  1. First connect succeeds (attempts=0) → no reload, logs "LiveReload connected".
  2. Server handler returns → uvicorn sends close frame → onclose fires.
  3. Client reconnects (attempts=1).
  4. Second connect succeeds with attempts > 0 → client calls window.location.reload().
  5. Page reload repeats from step 1 indefinitely.

Suggested Fix

Keep the websocket open until the client disconnects.

Current code (fasthtml/live_reload.py):

async def live_reload_ws(websocket): await websocket.accept()

Suggested fix:

from starlette.websockets import WebSocketDisconnect

async def live_reload_ws(websocket):
    await websocket.accept()
    try:
        while True:
            await websocket.receive()  # blocks; exits on disconnect
    except WebSocketDisconnect:
        pass

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions