From 5e67b2a3853fc361e86044e0beb7ba1ee92da637 Mon Sep 17 00:00:00 2001 From: Kye Russell Date: Mon, 3 Nov 2025 12:07:27 +0800 Subject: [PATCH 1/4] Allow specifying the host, port, and schema used to generate the websocket URL sent to the browser. --- sphinx_autobuild/__main__.py | 57 ++++++++++++++++++++++++++++++++-- sphinx_autobuild/middleware.py | 19 +++++++++--- tests/test_application.py | 6 ++-- 3 files changed, 73 insertions(+), 9 deletions(-) diff --git a/sphinx_autobuild/__main__.py b/sphinx_autobuild/__main__.py index b1f058f..d7feb83 100644 --- a/sphinx_autobuild/__main__.py +++ b/sphinx_autobuild/__main__.py @@ -48,6 +48,11 @@ def main(argv=()): port_num = args.port or find_free_port() url_host = f"{host_name}:{port_num}" + # Determine base URL for websocket URL sent to browser. (default to server host/port) + websocket_host = args.ws_host or host_name + websocket_port = args.ws_port if args.ws_port is not None else port_num + websocket_https = args.ws_https + pre_build_commands = list(map(shlex.split, args.pre_build)) post_build_commands = list(map(shlex.split, args.post_build)) builder = Builder( @@ -80,7 +85,15 @@ def main(argv=()): ] ignore_dirs = list(filter(None, ignore_dirs)) ignore_handler = IgnoreFilter(ignore_dirs, args.re_ignore) - app = _create_app(watch_dirs, ignore_handler, builder, serve_dir, url_host) + app = _create_app( + watch_dirs, + ignore_handler, + builder, + serve_dir, + websocket_host, + websocket_port, + websocket_https, + ) if not args.no_initial_build: show_message("Starting initial build") @@ -96,7 +109,15 @@ def main(argv=()): show_message("Server ceasing operations. Cheerio!") -def _create_app(watch_dirs, ignore_handler, builder, out_dir, url_host): +def _create_app( + watch_dirs, + ignore_handler, + builder, + out_dir, + websocket_host, + websocket_port, + websocket_https=False, +): watcher = RebuildServer(watch_dirs, ignore_handler, change_callback=builder) return Starlette( @@ -104,7 +125,14 @@ def _create_app(watch_dirs, ignore_handler, builder, out_dir, url_host): WebSocketRoute("/websocket-reload", watcher, name="reload"), Mount("/", app=StaticFiles(directory=out_dir, html=True), name="static"), ], - middleware=[Middleware(JavascriptInjectorMiddleware, ws_url=url_host)], + middleware=[ + Middleware( + JavascriptInjectorMiddleware, + websocket_host=websocket_host, + websocket_port=websocket_port, + websocket_https=websocket_https, + ) + ], lifespan=watcher.lifespan, ) @@ -245,6 +273,29 @@ def _add_autobuild_arguments(parser): default=[], help="additional command(s) to run after building the documentation", ) + group.add_argument( + "--ws-host", + type=str, + default=None, + help="host portion of the websocket URL to ask the browser to " + "connect to (defaults to --host, useful when running behind a " + "reverse proxy)", + ) + group.add_argument( + "--ws-port", + type=int, + default=None, + help="port portion of the websocket URL to ask the browser to " + "connect to (defaults to --port, useful when running behind a " + "reverse proxy)", + ) + group.add_argument( + "--ws-https", + action="store_true", + default=False, + help="tell the browser to use HTTPS/WSS for websocket connections. " + "(useful when running behind a reverse proxy)", + ) return group diff --git a/sphinx_autobuild/middleware.py b/sphinx_autobuild/middleware.py index 2652a0c..e82f448 100644 --- a/sphinx_autobuild/middleware.py +++ b/sphinx_autobuild/middleware.py @@ -8,20 +8,31 @@ from starlette.types import ASGIApp, Message, Receive, Scope, Send -def web_socket_script(ws_url: str) -> str: +def web_socket_script(host: str, port: int, https: bool = False) -> str: + websocket_url = f"{'wss' if https else 'ws'}://{host}:{port}/websocket-reload" # language=HTML return f""" """ class JavascriptInjectorMiddleware: - def __init__(self, app: ASGIApp, ws_url: str) -> None: + def __init__( + self, + app: ASGIApp, + websocket_host: str, + websocket_port: int, + websocket_https: bool = False, + ) -> None: self.app = app - self.script = web_socket_script(ws_url).encode("utf-8") + self.script = web_socket_script( + websocket_host, + websocket_port, + websocket_https, + ).encode("utf-8") async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: add_script = False diff --git a/tests/test_application.py b/tests/test_application.py index 6919d9a..7d19e84 100644 --- a/tests/test_application.py +++ b/tests/test_application.py @@ -18,7 +18,9 @@ def test_application(tmp_path): shutil.copytree(ROOT / "docs", tmp_path / "docs") out_dir.mkdir(parents=True, exist_ok=True) - url_host = "127.0.0.1:7777" + host = "127.0.0.1" + port = 7777 + url_host = f"{host}:{port}" ignore_handler = IgnoreFilter([out_dir], []) builder = Builder( [str(src_dir), str(out_dir)], @@ -26,7 +28,7 @@ def test_application(tmp_path): pre_build_commands=[], post_build_commands=[], ) - app = _create_app([src_dir], ignore_handler, builder, out_dir, url_host) + app = _create_app([src_dir], ignore_handler, builder, out_dir, host, port) client = TestClient(app) builder(changed_paths=()) From 5d53338efcc59dedee80fd312c3d6f1abbd5d9d0 Mon Sep 17 00:00:00 2001 From: Kye Russell Date: Mon, 3 Nov 2025 12:22:10 +0800 Subject: [PATCH 2/4] Fixed comment --- sphinx_autobuild/__main__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sphinx_autobuild/__main__.py b/sphinx_autobuild/__main__.py index d7feb83..31e1ecb 100644 --- a/sphinx_autobuild/__main__.py +++ b/sphinx_autobuild/__main__.py @@ -48,7 +48,8 @@ def main(argv=()): port_num = args.port or find_free_port() url_host = f"{host_name}:{port_num}" - # Determine base URL for websocket URL sent to browser. (default to server host/port) + # Determine host, port, and schema to report to the browser when hooking up the + # websocket connection. websocket_host = args.ws_host or host_name websocket_port = args.ws_port if args.ws_port is not None else port_num websocket_https = args.ws_https From 9587be43cb53e11ce3bee63fb764b6d67fc20d98 Mon Sep 17 00:00:00 2001 From: Kye Russell Date: Mon, 3 Nov 2025 12:23:28 +0800 Subject: [PATCH 3/4] fixed incorrecty-styled cli arg doc --- sphinx_autobuild/__main__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sphinx_autobuild/__main__.py b/sphinx_autobuild/__main__.py index 31e1ecb..c057fdf 100644 --- a/sphinx_autobuild/__main__.py +++ b/sphinx_autobuild/__main__.py @@ -294,7 +294,7 @@ def _add_autobuild_arguments(parser): "--ws-https", action="store_true", default=False, - help="tell the browser to use HTTPS/WSS for websocket connections. " + help="tell the browser to use HTTPS/WSS for websocket connections " "(useful when running behind a reverse proxy)", ) return group From d3e606f9e7a3d289e30d87cb86a7846fdf3ae564 Mon Sep 17 00:00:00 2001 From: Kye Russell Date: Mon, 3 Nov 2025 12:37:02 +0800 Subject: [PATCH 4/4] Add docs --- README.rst | 33 ++++++++++++++++++++++++++ sphinx_autobuild/__main__.py | 46 ++++++++++++++++++------------------ 2 files changed, 56 insertions(+), 23 deletions(-) diff --git a/README.rst b/README.rst index 190b9a1..cd4d2cf 100644 --- a/README.rst +++ b/README.rst @@ -59,6 +59,12 @@ which can seen by running ``sphinx-autobuild --help``: autobuild options: --port PORT port to serve documentation on. 0 means find and use a free port --host HOST hostname to serve documentation on + --ws-port WS_PORT port portion of the websocket URL to ask the browser to connect to + (defaults to --port, useful when running behind a reverse proxy) + --ws-host WS_HOST host portion of the websocket URL to ask the browser to connect to + (defaults to --host, useful when running behind a reverse proxy) + --ws-https tell the browser to use HTTPS/WSS for websocket connections + (useful when running behind a reverse proxy) --re-ignore RE_IGNORE regular expression for files to ignore, when watching for changes --ignore IGNORE glob expression for files to ignore, when watching for changes @@ -105,6 +111,33 @@ sphinx-autobuild asks the operating system for a free port number and use that for its server. Passing ``--port=0`` will enable this behaviour. +Using behind a reverse proxy +---------------------------- + +sphinx-autobuild tells the browser to reload by having it initiate a websocket +connection back to the sphinx-autobuild server, through which reload trigger +events are then sent. In some reverse proxy setups, you may find it necessary +to manually speify the host and port to use for the websocket URL sent to the +browser, as distinct from the host and port on which the sphinx-autobuild +server itself is listening. + +To handle this, you can pass ``--ws-host`` and / or ``--ws-port`` to +sphinx-autobuild, which will override the host and port reported to the browser +respectively. + +.. code-block:: bash + + sphinx-autobuild ... --ws-host docs.my-development.proxy.local --ws-port 80 + + +In addition, some reverse proxy setups may require that the browser initiate its +websocket connection using HTTPS (``wss://``) rather than HTTP (``ws://``). To +handle this, pass ``--ws-https`` to sphinx-autobuild. + +.. code-block:: bash + + sphinx-autobuild ... --ws-host docs.my-development.proxy.local --ws-port 443 --ws-https + Workflow suggestions ==================== diff --git a/sphinx_autobuild/__main__.py b/sphinx_autobuild/__main__.py index c057fdf..1de3db3 100644 --- a/sphinx_autobuild/__main__.py +++ b/sphinx_autobuild/__main__.py @@ -220,6 +220,29 @@ def _add_autobuild_arguments(parser): default="127.0.0.1", help="hostname to serve documentation on", ) + group.add_argument( + "--ws-port", + type=int, + default=None, + help="port portion of the websocket URL to ask the browser to " + "connect to (defaults to --port, useful when running behind a " + "reverse proxy)", + ) + group.add_argument( + "--ws-host", + type=str, + default=None, + help="host portion of the websocket URL to ask the browser to " + "connect to (defaults to --host, useful when running behind a " + "reverse proxy)", + ) + group.add_argument( + "--ws-https", + action="store_true", + default=False, + help="tell the browser to use HTTPS/WSS for websocket connections " + "(useful when running behind a reverse proxy)", + ) group.add_argument( "--re-ignore", action="append", @@ -274,29 +297,6 @@ def _add_autobuild_arguments(parser): default=[], help="additional command(s) to run after building the documentation", ) - group.add_argument( - "--ws-host", - type=str, - default=None, - help="host portion of the websocket URL to ask the browser to " - "connect to (defaults to --host, useful when running behind a " - "reverse proxy)", - ) - group.add_argument( - "--ws-port", - type=int, - default=None, - help="port portion of the websocket URL to ask the browser to " - "connect to (defaults to --port, useful when running behind a " - "reverse proxy)", - ) - group.add_argument( - "--ws-https", - action="store_true", - default=False, - help="tell the browser to use HTTPS/WSS for websocket connections " - "(useful when running behind a reverse proxy)", - ) return group