Skip to content

Print correct URL for datasette -p 0 --root by pre-binding the socket#2703

Open
alvinttang wants to merge 1 commit intosimonw:mainfrom
alvinttang:fix/serve-port-0-root-url
Open

Print correct URL for datasette -p 0 --root by pre-binding the socket#2703
alvinttang wants to merge 1 commit intosimonw:mainfrom
alvinttang:fix/serve-port-0-root-url

Conversation

@alvinttang
Copy link
Copy Markdown

@alvinttang alvinttang commented Apr 25, 2026

Summary

Fixes #873 (open since 2018). datasette/cli.py::serve printed the --root auth-token URL (and built the --open URL) using the user-supplied port value before handing off to uvicorn.run(). With -p 0, uvicorn would assign a real port internally, but Datasette had already echoed http://127.0.0.1:0/....

Fix

When port == 0 AND we need the URL upfront (--root or --open) AND not using --uds, pre-bind a TCP socket on (host, 0), read the assigned port via getsockname(), then pass the bound socket to uvicorn via uvicorn.Server(config).run(sockets=[sock]). (uvicorn.run()'s fd= shortcut assumes AF_UNIX, so we use the lower-level Config/Server API on this single branch; the default uvicorn.run() path is untouched for everyone else.) Handles IPv6 hosts. SSL flags preserved.

26 LOC of production change.

Test

tests/test_cli_serve_server.py::test_serve_root_url_uses_actual_port_when_port_is_zero — spawns python -m datasette --memory -p 0 --root as a subprocess, parses the printed auth-token URL with regex, asserts the port is non-zero, then issues an HTTPX request to that port and asserts 200 on /_memory.json.

pytest tests/test_cli.py tests/test_cli_serve_server.py tests/test_cli_serve_get.py tests/test_internals_datasette_client.py → 93 passed. Manual e2e: datasette --memory -p 0 --root now prints http://127.0.0.1:50940/... and lsof confirms the process is listening on the same port.

ruff + black --check clean.

Risk notes

  • SO_REUSEADDR matches uvicorn's own Config.bind_socket. --uds path explicitly excluded. Behaviour for the common case (port != 0 or no --root / --open) is byte-identical.
  • Cosmetic: when we pre-bind, uvicorn logs Uvicorn running on socket ('127.0.0.1', <port>) instead of the http://... line. The URL printed to stdout is correct, which is what the issue is about.

Refs #873


📚 Documentation preview 📚: https://datasette--2703.org.readthedocs.build/en/2703/

Closes simonw#873.

When `datasette -p 0 --root` was used, the printed auth-token URL
contained the literal placeholder port 0 instead of the OS-assigned
port that uvicorn would later bind to. Same applied to `--open`.

Fix: when `port == 0` and we need to print/open a URL before the
server starts (because of --root or --open), pre-bind a TCP socket
on (host, 0), read the assigned port via getsockname(), and hand the
bound socket to uvicorn via Server.run(sockets=[...]). uvicorn.run()'s
own `fd=` parameter assumes AF_UNIX so we use the Config/Server API
in this branch only; the existing uvicorn.run() path is unchanged.

Adds a regression test that launches `datasette --memory -p 0 --root`,
parses the printed URL, asserts the port is non-zero, and confirms a
server is actually listening on that port.
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.

"datasette -p 0 --root" gives the wrong URL

1 participant