Server entrypoints¶
serve¶
serve(app: Application, config: Config | None = None) -> None
Start the server. This is the primary programmatic entrypoint and matches the behavior of the h2corn CLI. For an in-process lifecycle, use Server.
On Unix, the call goes through the multi-worker supervisor, even for workers=1, so signal handling, inherited listeners, and worker lifecycle stay identical to the CLI. On Windows, a single in-process worker is used.
Blocks until the server is asked to shut down via one of:
| Signal | Effect |
|---|---|
SIGINT / SIGTERM | Graceful shutdown |
SIGHUP | Rolling worker reload |
SIGTTIN | Scale workers up by one |
SIGTTOU | Scale workers down by one |
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
app | Application | An ASGI 3 application callable. | required |
config | Config | None | Server configuration. Defaults to | None |
Examples:
examples/embedded.py shows the Server alternative: one worker inside an event loop you already own, rather than the supervisor this starts.
Server¶
Server(app: Application, config: Config | None = None)
In-process, single-worker ASGI server.
Use this when you want to embed h2corn inside an existing event loop — for example, inside a test harness, a custom CLI, or an application that manages its own process model. For ordinary deployments, prefer serve, which goes through the multi-worker supervisor and matches the h2corn CLI.
The configured bind listeners are opened, lifespan startup runs, then the server processes requests until shutdown() is called or the surrounding task is cancelled.
Config describes a whole deployment, so some of its settings belong to the supervisor that runs several workers rather than to one server. workers is refused outright — asking for eight and getting one is not a detail — while max_requests, max_requests_jitter and timeout_worker_healthcheck only say when a supervisor would replace a worker, so serve() warns and carries on.
Examples:
examples/embedded.py runs one worker on an ephemeral port, sends a request through it, and shuts it down.
addressesproperty¶
addresses: tuple[str, ...]
Resolved listener addresses, in Config.bind string form.
Empty until serve() has bound the listeners. Unlike Config.bind, these carry the port the kernel actually assigned — bind to port 0 and read the address back from here:
Illustrative (not executable):
server = Server(app, Config(bind=('127.0.0.1:0',)))
serving = asyncio.create_task(server.serve())
await server.wait_started()
host, _, port = server.addresses[0].rpartition(':')
releasingproperty¶
releasing: bool
Whether a previous serve() is still releasing its requests.
serve() stops waiting once the graceful budget is spent, but requests that ignored cancellation keep the application and its loop shards alive. While this is true the same Server cannot serve() again. Lifespan shutdown runs only after those requests finally release.
serveasync¶
serve() -> None
Run the server until shutdown.
Binds the configured listeners, runs ASGI lifespan startup, processes requests, then runs lifespan shutdown after the native drain has fully released the application.
One lifecycle at a time, reusable afterwards: once a serve() call has fully finished, the same Server can serve() again with fresh shutdown state, while a second concurrent call raises RuntimeError. Cancelling the serve() task does not abort in-flight work — it triggers the same bounded graceful drain as shutdown() and re-raises the cancellation once that drain finishes or its budget runs out.
The budget is real: an application that catches CancelledError and never finishes cannot hold serve() open. serve() raises RuntimeError instead, naming the problem. Lifespan shutdown still runs after those requests finally release; until then the same Server cannot serve() again.
Raises NotImplementedError if Config.workers is not 1; multi-worker deployments must go through serve.
shutdown¶
shutdown() -> None
Initiate a graceful shutdown of an in-flight serve() call.
Safe to call from any thread or coroutine. Native acceptance stops immediately and cooperative tasks receive up to Config.timeout_graceful_shutdown seconds to finish. Arbitrary synchronous Python code cannot be preempted in-process; use supervised workers when a hard kill bound is required.
wait_startedasync¶
wait_started() -> None
Wait until this server is accepting connections.
Resolves once the listeners are live, so a caller that starts serve() as a task can await this instead of watching addresses for one to appear.
Raises rather than waiting when waiting cannot end well: whatever stopped the server if it stops without ever serving, and RuntimeError if it is already shutting down — a server on its way down is not one that is about to come up.
Illustrative (not executable):
serving = asyncio.create_task(server.serve())
await server.wait_started()
# the port is open here