Commit aa9e23d5 authored by vertighel's avatar vertighel
Browse files

Switch uvicorn websocket backend off wsproto (sansio, fallback websockets)



wsproto has a known upstream bug (Kludex/uvicorn#997) where a client that
abandons a connection without closing cleanly (e.g. a page reload) can
wedge it into an unrecoverable state instead of raising — matches the
"Unk with no crash" reports on the LAN client, where tight timing makes
the underlying close race easier to hit than over the VPN link.

noctua.app.run() now tries ws="websockets-sansio" first, falling back to
the legacy "websockets" impl if the installed package is too old for the
sans-io API. Never falls back further to wsproto/auto — that's the
backend being replaced. Added "websockets" to pyproject.toml (previously
absent, which is why uvicorn's "auto" was silently landing on wsproto).

Co-Authored-By: default avatarClaude Sonnet 5 <noreply@anthropic.com>
parent 55a00be4
Loading
Loading
Loading
Loading
Loading
+24 −4
Original line number Diff line number Diff line
@@ -12,6 +12,7 @@ from noctua.api.baseresource import register_error_handlers
from noctua.web import web_blueprint
from noctua.api.fits_image import viewer_blueprint
from noctua.utils import check
from noctua.utils.logger import log


app = Quart(__name__)
@@ -23,9 +24,28 @@ app.register_blueprint(api_blueprint, url_prefix='/api')
app.register_blueprint(web_blueprint, url_prefix='/web')  # Added Web Blueprint
app.register_blueprint(viewer_blueprint, url_prefix='/api')

def _serve(ws):
    """Run uvicorn with the given websocket implementation until Ctrl-C."""

    try:
        uvicorn.run(app, host="0.0.0.0", port=5533, ws=ws)
    except KeyboardInterrupt as e:
        print(f"Shutting down: {e}")


def run():
    """
    Server run using uvicorn.

    ws="websockets-sansio" is preferred, falling back to the legacy
    "websockets" implementation if the installed 'websockets' package is
    too old for the sans-io API (both come from the same package, so this
    only matters until it's upgraded). Deliberately never falls back to
    "wsproto" (uvicorn's default when 'websockets' isn't installed at
    all): it has a known upstream bug where a client that doesn't close
    cleanly (e.g. a page reload) can wedge the connection into an
    unrecoverable state instead of raising — see
    https://github.com/Kludex/uvicorn/issues/997.
    """

    # Only the long-lived noctua-app process should survive an
@@ -35,10 +55,10 @@ def run():
    check.set_recoverable(True)

    try:
        uvicorn.run(app, host="0.0.0.0", port=5533)
    except KeyboardInterrupt as e:
        print(f"Shutting down: {e}")
        pass
        _serve(ws="websockets-sansio")
    except Exception as e:
        log.warning(f"uvicorn ws='websockets-sansio' unavailable ({e}), falling back to 'websockets'")
        _serve(ws="websockets")


if __name__ == "__main__":
+2 −2
Original line number Diff line number Diff line
@@ -138,11 +138,11 @@ label:has(~ div :disabled) {
}

.form-control:disabled,
.form-select:disabled {
.form-select:disabled,
input:read-only{
    color: var(--bs-secondary-color);
}


/* ── 8. Mode selector tabs ──────────────────────────────────────────────── */

#mode-selector .nav-link {
+1 −0
Original line number Diff line number Diff line
@@ -49,6 +49,7 @@ dependencies = [
    "quart",
    "quart_cors",
    "uvicorn",
    "websockets", # uvicorn ws=websockets-sansio/websockets — see noctua.app.run()
    "httpx", # Suggested for async calls to devices

    # "Fourth" party :)