flare_mlake · API · http

flare.http.server

HTTP/1.1 server with buffered reads, keep-alive, and per-connection handler callbacks.

Key performance characteristics:

  • Reads from the socket in chunks (configurable, default 8KB) instead of byte-at-a-time.
  • Scans for the header terminator (CRLFCRLF) in the buffer before parsing.
  • Supports HTTP/1.1 keep-alive (reuses connections for multiple requests).
  • Serialises the full response into a single buffer for one write_all call.
  • Sets recv/send timeouts on accepted sockets for DoS resilience.
  • Respects HTTP/1.0 close-by-default semantics.

Structs

HttpServer

struct HttpServer

A 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:QuicServerConfig for the h3 UDP bind. udp_cfg.host / udp_cfg.port apply 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. Offering h2 is 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 the serve.
  • num_workers >= 2: multicore — N workers via flare.runtime.scheduler.Scheduler. By default each worker binds its own SO_REUSEPORT listener (the kernel hashes new 4-tuples to one of N listeners; matches actix_web's listener strategy and gives the highest steady-state throughput). Export FLARE_REUSEPORT_WORKERS=0 before launch to switch back to the single shared listener with EPOLLEXCLUSIVE (Linux >= 4.5), which trades 7-22 % req/s (handler vs static fast path) for a uniformly tighter p99.99 σ under sustained load; see docs/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 (see Reactor.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. <= 0 is coerced to 1. Values > 256 are rejected (see Scheduler.start).
pin_cores: On Linux, pin worker N to core N % num_cpus. Ignored when num_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's Copyable constraint is stricter than necessary but the dispatch is still correct.
  • num_workers >= 2: multicore — N workers sharing a single listener fd via flare.runtime.scheduler.Scheduler.
Args
handler: The request handler (ownership transferred).
num_workers: Worker count. <= 0 is coerced to 1. Values > 256 are rejected (see Scheduler.start).
pin_cores: On Linux, pin worker N to core N % num_cpus. Ignored when num_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-After instead of hanging in the accept backlog.
retry_after_s: Retry-After seconds 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:

  1. Reads bytes into ConnHandle.read_buf.
  2. Parses the request as a RequestView borrowing into read_buf (no per-header String alloc, no body copy).
  3. Dispatches into handler.serve_view(view, cancel) — view.body() returns Span[UInt8, origin] directly.
  4. Serialises the response and resets read_buf for 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:

  1. Reads until the end of the headers (\r\n\r\n).
  2. Consumes the declared Content-Length bytes and discards them (no Request struct, no handler call).
  3. Writes resp.keepalive_bytes or resp.close_bytes into the write queue in a single memcpy, 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 from precompute_response(...).
num_workers: Reactor workers. Above 1 the response is served by num_workers threads, 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. 0 is a hard stop (equivalent to close()). Negative values are clamped to 0.

Raises NetworkError: If the listener cannot be closed.