Skip to content

feat(httpserver): add TLS support and socket tuning parameters - #165

Merged
Oaklight merged 5 commits into
masterfrom
worktree-feat+httpserver-tls
Sep 15, 2026
Merged

Oaklight merged 5 commits into
masterfrom
worktree-feat+httpserver-tls

Conversation

@Oaklight

Copy link
Copy Markdown
Owner

Summary

  • Add ssl_context parameter to run() / _serve() / _start_unix_socket() for TLS termination, forwarded as ssl= to asyncio.start_server() / start_unix_server()
  • Add backlog, reuse_address, reuse_port socket tuning parameters, also forwarded to asyncio
  • Update startup log messages to include scheme (http:// vs https://, unix: vs https+unix:)
  • Add 7 tests covering TLS connections, plaintext rejection, POST over TLS, socket tuning, and combined TLS+tuning

API follows the aiohttp pattern — caller passes a pre-built ssl.SSLContext, giving full control over certs, mTLS, ciphers, and ALPN without the server needing to know about any of it.

Closes #162

Test plan

  • All 7 new TLS tests pass (pytest httpserver/test_httpserver_tls.py)
  • All 95 existing correctness tests pass
  • All 8 unix socket tests pass
  • Pre-commit hooks pass (ruff, ruff-format, ty, complexipy)

@elena-oaklight elena-oaklight Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

CI green across lint + 3.10–3.13. Clean API design — passing a pre-built ssl.SSLContext is the right pattern (matches aiohttp/uvicorn). Version bump to 0.5.0 is appropriate for new public parameters. Tests are thorough: TLS handshake, plaintext rejection, POST over TLS, socket tuning, and the combined case. Module-scoped cert fixtures keep the expensive openssl calls from running per-test.

Two actionable items:

1. backlog silently dropped for Unix sockets
_serve() only forwards ssl_context to _start_unix_socket(), but asyncio.start_unix_server() also accepts backlog. A caller passing run(socket="/tmp/app.sock", backlog=2048) gets silent parameter loss. Should forward backlog to the Unix path too. (reuse_address/reuse_port are TCP-only so dropping those is correct.)

2. backlog docstring/type mismatch
Docstring says "None leaves the OS default (typically 128)" but the type annotation is int = 100, so None isn't accepted. Either change the type to int | None = None (and conditionally pass it to asyncio), or fix the docstring to say "Defaults to 100" (which matches asyncio's own default). The current text describes behavior the caller can't reach.

No other issues. Scheme-aware log messages (https://, https+unix:) are a nice touch.

@milo-oaklight milo-oaklight Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Clean API — following the aiohttp pattern of accepting a pre-built SSLContext is the right call for a zero-dep server. TLS forwarding to both TCP and Unix socket paths is correct, socket tuning params are well-documented, and the 7 new tests cover the key combinations. CI green.

Actionable (non-blocking)

1. backlog docstring/type mismatch

The docstring says:

None leaves the OS default (typically 128).

But the type is int = 100 — callers can't actually pass None. Either widen the type to int | None = None (and let asyncio pick its own default), or update the docstring to describe the current 100 default. Applies to both run() and _serve().

2. Socket tuning params silently ignored on Unix socket path

backlog, reuse_address, reuse_port are forwarded to asyncio.start_server() for TCP, but the Unix socket path only receives ssl_context:

server = await self._start_unix_socket(socket, ssl_context=ssl_context)
# ← backlog not forwarded

asyncio.start_unix_server() also accepts backlog, so a caller doing run(socket="/tmp/app.sock", backlog=2048) silently drops the tuning. Either forward backlog to _start_unix_socket(), or document that socket tuning params are TCP-only. (reuse_address/reuse_port don't apply to Unix sockets, so those are fine to skip.)

3. No test for TLS over Unix socket

The code correctly forwards ssl_context to start_unix_server(), but there's no test exercising that path. A single TLS-over-UDS test would close the coverage gap — the _start_unix_socket method already handles socket cleanup and permissions, and TLS adds another layer worth verifying end-to-end.

Minor

  • _generate_self_signed_cert depends on openssl CLI with no skip guard — if CI or a contributor's machine doesn't have it, all 7 tests fail at fixture time. A pytest.importorskip-style check or shutil.which("openssl") skip would be safer.
  • Test cleanup (app.shutdown() + await serve_task) runs after assertions — if an assertion fails, the server leaks. A try/finally would be more robust, though this matches the existing test style.

@clementine-oaklight clementine-oaklight Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review — PR #165: feat(httpserver): add TLS support and socket tuning parameters

CI: ✅ lint + test 3.10–3.13 all pass
Scope: +314/−14, 3 files — TLS via ssl.SSLContext pass-through, plus backlog/reuse_address/reuse_port

Design is clean — the pass-through SSLContext pattern (aiohttp/uvicorn style) keeps the module zero-dependency while giving callers full control over certs, mTLS, ciphers, and ALPN. The kwargs-dict pattern for optional params avoids passing None to asyncio, which is the right approach. reuse_address/reuse_port correctly omitted from the Unix socket path. Version bump to 0.5.0 is appropriate. https+unix: scheme in log output is a nice touch.

Actionable

  1. No test for TLS over Unix socket
    _start_unix_socket() now accepts and forwards ssl_context, but all 7 TLS tests use the TCP path. A test exercising _serve("127.0.0.1", 0, socket="/tmp/test.sock", ssl_context=ctx) would close the gap — especially since this is new code path that can't be verified by the TCP tests.

Non-blocking

  1. asyncio.sleep(0.3) startup race — All 7 tests use a fixed 300ms delay for server readiness. On a slow CI runner or under load, this races. An asyncio.wait_for polling loop on the port would be more robust, though the existing httpserver tests likely use the same pattern, so it's at least consistent.

  2. openssl CLI dependency in tests — _generate_self_signed_cert() shells out to openssl. If it's missing (unlikely on CI, but possible in minimal containers), all TLS tests fail with a subprocess error instead of a clean skip. A pytest.importorskip or pytest.mark.skipif(shutil.which("openssl") is None, ...) would degrade gracefully.

  3. Manifest rebase noise — The diff includes content_hash / last_updated changes for ratelimit and profiler modules, unrelated to this PR. Not blocking, but a clean rebase onto current master before merge would trim the diff.

LGTM with item 1 addressed.

@Oaklight

Copy link
Copy Markdown
Owner Author

Thanks @elena-oaklight and @milo-oaklight — all three items addressed:

  1. backlog type mismatch: changed to int | None = None on run(), _serve(), and _start_unix_socket(). None now correctly lets asyncio use OS defaults. (eee4d4c)
  2. backlog forwarded to Unix socket path: _start_unix_socket() now accepts and forwards backlog to asyncio.start_unix_server(). reuse_address/reuse_port are TCP-only so intentionally not forwarded. (eee4d4c)
  3. TLS-over-Unix-socket test added: new TestTLSUnixSocket::test_tls_over_unix_socket covers the ssl_context forwarding path in _start_unix_socket(). (b9e0f45)

Expose ssl_context, backlog, reuse_address, and reuse_port on run() and
_serve(), forwarded to asyncio.start_server() / start_unix_server().

TLS is opt-in: pass a pre-built ssl.SSLContext to enable HTTPS.  Startup
log messages now include the scheme (http:// vs https://).

Closes #162
- Forward `backlog` to `_start_unix_socket()` so callers using unix
  sockets don't get silent parameter loss.
- Change `backlog` type from `int = 100` to `int | None = None` so
  `None` actually works as the docstring describes.
- Conditionally pass `backlog`/`reuse_address`/`reuse_port` only when
  not None, letting asyncio use its own defaults.
Cover the ssl_context forwarding path in _start_unix_socket(), as
suggested in review.
- Replace asyncio.sleep(0.3) with readiness polling (_wait_ready helper)
  for deterministic startup detection instead of fixed delays.
- Wrap all test bodies in try/finally to ensure server cleanup on
  assertion failure.
- Add ASYNC240 ignore for TLS test file (os.path.exists in readiness
  polling, same as unix socket tests).
@Oaklight
Oaklight force-pushed the worktree-feat+httpserver-tls branch from d4f1b90 to df81edc Compare September 15, 2026 05:12
@Oaklight
Oaklight merged commit 47599c6 into master Sep 15, 2026
6 checks passed
@Oaklight
Oaklight deleted the worktree-feat+httpserver-tls branch September 15, 2026 05:20
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.

httpserver: add TLS support and expose start_server tuning parameters

1 participant