HttpServer
struct HttpServerA non-blocking reactor HTTP server: HTTP/1.1, HTTP/2, HTTP/3, TLS.
One serve call runs an event loop over kqueue / epoll
(opt-in io_uring on Linux), so many connections are in flight at
once rather than one per calling thread. Each accepted cleartext
connection is dispatched by peeking its first 24 bytes: the RFC 9113
§3.4 client preface selects the HTTP/2 handle, anything else the
HTTP/1.1 one. A server built by :meth:bind_tls handshakes on the
reactor and dispatches by ALPN instead. HTTP/3 has its own listener
and its own entry point, :meth:serve_http3.
num_workers >= 2 runs one reactor per worker thread, each with
its own copy of the handler, which is why that arity additionally
requires Copyable.
Reads are buffered (default 8KB chunks) for efficient I/O. HTTP/1.1 keep-alive is enabled by default. Recv/send timeouts are set on accepted sockets to prevent DoS.
This type is Movable but not Copyable.
Example:
def handle(req: Request) raises -> Response:
return Response(Status.OK, body="hello".as_bytes())
var srv = HttpServer.bind(SocketAddr.localhost(8080))
srv.serve(handle)
Implements Movable
- Fields
config: ServerConfig
__init__
def __init__(out self, var listener: TcpListener, var config: ServerConfig = ServerConfig(Int(8192), Int(8192), Int(10485760), Int(8192), True, Int(1000), Int(500), Int(5000), Int(5000), False, Int(30000), Int(30000), Int(60000), False, False, H1LeniencyConfig(allow_lf_only_line_endings=False, allow_mixed_case_method=False, allow_ows_around_colon=False, allow_obs_fold=False, allow_oversized_request_uri=False, allow_oversized_header_list=False, accept_empty_chunk_extensions=False, allow_multiple_content_length=False, allow_te_chunked_when_cl_present=False, accept_obs_text_in_field_value=False, accept_invalid_chunk_extension_chars=False, allow_leading_whitespace_before_request_line=False), Int(0), WsUpgrade(), Http2Config())) __deinit__
def __deinit__(deinit self) bind
def bind(addr: SocketAddr, var config: ServerConfig = ServerConfig(Int(8192), Int(8192), Int(10485760), Int(8192), True, Int(1000), Int(500), Int(5000), Int(5000), False, Int(30000), Int(30000), Int(60000), False, False, H1LeniencyConfig(allow_lf_only_line_endings=False, allow_mixed_case_method=False, allow_ows_around_colon=False, allow_obs_fold=False, allow_oversized_request_uri=False, allow_oversized_header_list=False, accept_empty_chunk_extensions=False, allow_multiple_content_length=False, allow_te_chunked_when_cl_present=False, accept_obs_text_in_field_value=False, accept_invalid_chunk_extension_chars=False, allow_leading_whitespace_before_request_line=False), Int(0), WsUpgrade(), Http2Config())) -> Self Bind an HTTP server on addr.
- Args
addr: Local address to listen on.config: HTTP/1.1 server configuration (optional).
Raises AddressInUse: If the port is already bound. NetworkError: For any other OS error.
def bind(var addrs: List[SocketAddr], var config: ServerConfig = ServerConfig(Int(8192), Int(8192), Int(10485760), Int(8192), True, Int(1000), Int(500), Int(5000), Int(5000), False, Int(30000), Int(30000), Int(60000), False, False, H1LeniencyConfig(allow_lf_only_line_endings=False, allow_mixed_case_method=False, allow_ows_around_colon=False, allow_obs_fold=False, allow_oversized_request_uri=False, allow_oversized_header_list=False, accept_empty_chunk_extensions=False, allow_multiple_content_length=False, allow_te_chunked_when_cl_present=False, accept_obs_text_in_field_value=False, accept_invalid_chunk_extension_chars=False, allow_leading_whitespace_before_request_line=False), Int(0), WsUpgrade(), Http2Config())) -> Self Bind an HTTP server on multiple addresses simultaneously.
The list overload of :meth:bind. bind takes either a
single address or a list (the pre-0.11 bind_many was removed
in v0.12).
Each address gets its own TcpListener fd; the unified
reactor loop accepts on all of them and dispatches each
accepted connection through the same handler. Useful for
binding the same service on both IPv4 and IPv6, on
multiple ports for split traffic classes (e.g. internal
admin port + public service port), or on a UNIX socket
plus a TCP socket sharing one process (after the upcoming
UdsListener integration).
addrs must be non-empty. The first address is the
"primary" listener (used by local_addr() and any
legacy single-listener call site); the remainder become
extras. All addresses bind in order before any returns,
so a partial-bind failure leaves no half-bound state
(already-bound listeners are dropped + closed by the
TcpListener.__deinit__).
Composes with multi-worker serving: serve(handler, num_workers=M) on a multi-address server gives N addresses
x M workers, where each pairing owns its own
SO_REUSEPORT listener (N*M in total, bound serially on
the scheduler thread). The listeners created here are closed
first, because SO_REUSEPORT only composes with other
SO_REUSEPORT sockets and these are plain binds.
Still single-listener: serve_streaming, which raises when
extras are present rather than leaving an address unserved.
Example:
var srv = HttpServer.bind(
[SocketAddr.localhost(8080), SocketAddr.localhost(8081)],
)
srv.serve(handler)
- Args
addrs: One or more local addresses to listen on. Order matters:addrs[0]is the primary.config: HTTP/1.1 server configuration (optional).
Raises Error: If addrs is empty.
AddressInUse: If any port is already bound.
NetworkError: For any other OS error.
local_addrs
def local_addrs(self) -> List[SocketAddr] Return the bound addresses, primary first then extras in the order they were passed to :meth:bind. Always returns at least one entry.
bind_with_http3
def bind_with_http3(tcp_addr: SocketAddr, var udp_cfg: QuicServerConfig, var config: ServerConfig = ServerConfig(Int(8192), Int(8192), Int(10485760), Int(8192), True, Int(1000), Int(500), Int(5000), Int(5000), False, Int(30000), Int(30000), Int(60000), False, False, H1LeniencyConfig(allow_lf_only_line_endings=False, allow_mixed_case_method=False, allow_ows_around_colon=False, allow_obs_fold=False, allow_oversized_request_uri=False, allow_oversized_header_list=False, accept_empty_chunk_extensions=False, allow_multiple_content_length=False, allow_te_chunked_when_cl_present=False, accept_obs_text_in_field_value=False, accept_invalid_chunk_extension_chars=False, allow_leading_whitespace_before_request_line=False), Int(0), WsUpgrade(), Http2Config())) -> Self Bind an HTTP server that speaks h1 / h2c / h2 over TCP on tcp_addr AND h3 over QUIC/UDP on the address in udp_cfg.
The TLS ALPN list advertised by the TCP listener (h2,
http/1.1) and the QUIC listener (h3 only) is what tells
peers which wire is reachable on which transport. The
decision function
:func:flare.http.alpn_dispatch.dispatch_alpn routes the
negotiated ALPN identifier to the matching driver:
"h3"-> the QUIC listener / :class:Http3Connection."h2"-> the TCP h2 reactor / :class:Http2ConnHandle."http/1.1"/ empty -> the TCP h1 reactor / :class:ConnHandle.- h2c upgrade hint -> H2C (TCP path only).
The two wires have two entry points. :meth:serve drives the
TCP reactor only; the UDP listener is driven by
:meth:serve_http3, so a caller serving both spawns one OS
thread per loop. The UDP listener is also reachable via
:meth:local_http3_addr / :meth:tick_http3_once for tests
that want to drive the h3 path without spinning up the full
reactor. Closing the server (via :meth:close or
__deinit__) closes both listeners.
- Args
tcp_addr: Local TCP address for h1 / h2c / h2.udp_cfg: :class:QuicServerConfigfor the h3 UDP bind.udp_cfg.host/udp_cfg.portapply to the QUIC listener; the rest of the config (CC choice, idle timeout, ...) is passed through.config: HTTP/1.1 server configuration (optional).
Raises AddressInUse: If either port is already bound. NetworkError: For any other OS error.
bind_tls
def bind_tls(addr: SocketAddr, cert_file: String, key_file: String, var alpn: List[String] = List(), var config: ServerConfig = ServerConfig(Int(8192), Int(8192), Int(10485760), Int(8192), True, Int(1000), Int(500), Int(5000), Int(5000), False, Int(30000), Int(30000), Int(60000), False, False, H1LeniencyConfig(allow_lf_only_line_endings=False, allow_mixed_case_method=False, allow_ows_around_colon=False, allow_obs_fold=False, allow_oversized_request_uri=False, allow_oversized_header_list=False, accept_empty_chunk_extensions=False, allow_multiple_content_length=False, allow_te_chunked_when_cl_present=False, accept_obs_text_in_field_value=False, accept_invalid_chunk_extension_chars=False, allow_leading_whitespace_before_request_line=False), Int(0), WsUpgrade(), Http2Config())) -> Self Bind an HTTPS (TLS-terminated HTTP/1.1) server on addr.
Loads cert_file / key_file into a server SSL_CTX and
stashes it on the returned server; :meth:serve then drives
each accepted connection through a non-blocking
:class:flare.http._reactor.tls_conn_handle.TlsConnHandle
(handshake + SSL_read / SSL_write) before dispatching the
decrypted request to the handler. The plaintext reactor path is
untouched.
- Args
addr: Local address to listen on.cert_file: Path to the PEM server certificate (chain).key_file: Path to the PEM server private key.alpn: ALPN protocol identifiers to advertise, in preference order (e.g.["h2", "http/1.1"]). Empty (the default) advertises no ALPN and serves HTTP/1.1. Offeringh2is served: the connection is handshaken on the reactor and the negotiated identifier selects the HTTP/2 or HTTP/1.1 handle.config: HTTP/1.1 server configuration (optional).
Raises AddressInUse: If the port is already bound. NetworkError: For any other OS error. Error: If the cert / key fails to load.
def bind_tls(addr: SocketAddr, var tls: TlsServerConfig, var config: ServerConfig = ServerConfig(Int(8192), Int(8192), Int(10485760), Int(8192), True, Int(1000), Int(500), Int(5000), Int(5000), False, Int(30000), Int(30000), Int(60000), False, False, H1LeniencyConfig(allow_lf_only_line_endings=False, allow_mixed_case_method=False, allow_ows_around_colon=False, allow_obs_fold=False, allow_oversized_request_uri=False, allow_oversized_header_list=False, accept_empty_chunk_extensions=False, allow_multiple_content_length=False, allow_te_chunked_when_cl_present=False, accept_obs_text_in_field_value=False, accept_invalid_chunk_extension_chars=False, allow_leading_whitespace_before_request_line=False), Int(0), WsUpgrade(), Http2Config())) -> Self Bind an HTTPS server from a full :class:TlsServerConfig.
The context is built by :class:TlsAcceptor, so everything the
config carries applies to the reactor's handshakes: ALPN, client
certificate verification (mTLS), the minimum protocol version and
the session-ticket policy (off unless enabled). The cert/key form
of :meth:bind_tls reached none of these. A misconfiguration
raises here, at bind time.
reload_tls
def reload_tls(self) Re-read every certificate and key file the server was bound with and serve them to new handshakes. Safe to call from another thread while :meth:serve runs; connections already up keep their certificate. A pair that fails to load raises, and that pair stays as it was.
has_http3
def has_http3(self) -> Bool Whether this server has an h3 UDP listener bound.
local_http3_addr
def local_http3_addr(self) -> SocketAddr Return the local address of the h3 UDP listener.
Raises Error: If no h3 listener is bound.
advertised_alpn_protocols
def advertised_alpn_protocols(self) -> List[String] Return the ALPN identifier list this server expects to advertise on its TLS handshakes. The TCP listener advertises ["h2", "http/1.1"] and the QUIC listener advertises ["h3"]; here we surface the union (for diagnostics + the alpn_dispatch_demo example).
Order matters: server preference is highest -> lowest,
which :func:flare.http.alpn_dispatch.negotiate_alpn
consumes verbatim.
route_alpn
def route_alpn(self, alpn: String) -> Int Map a negotiated ALPN identifier to a :class:flare.http.alpn_dispatch.WireProtocol codepoint, cross-checked against which listeners this server has bound. "h3" routes to WireProtocol.HTTP_3 only when this server has an h3 listener; otherwise the decision raises so the reactor can close the connection with no_application_protocol.
- Args
alpn: The ALPN identifier returned by the TLS handshake (empty string == "no ALPN advertised").
Raises Error: If alpn == "h3" but no h3 listener is
bound.
tick_http3_once
def tick_http3_once(mut self, now_ms: UInt64) -> Int Advance the h3 listener's timer wheel one tick. Test- only entry point used to validate the bind path; returns the number of connections still alive after the sweep.
Raises Error: If no h3 listener is bound.
pump_http3_handler_once
def pump_http3_handler_once[H: Handler & Copyable](mut self, mut handler: H) -> Int Drain every connection's H3 dispatcher once: for each completed request stream the handler is invoked with the materialized :class:Request, the resulting :class:Response is encoded into the slot's H3 outbox via :meth:QuicListener.emit_http3_response, and the outbound bytes accumulate in the per-(slot, stream_id) egress buffer.
Returns the number of (slot, stream) pairs dispatched this pass. Zero when no H3 request is ready. The buffered bytes leave the wire via the 1-RTT STREAM egress drain once the slot's 1-RTT keys are installed.
Raises Error: If no h3 listener is bound.
serve_http3
def serve_http3[H: Handler & Copyable](mut self, var handler: H) Run the QUIC reactor with H3 handler dispatch as a single-threaded loop.
This is the H3-aware blocking entry point: each iteration
runs :meth:QuicListener.tick to drain one inbound UDP
datagram + drive the QUIC + rustls state machines, then
:meth:pump_http3_handler_once to dispatch any completed
H3 request streams through handler, then
:meth:QuicListener.advance_timers so PTO + idle +
ack-delay callbacks fire on time. Exits cleanly once
the h3 listener's stop flag flips
(:meth:QuicListener.shutdown).
The serve-loop pairs the TCP unified reactor (the
canonical :meth:serve overloads above) as a peer entry
point; callers running both wires spawn one OS thread per
loop.
Raises Error: If no h3 listener is bound.
NetworkError: On fatal listener errors;
per-connection errors close the offending
connection silently inside tick.
attach_ws_h3
def attach_ws_h3[W: WsH3Handler](mut self, var ws_handler: W) Serve WebSocket over HTTP/3 (RFC 9220) through ws_handler: extended CONNECT is advertised, and :protocol: websocket requests become tunnels handed to it while :meth:serve_http3's handler serves everything else. Another :protocol gets 501. Call after :meth:bind_with_http3 and before :meth:serve_http3; each worker serves its own copy.
serve
def serve(mut self, handler: def(Request) raises thin -> Response, num_workers: Int = Int(1), pin_cores: Bool = True) Run the reactor loop, calling handler per request.
Plain-function overload: pass a def(Request) raises -> Response
and the server wraps it in a FnHandler internally. This is
the -compatible shape; the argument list is extended with
num_workers / pin_cores to match the Handler-typed
overload below so every user has one entry point to learn.
num_workers == 1(default): single-threaded reactor (kqueue on macOS, epoll on Linux). Same hot path as theserve.num_workers >= 2: multicore — N workers viaflare.runtime.scheduler.Scheduler. By default each worker binds its ownSO_REUSEPORTlistener (the kernel hashes new 4-tuples to one of N listeners; matches actix_web's listener strategy and gives the highest steady-state throughput). ExportFLARE_REUSEPORT_WORKERS=0before launch to switch back to the single shared listener withEPOLLEXCLUSIVE(Linux >= 4.5), which trades 7-22 % req/s (handler vs static fast path) for a uniformly tighter p99.99 σ under sustained load; seedocs/benchmark.md. The exclusive wakeup goes to one waiting worker per event, so connections are not spread evenly: a lightly loaded server may keep landing them on the same worker (seeReactor.register_exclusive).
For Router / middleware / stateful-struct handlers, use the
Handler-typed overload serve[H: Handler & Copyable].
- Args
handler: Called once per parsed request.num_workers: Worker count.<= 0is coerced to 1. Values > 256 are rejected (seeScheduler.start).pin_cores: On Linux, pin worker N to coreN % num_cpus. Ignored whennum_workers == 1. No-op on macOS.
Raises NetworkError: On fatal listener errors; per-connection errors
close the offending connection silently.
Error: If a worker fails to spawn when
num_workers >= 2.
def serve[H: Handler](mut self, var handler: H) Run the single-worker reactor loop with any Handler.
The arity-1 overload that accepts Handler-only types
without requiring Copyable. This is the right entry
point for Router (which carries heap-allocated boxed
struct handlers and is not safely Copyable for every
struct shape), middleware-wrapping handler chains whose
innermost element is a Router, or any other
Handler-only struct.
For multi-worker mode (num_workers >= 2), the handler
type must be Copyable because each worker gets its
own H.copy(). Use the parametric serve[H: Handler & Copyable](handler, num_workers, pin_cores) overload
below for that.
- Args
handler: The request handler (ownership transferred).
Raises NetworkError: On fatal listener errors; per-connection errors close the offending connection silently.
def serve[H: Handler & Copyable](mut self, var handler: H, num_workers: Int, pin_cores: Bool = True) Run the multi-worker reactor loop with a Copyable Handler.
Each worker gets its own H.copy() and runs an independent
reactor on its own thread; they share a single listener fd
via flare.runtime.scheduler.Scheduler. Copyable is
required here because of the per-worker copy.
num_workers == 1: routed back to the single-worker overload; this overload'sCopyableconstraint is stricter than necessary but the dispatch is still correct.num_workers >= 2: multicore — N workers sharing a single listener fd viaflare.runtime.scheduler.Scheduler.
- Args
handler: The request handler (ownership transferred).num_workers: Worker count.<= 0is coerced to 1. Values > 256 are rejected (seeScheduler.start).pin_cores: On Linux, pin worker N to coreN % num_cpus. Ignored whennum_workers == 1. No-op on macOS.
Raises NetworkError: On fatal listener errors; per-connection errors
close the offending connection silently.
Error: If a worker fails to spawn when
num_workers >= 2.
attach_ws_h2
def attach_ws_h2[W: WsH2Handler](mut self, var ws_handler: W) Bridge RFC 8441 Extended CONNECT tunnels to ws_handler.
Serves ordinary HTTP through whatever handler :meth:serve is
given and, on the same HTTP/2 server, routes :protocol= websocket tunnels to this sidecar. Advertising
SETTINGS_ENABLE_CONNECT_PROTOCOL is turned on for you.
Call before :meth:serve. One instance serves every tunnel on a
worker; with several workers each serves its own copy. A
single-worker server bound to several addresses cannot carry it
(serve raises).
Changed in v0.11. This replaces the two-handler
serve(handler, ws_handler) overload (removed in v0.12). The old shape made a
WebSocket sidecar look like an argument to serving, when it is a
property of the server -- the same mistake the h2_config
argument made.
- Args
ws_handler: The WS-over-h2 sidecar (ownership transferred).
serve_streaming
def serve_streaming[H: StreamHandler](mut self, var handler: H, max_in_flight: Int = Int(0), retry_after_s: Int = Int(1)) Run the single-threaded streaming reactor loop.
The typed-streaming counterpart of serve: instead of a
request/response Handler, it drives a StreamHandler
through the per-connection lifecycle (on_open /
on_writable / on_upstream / on_close) over the
reactor, owning the EPOLLOUT-driven outbound drain so the front
never blocks the event loop. Use this for multiplexing /
streaming fronts (e.g. a streaming proxy).
Single-listener, single-worker. Extra bind(List[SocketAddr]) listeners
and multi-worker mode are not supported here yet.
- Args
handler: The streaming front (ownership transferred). One instance services every connection.max_in_flight: Admission cap;0(default) = unlimited. When the live-connection count is at the cap, a new connection is refused with a 503 +Retry-Afterinstead of hanging in the accept backlog.retry_after_s:Retry-Afterseconds advertised in the 503.
Raises NetworkError: On fatal listener / reactor errors; per-connection errors close the offending connection.
def serve_streaming[H: StreamHandler & Copyable](mut self, var handler: H, num_workers: Int, pin_cores: Bool = True, max_in_flight: Int = Int(0), retry_after_s: Int = Int(1)) Multi-worker streaming reactor (N workers over the scheduler).
The :trait:StreamHandler twin of serve(handler, num_workers):
each worker runs its own copy of handler on its own reactor +
connection table (state is fully per-worker; the H: Copyable
bound clones the handler once per worker before spawn). Listener
sharing (per-worker SO_REUSEPORT vs one EPOLLEXCLUSIVE fd) is the
scheduler's decision via FLARE_REUSEPORT_WORKERS.
num_workers <= 1 falls through to the single-worker loop.
Single-listener only: extra bind(List[SocketAddr]) listeners are rejected
(multi-worker uses SO_REUSEPORT, not multi-address).
serve_cancellable
def serve_cancellable[CH: CancelHandler](mut self, var handler: CH) Run the cancel-aware reactor loop with a CancelHandler.
Single-threaded entry point. The reactor allocates one
CancelCell per connection, hands a Cancel handle bound
to it into handler.serve(req, cancel), and flips the cell
on:
CancelReason.PEER_CLOSED-- peer FIN observed before the response was queued.CancelReason.TIMEOUT-- idle-timeout driven.CancelReason.SHUTDOWN-- listener stop requested.
For plain Handlers that don't observe cancellation, wrap
with WithCancel[H](inner=h) to plug them into this entry
point unchanged. This is the shared-middleware path for the
cancel-aware reactor: a full Handler middleware stack over a
Router composes directly, because every middleware layer is
itself a Handler and WithCancel bridges the whole stack
in one wrap (cancel-awareness composes with routing)::
var r = Router()
r.get("/", home)
var stack = Logger(RequestId(r^), prefix="[app]")
var srv = HttpServer.bind(SocketAddr.localhost(8080))
srv.serve_cancellable(WithCancel(stack^))
To run the same stack inside a streaming front (one process,
one logging/auth/routing stack behind both a REST control plane
and a token-streaming endpoint), call stack.serve(req) in the
StreamHandler and frame the result with
StreamConn.send_response (see
:mod:flare.http.streaming_server).
- Args
handler: Cancel-aware request handler (ownership transferred).
Raises NetworkError: On fatal listener errors.
Error: If a TLS context is bound, or bind was given several
addresses (these loops serve one listener; use serve).
serve_view
def serve_view[VH: ViewHandler](mut self, var handler: VH) Run the view-aware reactor loop with a ViewHandler.
Single-threaded entry point. Per-request the reactor:
- Reads bytes into
ConnHandle.read_buf. - Parses the request as a
RequestViewborrowing intoread_buf(no per-header String alloc, no body copy). - Dispatches into
handler.serve_view(view, cancel)—view.body()returnsSpan[UInt8, origin]directly. - Serialises the response and resets
read_buffor the next pipelined request.
Use this entry point for handlers that benefit from
zero-copy reads — multipart upload parsers, large-body
echos, anything that scans the body without re-encoding
it. For Handler.serve(req: Request) plug-in,
wrap with WithViewCancel[H](inner=h) (the adapter
does view.into_owned() and forwards).
- Args
handler: View-aware request handler (ownership transferred).
Raises NetworkError: On fatal listener errors.
Error: If a TLS context is bound, or bind was given several
addresses (these loops serve one listener; use serve).
serve_static
def serve_static(mut self, resp: StaticResponse, num_workers: Int = Int(1), pin_cores: Bool = True) Run the reactor loop in static-response mode.
Every parsed request — regardless of path, method, or body — is
answered with the pre-encoded resp bytes. The reactor:
- Reads until the end of the headers (
\r\n\r\n). - Consumes the declared
Content-Lengthbytes and discards them (noRequeststruct, no handler call). - Writes
resp.keepalive_bytesorresp.close_bytesinto the write queue in a singlememcpy, then returns the socket to readable-interest for the next pipelined request.
Intended for health-check endpoints, TFB plaintext benchmarks,
and any workload where the response body is genuinely static.
For heterogeneous routes that happen to share static bodies,
combine serve_static under a reverse-proxy router upstream
of the flare process.
- Args
resp: Pre-encoded static response fromprecompute_response(...).num_workers: Reactor workers. Above 1 the response is served bynum_workersthreads, each with its own listener.pin_cores: Pin worker N to core N % num_cpus (Linux only).
Raises NetworkError: On fatal listener errors; per-connection
errors close the offending connection silently.
Error: If a TLS context is bound, or bind was given several
addresses (these loops serve one listener; use serve).
local_addr
def local_addr(self) -> SocketAddr Return the local address the server is bound to.
close
def close(mut self) Stop accepting new connections and break the reactor loop.
Hard stop. In-flight handlers may be cut mid-write — there
is no wait. Use drain(timeout_ms) for a graceful tear-down.
Idempotent. The loop finishes processing in-flight events before returning; a concurrent caller from another thread can use this to request graceful shutdown (the reactor's wakeup fd will be notified automatically next iteration).
drain
def drain(mut self, timeout_ms: Int) -> ShutdownReport Graceful shutdown.
Closes the listening socket so no new connections are accepted,
keeps the reactor running for timeout_ms milliseconds so
in-flight requests and responses can finish, then breaks the
reactor loop. When the loop stops it flips Cancel.SHUTDOWN
on whatever is still live and closes it.
Wires ServerConfig.shutdown_timeout_ms into a real
wait-for-drain loop.
Notes:
drain is called from another thread than the one
running serve(), and it blocks the caller for the whole
timeout_ms -- it cannot return early when the last
connection finishes, because the reactor publishes no
live-connection count on this path (idle keep-alive
connections would also count as live). Pass the longest
wait you can tolerate.
The single-threaded reactor cannot preempt a synchronous
handler -- the handler runs to completion even if it
ignores Cancel.SHUTDOWN. Cancel-aware handlers
(CancelHandler) observe the CancelReason.SHUTDOWN
flip and short-circuit on their next cancel.cancelled()
poll once the window has elapsed.
- Args
timeout_ms: Milliseconds to keep serving in-flight connections.0is a hard stop (equivalent toclose()). Negative values are clamped to0.
Raises NetworkError: If the listener cannot be closed.