Reference
API reference
Every public type, function and value, in the package it comes from. Straight from the interfaces and the doc comments in the source.
The facade package, moonbitstack/moonasgi, re-exports all ten of these under one name; each declaration is documented below in the package that defines it.
scripts/gen_api.py reads each package's pkg.generated.mbti and the /// comments beside the code, then writes this page. If a declaration changes and the page does not, the build fails.
moonasgi/spec
60 itemsThe contract itself: the three scopes, the event set, the extensions, and the legacy 2.0 wrappers. Everything below is built on these types.
suberrorClosed
A message sent on a connection that has already closed (HTTP and WebSocket spec 2.4): the client went away, or the response or the WebSocket was already finished. A server raises it from send. An application that keeps sending after a disconnect catches it and stops; a server need not log it as a failure. The text says which connection, for whoever reads the log.
pub(all) suberror Closed {
Closed(String)
} derive(Eq, @debug.Debug)enumAsgiApplication
An application in either calling convention — the current 3.0 single-callable form or the legacy 2.0 double-callable form. A server holds one of these and normalizes it with guarantee_single_callable; the enum is the typed, reflection-free way to name which convention an application is written in.
pub(all) enum AsgiApplication {
Single(async (Scope, async () -> Event, async (Event) -> Unit) -> Unit)
Legacy((Scope) -> async (async () -> Event, async (Event) -> Unit) -> Unit)
}structAsgiVersion
The asgi sub-dict every scope carries: the ASGI protocol version ("3.0") and the spec_version of the concrete http / websocket / lifespan sub-spec the server implements. Applications negotiate optional behaviour against spec_version with at_least.
pub(all) struct AsgiVersion {
version : String
spec_version : String
} derive(Eq)pub fn AsgiVersion::at_least(Self, major~ : Int32, minor~ : Int32) -> Bool
Strict spec_version negotiation: is this scope's sub-spec at least major.minor? Parses the dotted spec_version (non-digit runs count as 0) and compares numerically, so "2.4".at_least(major=2, minor=3) is true. An application guards a version-gated feature with this instead of string equality.
pub fn AsgiVersion::http() -> Self
The HTTP handshake: protocol 3.0, HTTP sub-spec 2.5 — the www sub-spec's current revision, whose own history runs 2.1 websocket accept headers, 2.2 a None server port, 2.3 a websocket close reason, 2.4 send() raising on a closed connection, 2.5 a websocket disconnect reason. Of those, 2.4 is the one this seam cannot express: see the note in the README.
pub fn AsgiVersion::lifespan() -> Self
The Lifespan handshake: protocol 3.0, lifespan sub-spec 2.0 (the revision that carries state). Lifespan versions independently of http/websocket, so a lifespan scope must not borrow the http spec_version.
pub fn AsgiVersion::websocket() -> Self
The WebSocket handshake: protocol 3.0, WebSocket sub-spec 2.5 — the revision that adds reason to the disconnect event, alongside state and the websocket.http.response denial extension.
enumEvent
A protocol event flowing between server and application. Replaces ASGI's stringly-typed message dicts with a typed sum covering the http / websocket / lifespan message sets in both directions, including the standard extension messages (server push, path-send, zero-copy send, response trailers, early hints, debug, and WebSocket denial with a full HTTP response).
pub(all) enum Event {
HttpRequest(body~ : Bytes, more_body~ : Bool)
HttpDisconnect
HttpResponseStart(status~ : Int32, headers~ : Array[(String, String)], trailers~ : Bool)
HttpResponseBody(body~ : Bytes, more_body~ : Bool)
HttpResponseTrailers(headers~ : Array[(String, String)], more_trailers~ : Bool)
HttpResponsePush(path~ : String, headers~ : Array[(String, String)])
HttpResponsePathSend(path~ : String)
HttpResponseZeroCopySend(fd~ : Int32, offset~ : Int32?, count~ : Int32?, more_body~ : Bool)
HttpResponseDebug(info~ : Json)
HttpResponseEarlyHint(links~ : Array[String])
WebSocketConnect
WebSocketReceive(text~ : String?, bytes~ : Bytes?)
WebSocketDisconnect(code~ : Int32, reason~ : String?)
WebSocketAccept(subprotocol~ : String?, headers~ : Array[(String, String)])
WebSocketSendText(String)
WebSocketSendBytes(Bytes)
WebSocketClose(code~ : Int32, reason~ : String)
WebSocketHttpResponseStart(status~ : Int32, headers~ : Array[(String, String)], trailers~ : Bool)
WebSocketHttpResponseBody(body~ : Bytes, more_body~ : Bool)
LifespanStartup
LifespanShutdown
LifespanStartupComplete
LifespanStartupFailed(message~ : String)
LifespanShutdownComplete
LifespanShutdownFailed(message~ : String)
Other(type_~ : String, payload~ : Json)
} derive(Eq, @debug.Debug)structExtensions
The scope's extensions map, modelled as typed capability flags plus the one extension that carries data (tls). A server sets a flag to advertise that it will honour the matching outbound event — an application checks the flag before emitting HttpResponsePush / HttpResponsePathSend / HttpResponseEarlyHint / HttpResponseTrailers / WebSocketHttpResponseStart, exactly as an ASGI app tests "http.response.push" in scope["extensions"].
pub(all) struct Extensions {
tls : TlsExtension?
http_response_push : Bool
http_response_trailers : Bool
http_response_pathsend : Bool
http_response_zerocopysend : Bool
http_response_early_hint : Bool
http_response_debug : Bool
websocket_http_response : Bool
custom : Map[String, Json]
} derive(Eq)pub fn Extensions::enable(Self, String, value? : Json) -> Self
Advertise an extension this seam does not name, with whatever value the server wants to hand the application (the spec's own http.fullflush example carries an empty object).
pub fn Extensions::enable_debug(Self) -> Self
Advertise the debug extension (http.response.debug). Per the ASGI spec this is for testing only and production servers should not implement it.
pub fn Extensions::enable_early_hint(Self) -> Self
Advertise the early-hints extension (http.response.early_hint): the server will forward HttpResponseEarlyHint messages as 103 Early Hints informational responses ahead of the final status.
pub fn Extensions::enable_pathsend(Self) -> Self
Advertise the path-send extension (http.response.pathsend).
pub fn Extensions::enable_push(Self) -> Self
Advertise the server-push extension (http.response.push).
pub fn Extensions::enable_trailers(Self) -> Self
Advertise the response-trailers extension (http.response.trailers).
pub fn Extensions::enable_websocket_http_response(Self) -> Self
Advertise the WebSocket-denial-with-response extension (websocket.http.response).
pub fn Extensions::enable_zerocopysend(Self) -> Self
Advertise the zero-copy-send extension (http.response.zerocopysend): the server will send the contents of an open file descriptor with zero copies.
pub fn Extensions::get(Self, String) -> Json?
What the server advertised for name, or None if it did not. Only the extensions this seam has no field for; the named ones are the Bools above.
pub fn Extensions::none() -> Self
No extensions advertised: every capability off, no TLS data. The starting point servers and tests build from with the enable_* / with_tls helpers.
pub fn Extensions::with_tls(Self, TlsExtension) -> Self
Attach tls extension data to a connection scope.
enumHttp2HeaderError
Why a HEADERS frame does not map onto an HttpScope field-by-field: an HTTP/2 (RFC 7540 §8.1.2.3) or HTTP/3 request carries its request line as four *pseudo-headers* — :method, :scheme, :authority, :path — interleaved with the ordinary fields, and ASGI reserves no slot for them in scope["headers"]. A conforming server must consume the pseudo-headers into the scope's typed fields and hand the application only the ordinary headers, with host synthesised from :authority. moonasgi owns that lowering so every h2/h2c/h3 transport produces the same scope from the same frame, and an app never sees a :-prefixed header.
A malformed pseudo-header set — a missing required one, a duplicate, an unknown :foo, or a pseudo-header after an ordinary field — is rejected with the exact Http2HeaderError, matching RFC 7540's "malformed request" treatment, so the transport can answer RST_STREAM(PROTOCOL_ERROR) instead of forwarding a bad scope.
pub(all) enum Http2HeaderError {
MissingMethod
MissingScheme
MissingPath
EmptyPath
DuplicatePseudoHeader(String)
UnknownPseudoHeader(String)
PseudoHeaderAfterRegular(String)
} derive(Eq)structHttpScope
HTTP connection scope (ASGI type == "http"). raw_path / query_string stay Bytes because they are not guaranteed valid UTF-8; header names follow ASGI's lowercased-latin1 convention. asgi carries the version handshake, extensions the advertised server capabilities, and state is per-connection scratch copied from the lifespan state.
pub(all) struct HttpScope {
http_version : String
http_method : String
scheme : String
path : String
raw_path : Bytes
query_string : Bytes
root_path : String
headers : Array[(String, String)]
client : (String, Int32)?
server : (String, Int32?)?
asgi : AsgiVersion
extensions : Extensions
state : Map[String, Json]
}pub fn HttpScope::from_h2_headers(Array[(String, String)], http_version? : String, root_path? : String, client? : (String, Int32), server? : (String, Int32?), extensions? : Extensions, asgi? : AsgiVersion, state? : Map[String, Json]) -> Result[Self, Http2HeaderError]
Lower an HTTP/2 (or HTTP/3) request's HEADERS block — pseudo-headers and ordinary fields in wire order — into an HttpScope, the way a conforming ASGI server consumes a frame. :method, :scheme and :path become the scope's http_method / scheme / path (with the query string split off :path); the returned headers are the ordinary fields only, with host synthesised at the front from :authority (replacing any host the peer also sent, per RFC 7540 §8.1.2.3). http_version defaults to "2"; pass "3" for an HTTP/3 transport, which shares this pseudo-header contract.
The remaining scope fields (root_path, client, server, extensions, asgi, state) are supplied by the server exactly as for HttpScope::new. Returns Err naming the first violation for a malformed pseudo-header set, so the SEAM never hands a framework a scope with a :-prefixed header or a missing request-line field.
pub fn HttpScope::new(http_method~ : String, path~ : String, http_version? : String, scheme? : String, raw_path? : Bytes, query_string? : Bytes, root_path? : String, headers? : Array[(String, String)], client? : (String, Int32), server? : (String, Int32?), asgi? : AsgiVersion, extensions? : Extensions, state? : Map[String, Json]) -> Self
Build an HttpScope with ASGI's usual defaults filled in — http_version "1.1", scheme "http", empty query/headers/state, the default asgi handshake, and no extensions. raw_path defaults to the UTF-8 encoding of path. The ergonomic constructor servers and the TestClient build scopes through, so callers spell only what differs from the common case.
structLifespanScope
Lifespan scope (ASGI type == "lifespan"): a single run spanning process startup and shutdown, whose state seeds every request scope. Carries its own asgi handshake — lifespan versions independently (sub-spec 2.0), so it does not borrow the http/websocket spec_version.
pub(all) struct LifespanScope {
asgi : AsgiVersion
state : Map[String, Json]
}pub fn LifespanScope::new(asgi? : AsgiVersion, state? : Map[String, Json]) -> Self
Build a LifespanScope with the lifespan asgi handshake (sub-spec 2.0) and an empty state by default.
enumScope
The connection scope: one value per HTTP request, WebSocket connection, or lifespan run. It carries the immutable connection metadata a server hands to an application, mirroring ASGI 3.0's scope dict as a typed sum.
pub(all) enum Scope {
Http(HttpScope)
WebSocket(WebSocketScope)
Lifespan(LifespanScope)
}structTlsExtension
The tls extension's per-connection data (ASGI TLS extension). Present in a scope's extensions only when the connection is TLS-terminated by the server. Certificates are PEM text; tls_version / cipher_suite are the numeric IANA identifiers, None when the server does not expose them.
pub(all) struct TlsExtension {
server_cert : String?
client_cert_chain : Array[String]
client_cert_name : String?
client_cert_error : String?
tls_version : Int32?
cipher_suite : Int32?
} derive(Eq)structWebSocketScope
WebSocket connection scope (ASGI type == "websocket"). scheme is "ws"/"wss"; subprotocols are the client-offered values. Carries the same asgi handshake and extensions capabilities as an http scope.
pub(all) struct WebSocketScope {
http_version : String
scheme : String
path : String
raw_path : Bytes
query_string : Bytes
root_path : String
headers : Array[(String, String)]
client : (String, Int32)?
server : (String, Int32?)?
subprotocols : Array[String]
asgi : AsgiVersion
extensions : Extensions
state : Map[String, Json]
}pub fn WebSocketScope::new(path~ : String, http_version? : String, scheme? : String, raw_path? : Bytes, query_string? : Bytes, root_path? : String, headers? : Array[(String, String)], client? : (String, Int32), server? : (String, Int32?), subprotocols? : Array[String], asgi? : AsgiVersion, extensions? : Extensions, state? : Map[String, Json]) -> Self
Build a WebSocketScope with ASGI's usual defaults filled in — scheme "ws", empty query/headers/subprotocols/state, the default asgi handshake, and no extensions. raw_path defaults to the UTF-8 encoding of path.
pub fn WebSocketScope::select_subprotocol(Self, Array[String]) -> String?
Choose the subprotocol to accept from those the client offered (RFC 6455 §4.1): the first of the client's offered subprotocols, in the client's own order of preference, that the server also lists in supported; None when the two share none (the connection then proceeds with no subprotocol). The result is what the app hands to WebSocketAccept(subprotocol=..).
typeAsgiApp
The load-bearing ASGI callable: (scope, receive, send). Every server binds to this shape and every framework in the suite ultimately compiles down to it.
pub type AsgiApp = async (Scope, async () -> Event, async (Event) -> Unit) -> Unit
typeAsgiAppInstance
The asynchronous instance a legacy app's first callable returns: already bound to the scope, it drives the connection with receive / send.
pub type AsgiAppInstance = async (async () -> Event, async (Event) -> Unit) -> Unit
typeLegacyAsgiApp
A legacy ASGI 2.0 double-callable application: a synchronous application(scope) that returns the AsgiAppInstance.
pub type LegacyAsgiApp = (Scope) -> async (async () -> Event, async (Event) -> Unit) -> Unit
typeReceive
Pull the next inbound event. The async awaitable an application calls to read request body chunks, websocket frames, or lifespan signals.
pub type Receive = async () -> Event
typeSend
Push an outbound event. The async awaitable an application calls to emit response start/body, websocket frames, or lifespan completion.
pub type Send = async (Event) -> Unit
Functions
pub fn double_to_single_callable((Scope) -> async (async () -> Event, async (Event) -> Unit) -> Unit) -> async (Scope, async () -> Event, async (Event) -> Unit) -> Unit
Wrap a legacy 2.0 double-callable app as a 3.0 single-callable AsgiApp: call the first callable with the scope to get the instance, then drive it with receive / send.
pub fn guarantee_single_callable(AsgiApplication) -> async (Scope, async () -> Event, async (Event) -> Unit) -> Unit
Normalize any application to a single-callable AsgiApp: a 3.0 app passes straight through, and a legacy 2.0 app is wrapped. A server calls this once and then only ever drives single-callables.
pub fn headers_from_wire(Array[(Bytes, Bytes)]) -> Array[(String, String)]
Decode a wire header list (raw ASGI byte-string pairs, as they arrive off an HTTP/1.1 or HPACK frame) into the (String, String) pairs the seam uses.
pub fn headers_to_wire(Array[(String, String)]) -> Array[(Bytes, Bytes)]
Encode the seam's header list back to raw ASGI byte-string pairs for the wire. The exact inverse of headers_from_wire, byte for byte.
pub fn latin1_decode(Bytes) -> String
Decode a raw byte string to a latin-1 String: each byte becomes the character with that code point. Total — every byte 0x00..0xFF is a valid code point.
pub fn latin1_encode(String) -> Bytes
Encode a String back to a header byte string. The inverse of latin1_decode for anything that came from the wire, since HTTP header values are latin-1 by the protocol's own rules.
A character above U+00FF never came off a wire — it was put there by a framework setting, say, a content-disposition filename — and truncating it to its low byte would silently corrupt it. Those are written as their UTF-8 bytes instead, which is what real servers emit and what RFC 6266 §4.3 expects a recipient to decode.
pub fn percent_decode(String) -> Bytes
Percent-decode a request target's path into the characters ASGI's path carries, leaving the undecoded bytes for raw_path. %2F inside a segment therefore reaches the application as a slash in path, which is why a router that cares about segment boundaries reads raw_path.
A stray % or a truncated escape is passed through as written rather than raising: the target is attacker-controlled, and a server that refuses to build a scope cannot answer 400 either.
pub fn run_legacy((Scope) -> (Array[Event]) -> Array[Event], Scope, Array[Event]) -> Array[Event]
The synchronous core of the legacy two-call convention, testable without an async runtime the way run_http is for to_asgi: the first callable takes the scope and returns an instance that folds the inbound event stream into the outbound one. double_to_single_callable is the async lift of exactly this two-step shape, so a green run_legacy covers the convention's semantics that the async wrapper relies on.
pub fn split_target(String) -> (String, String)
Split a request target into its path and raw query string on the first ?; "/a/b?x=1&y=2" → ("/a/b", "x=1&y=2"), "/a/b" → ("/a/b", "").
moonasgi/http
31 itemsThe request and response a handler works in, and the synchronous cores that lower them onto events — a whole request without a socket.
enumDrain
Why a body drain stopped: the request body finished, the client went away mid-request, or the stream ran out without either. A long-polling or streaming handler cares about the difference — Disconnected means nothing it sends will arrive, so cleanup is all that is left to do.
pub(all) enum Drain {
Complete
Disconnected
Truncated
} derive(Eq, @debug.Debug)structRequest
An inbound HTTP request in ergonomic form: the request line, headers, and the fully-read body. The sugar over @spec.HttpScope plus a drained Receive, so a Handler never touches the async transport directly.
pub(all) struct Request {
http_method : String
path : String
query_string : Bytes
headers : Array[(String, String)]
body : Bytes
} derive(Eq)pub fn Request::header(Self, String) -> String?
Look up the first header matching name, following ASGI's lowercased-name convention.
structResponse
An outbound HTTP response: status, headers, and the full body. Mutable so middleware can decorate it before the server serialises it into HttpResponseStart + HttpResponseBody.
pub(all) struct Response {
status : Int32
headers : Array[(String, String)]
body : Bytes
} derive(Eq)pub fn Response::events(Self) -> Array[@spec.Event]
Serialise a Response into the outbound event pair a server sends: an HttpResponseStart carrying status and headers (no trailers), then a single HttpResponseBody with the whole body and more_body: false. The public Response::events for callers that want the lowered form directly.
pub fn Response::header(Self, String) -> String?
Look up the first response header matching name, same convention as Request::header.
pub fn Response::json(status? : Int32, Json) -> Self
An application/json response whose body is the UTF-8 encoding of value serialised with Json::stringify. The ergonomic constructor for a JSON reply.
pub fn Response::new(Int32, Array[(String, String)], Bytes) -> Self
Build a response from raw bytes.
pub fn Response::text(status? : Int32, String) -> Self
A text/plain; charset=utf-8 response whose body is the UTF-8 encoding of body. The ergonomic constructor for the common string reply.
structStreamingResponse
A streamed outbound response: the status line, headers, an ordered list of body chunks each emitted as its own HttpResponseBody, and optional trailing trailers. Models ASGI response streaming (multiple body messages with more_body: true) and the http.response.trailers extension without an async transport. events lowers it to the exact event sequence a server would send. early_hints (the http.response.early_hint extension) are the 103 Early Hints messages emitted ahead of the final response, each element a list of Link header values.
pub(all) struct StreamingResponse {
status : Int32
headers : Array[(String, String)]
chunks : Array[Bytes]
trailers : Array[(String, String)]
early_hints : Array[Array[String]]
} derive(Eq)pub fn StreamingResponse::events(Self) -> Array[@spec.Event]
Lower a streamed response to the outbound events a server sends: one HttpResponseEarlyHint per early-hint message (before the response starts), then an HttpResponseStart (with trailers set when any are present), then one HttpResponseBody per chunk — more_body: true on all but the last — and, if there are trailers, a terminating HttpResponseTrailers. An empty chunks still yields a single empty final body, so the stream is always well-formed.
pub fn StreamingResponse::new(chunks~ : Array[Bytes], status? : Int32, headers? : Array[(String, String)], trailers? : Array[(String, String)], early_hints? : Array[Array[String]]) -> Self
Build a StreamingResponse. status defaults to 200, headers, trailers and early_hints to empty; chunks is the ordered body, each element becoming one HttpResponseBody frame.
typeHandler
The ergonomic request→response function most handlers are written as. The synchronous sugar the suite lifts onto @spec.AsgiApp at the server boundary.
pub type Handler = (Request) -> Response
typeMiddleware
A handler transformer that wraps a downstream Handler to add cross-cutting behaviour. Composed as an onion where the first registered is outermost.
pub type Middleware = ((Request) -> Response) -> (Request) -> Response
typeStreamHandler
A streaming handler: produces a StreamingResponse (multi-chunk body, optional trailers) instead of a single buffered Response. Driven by run_http_stream and the TestClient.
pub type StreamHandler = (Request) -> StreamingResponse
Functions
pub fn compose(Array[((Request) -> Response) -> (Request) -> Response], (Request) -> Response) -> (Request) -> Response
Compose middlewares over a base handler. The first element is the outermost wrapper, matching registration order.
pub fn run_http((Request) -> Response, @spec.Scope, Array[@spec.Event]) -> Array[@spec.Event]
The synchronous counterpart of to_asgi: drive a Handler over an already materialised inbound event sequence and return the outbound events a server would send — [HttpResponseStart, HttpResponseBody] for an http scope, [] otherwise. A thin run_http_app wrapper over Response::events, the sans-transport core to_asgi mirrors with async receive/send.
pub fn run_http_app((Request) -> Array[@spec.Event], @spec.Scope, Array[@spec.Event]) -> Array[@spec.Event]
The Request-level sans-transport core: drain the http request body from inbound, hand the assembled Request to app, and return the outbound events app emits. The ergonomic wrapper over run_http_scoped for apps that only need the request line, headers, and body — every server and the TestClient drive a Handler through it. Non-http scopes yield [].
pub fn run_http_scoped((@spec.HttpScope, Bytes) -> Array[@spec.Event], @spec.Scope, Array[@spec.Event]) -> Array[@spec.Event]
The scope-aware sans-transport core: drain the http request body from inbound (accumulating HttpRequest chunks until more_body is false) and hand app the full @spec.HttpScope alongside the assembled body, returning the outbound events app emits. This is the seam a framework binds to — a framework reads what the ergonomic Request drops: root_path (for mounted sub-apps), extensions (to gate a feature on what the server advertises), client/server peers, the asgi handshake, and state (the map a lifespan startup seeds and the server copies onto every request scope). It is the synchronous analog of the ASGI (scope, receive, send) callable for the common drain-then-handle shape; run_http_app is the Request-level wrapper over it. Non-http scopes yield [].
pub fn run_http_scoped_drain((@spec.HttpScope, Bytes, Drain) -> Array[@spec.Event], @spec.Scope, Array[@spec.Event]) -> Array[@spec.Event]
run_http_scoped, but the app also learns why the body drain stopped. A handler that holds a connection open — long polling, a slow upload — needs to tell "the client sent everything" from "the client went away", which the plain form cannot: it runs the handler to completion either way and the response goes nowhere. Non-http scopes yield [].
pub fn run_http_stream((Request) -> StreamingResponse, @spec.Scope, Array[@spec.Event]) -> Array[@spec.Event]
Drive a streaming StreamHandler over an inbound event sequence, returning the full outbound stream — HttpResponseStart, one HttpResponseBody per chunk (more_body: true on all but the last), and a trailing HttpResponseTrailers when the response carries trailers. The streaming counterpart of run_http.
pub fn to_asgi((Request) -> Response) -> async (@spec.Scope, async () -> @spec.Event, async (@spec.Event) -> Unit) -> Unit
Lift a synchronous Handler onto the load-bearing @spec.AsgiApp the server binds to. For an http scope it drains the request body — looping receive() and accumulating HttpRequest chunks until more_body is false — assembles a Request, runs the handler, then emits HttpResponseStart followed by a single HttpResponseBody. Non-http scopes (websocket, lifespan) are no-ops: this sugar covers request→response handlers only. Shares its drain, request assembly, and response serialisation with run_http, which tests the same logic without the async transport.
moonasgi/ws
15 itemsThe WebSocket core: a connect / receive / disconnect handler, driven in-process by ws_run.
structWebSocketHandler
A synchronous WebSocket application in the connect / receive / disconnect shape, the WebSocket analog of the http Handler. on_connect decides the handshake from the scope; on_receive maps each inbound WsMessage to the frames to send back (it may Close); on_disconnect runs when the client goes away, carrying the close code and the optional close reason (ASGI 2.5). Driven in-process by ws_run and the TestClient, so accept/subprotocol/echo/close logic is testable on every backend without an async socket — the faithful synchronous core the async server lifts onto AsgiApp.
pub(all) struct WebSocketHandler {
on_connect : (@spec.WebSocketScope) -> WsAccept
on_receive : (WsMessage) -> Array[WsSend]
on_disconnect : (Int32, String?) -> Array[WsSend]
}pub fn WebSocketHandler::echo(subprotocol? : String) -> Self
An echo handler: accept the handshake (optionally negotiating subprotocol), then send every received message straight back — text as text, binary as binary. The canonical WebSocket smoke test.
pub fn WebSocketHandler::new(on_connect? : (@spec.WebSocketScope) -> WsAccept, on_receive? : (WsMessage) -> Array[WsSend], on_disconnect? : (Int32, String?) -> Array[WsSend]) -> Self
Build a WebSocketHandler. on_connect defaults to accepting the handshake with no chosen subprotocol and no extra headers; on_receive and on_disconnect default to doing nothing. Callers override only the phases they care about.
enumWsAccept
The application's answer to a WebSocketConnect, mirroring the three replies ASGI allows to an opening handshake: Accept it (optionally choosing a subprotocol and adding response headers), Reject it with a bare close code (ASGI closes the handshake with websocket.close), or DenyHttp it with a full HTTP response (the websocket.http.response extension — a real status + headers + body instead of a bare close).
pub(all) enum WsAccept {
Accept(subprotocol~ : String?, headers~ : Array[(String, String)])
Reject(code~ : Int32, reason~ : String)
DenyHttp(status~ : Int32, headers~ : Array[(String, String)], body~ : Bytes)
} derive(Eq)enumWsMessage
A message crossing a WebSocket in either direction: a UTF-8 Text frame or a Binary frame. The ergonomic form of WebSocketReceive / WebSocketSendText / WebSocketSendBytes, so a WebSocketHandler reasons over messages instead of raw events.
pub(all) enum WsMessage {
Text(String)
Binary(Bytes)
} derive(Eq, @debug.Debug)enumWsSend
An outbound action a WebSocketHandler takes while the connection is open: send a SendText / SendBinary frame, or Close the connection with a code and reason. A Close ends the handler's send stream — the driver stops pulling further inbound messages, mirroring a server that has closed the socket.
pub(all) enum WsSend {
SendText(String)
SendBinary(Bytes)
Close(code~ : Int32, reason~ : String)
} derive(Eq)Functions
pub fn ws_run(WebSocketHandler, @spec.Scope, Array[@spec.Event]) -> Array[@spec.Event]
Drive a WebSocketHandler over an inbound event sequence, returning the outbound events a server would send. The WebSocket counterpart of run_http: a WebSocket scope is driven through drive_ws; any other scope yields [], since this sugar covers websocket connections only.
pub fn ws_run_app((@spec.WebSocketScope, Array[@spec.Event]) -> Array[@spec.Event], @spec.Scope, Array[@spec.Event]) -> Array[@spec.Event]
The general sans-transport WebSocket core: hand the whole inbound event stream and the @spec.WebSocketScope to app and return the outbound events it emits. The escape hatch under ws_run for applications whose control flow is not the connect/receive/disconnect fold — an app is free to inspect every scope field (subprotocols, headers, extensions, state) and emit any sequence. Non-websocket scopes yield []. The WebSocket analog of run_http_app.
moonasgi/lifespan
6 itemsThe lifespan core: a startup and a shutdown hook, driven in-process by run_lifespan.
structLifespanHandler
A synchronous lifespan application in the startup / shutdown shape, the lifespan analog of the http Handler and the WebSocket WebSocketHandler. on_startup runs once at boot: it seeds the scope's state in place (the map the server then copies onto every request scope — a DB pool handle, a loaded config) and returns Complete or Failed. on_shutdown runs once at teardown to release those resources. Driven in-process by run_lifespan, so boot/teardown logic is testable on every backend without an async runtime — the synchronous core a server lifts onto the lifespan protocol.
pub(all) struct LifespanHandler {
on_startup : (@spec.LifespanScope) -> LifespanReply
on_shutdown : (@spec.LifespanScope) -> LifespanReply
}pub fn LifespanHandler::new(on_startup? : (@spec.LifespanScope) -> LifespanReply, on_shutdown? : (@spec.LifespanScope) -> LifespanReply) -> Self
Build a LifespanHandler. Both phases default to succeeding without doing anything, so a caller overrides only the phase it needs — an app that just wants a startup hook leaves on_shutdown alone.
enumLifespanReply
An application's answer to a lifespan phase: Complete when startup or shutdown succeeded, or Failed with a message the server logs and (for startup) aborts the boot on. Mirrors the two replies ASGI allows to each lifespan message — lifespan.startup.complete / .failed and lifespan.shutdown.complete / .failed.
pub(all) enum LifespanReply {
Complete
Failed(message~ : String)
} derive(Eq)Functions
pub fn run_lifespan(LifespanHandler, @spec.Scope, Array[@spec.Event]) -> Array[@spec.Event]
Drive a LifespanHandler over a materialised inbound event stream, folding it into the outbound replies a server would send. LifespanStartup runs on_startup and emits LifespanStartupComplete or LifespanStartupFailed; a failed startup ends the run, since ASGI has the server abort the boot and never send lifespan.shutdown. LifespanShutdown runs on_shutdown and emits the matching shutdown reply. The handler mutates scope.state in place during startup, exactly as an ASGI app populates scope["state"]. This is the synchronous lifespan core mirroring run_http / ws_run; a non-lifespan scope yields [].
moonasgi/client
38 itemsTestClient: drive a handler or a WebSocket session in-process and read back the reassembled answer.
structPushPromise
A server-side push promise captured by the TestClient: the pushed path and the request headers the server would send for it (http.response.push).
pub(all) struct PushPromise {
path : String
headers : Array[(String, String)]
} derive(Eq, @debug.Debug)pub fn PushPromise::header(Self, String) -> String?
First push-request header matching name.
structTestClient
An in-process application driver — the ASGI TestClient — built on the synchronous @http.run_http_app core, so it needs no socket and runs on every backend. It builds a synthetic http scope, feeds the request body in as HttpRequest events, runs the application, and reassembles the outbound stream into a TestResponse. app is the event-emitting application; the new / from_stream constructors adapt a @http.Handler / @http.StreamHandler.
pub(all) struct TestClient {
app : (@http.Request) -> Array[@spec.Event]
root_path : String
base_headers : Array[(String, String)]
client : (String, Int32)?
server : (String, Int32?)?
extensions : @spec.Extensions
}pub fn TestClient::delete(Self, String, headers? : Array[(String, String)]) -> TestResponse
DELETE path.
pub fn TestClient::from_app((@http.Request) -> Array[@spec.Event], root_path? : String, base_headers? : Array[(String, String)], client? : (String, Int32)?, server? : (String, Int32?)?, extensions? : @spec.Extensions) -> Self
Build a TestClient from a raw event-emitting application (the shape @http.run_http_app drives). Defaults mirror a typical test harness: empty root_path, no base headers, a ("testclient", 50000) client peer, a ("testserver", 80) server, and no advertised extensions.
pub fn TestClient::from_stream((@http.Request) -> @http.StreamingResponse, root_path? : String, base_headers? : Array[(String, String)], client? : (String, Int32)?, server? : (String, Int32?)?, extensions? : @spec.Extensions) -> Self
Build a TestClient from a streaming @http.StreamHandler (multi-chunk body, optional trailers).
pub fn TestClient::get(Self, String, headers? : Array[(String, String)]) -> TestResponse
GET path.
pub fn TestClient::head(Self, String, headers? : Array[(String, String)]) -> TestResponse
HEAD path.
pub fn TestClient::new((@http.Request) -> @http.Response, root_path? : String, base_headers? : Array[(String, String)], client? : (String, Int32)?, server? : (String, Int32?)?, extensions? : @spec.Extensions) -> Self
Build a TestClient from a unary @http.Handler.
pub fn TestClient::patch(Self, String, body? : Bytes, headers? : Array[(String, String)]) -> TestResponse
PATCH path with body.
pub fn TestClient::post(Self, String, body? : Bytes, headers? : Array[(String, String)]) -> TestResponse
POST path with body.
pub fn TestClient::put(Self, String, body? : Bytes, headers? : Array[(String, String)]) -> TestResponse
PUT path with body.
pub fn TestClient::request(Self, http_method~ : String, path~ : String, headers? : Array[(String, String)], body? : Bytes, http_version? : String, chunks? : Array[Bytes]) -> TestResponse
Drive one request through the application and capture the response. Splits path into path + query string, merges the client's base_headers before the per-request headers, builds an http scope (carrying the client's root_path / peers / advertised extensions), feeds the body in, runs the app, and reassembles the outbound stream. Pass chunks to stream the request body as several HttpRequest events.
pub fn TestClient::websocket(Self, path~ : String, handler~ : @ws.WebSocketHandler, send? : Array[@ws.WsMessage], headers? : Array[(String, String)], subprotocols? : Array[String], disconnect? : Int32?) -> WsTestSession
Drive a @ws.WebSocketHandler through a full connection in-process and capture the result. Opens a WebSocketConnect, feeds each send message in as a WebSocketReceive, then (unless disconnect is None) a WebSocketDisconnect with the given close code, runs the handler through the synchronous @ws.ws_run core, and reassembles the server's outbound frames into a WsTestSession. No socket — testable on every backend, the WebSocket counterpart of request.
pub fn TestClient::websocket_app(Self, path~ : String, app~ : (@spec.WebSocketScope, Array[@spec.Event]) -> Array[@spec.Event], send? : Array[@ws.WsMessage], headers? : Array[(String, String)], subprotocols? : Array[String], disconnect? : Int32?) -> WsTestSession
Drive a general (@spec.WebSocketScope, Array[@spec.Event]) -> Array[@spec.Event] application through a full connection in-process. Like websocket, but for an app that consumes the whole inbound stream directly (the @ws.ws_run_app escape hatch) rather than the connect/receive/disconnect fold.
structTestResponse
The materialised result of driving an application in-process: the response status and headers, the body reassembled from every HttpResponseBody chunk, the trailing headers gathered from HttpResponseTrailers, any push promises, the pathsend path if the app used the path-send extension, the last zerocopysend handoff, every debug info payload, and the early_hints (each a 103 message's Link values). The captured, sans-transport analog of what a real client would observe.
pub(all) struct TestResponse {
status : Int32
headers : Array[(String, String)]
body : Bytes
trailers : Array[(String, String)]
pushes : Array[PushPromise]
pathsend : String?
zerocopysend : ZeroCopySend?
debug : Array[Json]
early_hints : Array[Array[String]]
} derive(Eq, @debug.Debug)pub fn TestResponse::header(Self, String) -> String?
First response header matching name (ASGI lowercased-name convention).
pub fn TestResponse::json(Self) -> Json raise
The response body parsed as JSON. Raises @json.ParseError if the body is not valid JSON, mirroring a real client's .json().
pub fn TestResponse::text(Self) -> String
The response body decoded as UTF-8 (lossy: invalid sequences become the replacement character), for asserting on text responses.
pub fn TestResponse::trailer(Self, String) -> String?
First trailing header matching name.
structWsTestSession
The materialised result of driving a WebSocket connection in-process: whether the handshake was accepted, the negotiated subprotocol and accept_headers, every server→client message (messages), whether the server closed and with what close_code / close_reason, and — when the handshake was denied with the websocket.http.response extension — the captured HTTP denial response. The WebSocket analog of TestResponse.
pub(all) struct WsTestSession {
accepted : Bool
subprotocol : String?
accept_headers : Array[(String, String)]
messages : Array[@ws.WsMessage]
closed : Bool
close_code : Int32?
close_reason : String?
denial : TestResponse?
} derive(Eq, @debug.Debug)pub fn WsTestSession::header(Self, String) -> String?
First accept-response header matching name.
pub fn WsTestSession::texts(Self) -> Array[String]
The server→client messages that were text frames, decoded to their strings (binary frames are skipped). The ergonomic assertion target for an echo/chat exchange.
structZeroCopySend
A http.response.zerocopysend message captured by the TestClient: the open file descriptor the app handed off, and the optional byte offset / count. The sans-transport analog of the server performing the zero-copy send.
pub(all) struct ZeroCopySend {
fd : Int32
offset : Int32?
count : Int32?
} derive(Eq, @debug.Debug)Functions
pub fn reassemble(Array[@spec.Event]) -> TestResponse
Fold an outbound event stream into a TestResponse: HttpResponseStart sets status/headers, every HttpResponseBody appends to the body, every HttpResponseTrailers appends to the trailers, each HttpResponsePush records a promise, HttpResponsePathSend records the served path, and every HttpResponseEarlyHint records one 103 message's Link values. Other events (websocket, lifespan) are ignored — the http response is what a client observes. 把一串事件拼回一个响应,一致性套件与测试客户端共用。
pub fn request_events(Bytes, chunks? : Array[Bytes]) -> Array[@spec.Event]
Materialise a request body into inbound HttpRequest events. When chunks is given each element becomes its own event (more_body: true on all but the last), exercising a streamed request; otherwise body is a single event.
moonasgi/mid
58 itemsApplication-level middleware, each one a record implementing Layer, stackable in any order.
structBodyLimit
A ceiling on a request body. A Content-Length above it is answered 413 at once, without calling the application. A body that grows past it while the application reads it is answered 413 too, when no response has started; the application then reads a disconnect, and what it sends afterwards is dropped, since the client already has its answer.
pub(all) struct BodyLimit {
limit : Int32
} derive(Eq, @debug.Debug)pub impl Layer for BodyLimit
pub fn BodyLimit::new(limit? : Int32) -> Self
pub fn BodyLimit::wrap(Self, async (@spec.Scope, async () -> @spec.Event, async (@spec.Event) -> Unit) -> Unit) -> async (@spec.Scope, async () -> @spec.Event, async (@spec.Event) -> Unit) -> Unit
structCors
Cross-origin resource sharing, per the Fetch standard §3.2.
origins, methods and headers are allow-lists, and "*" in one allows anything. A preflight is answered here: 200 with the negotiated headers, or 400 naming what was refused. Other cross-origin responses get access-control-allow-origin when the origin is allowed. With credentials the origin is echoed rather than *, as the standard requires.
pub(all) struct Cors {
origins : Array[String]
methods : Array[String]
headers : Array[String]
credentials : Bool
expose : Array[String]
max_age : Int32
} derive(Eq, @debug.Debug)pub impl Layer for Cors
pub fn Cors::new(origins? : Array[String], methods? : Array[String], headers? : Array[String], credentials? : Bool, expose? : Array[String], max_age? : Int32) -> Self
pub fn Cors::wrap(Self, async (@spec.Scope, async () -> @spec.Event, async (@spec.Event) -> Unit) -> Unit) -> async (@spec.Scope, async () -> @spec.Event, async (@spec.Event) -> Unit) -> Unit
structGzip
Gzip for responses a client accepts it for: a body of at least minimum octets, sent in one message, not already content-encoded. level is the zlib effort, 0 to 9.
A body sent in several messages is passed through as it comes: compressing it would hold it back until the end, and a stream is sent in pieces so that it is not held back. An event stream is never compressed, for the same reason.
pub(all) struct Gzip {
minimum : Int32
level : Int32
} derive(Eq, @debug.Debug)pub impl Layer for Gzip
pub fn Gzip::new(minimum? : Int32, level? : Int32) -> Self
pub fn Gzip::wrap(Self, async (@spec.Scope, async () -> @spec.Event, async (@spec.Event) -> Unit) -> Unit) -> async (@spec.Scope, async () -> @spec.Event, async (@spec.Event) -> Unit) -> Unit
structHttpsRedirect
Send every plain request to its secure equivalent: http to https and ws to wss, with a 307 so the method and body are kept. Port 80 or 443 in the host is dropped; any other port stays.
pub(all) struct HttpsRedirect {
} derive(Eq, @debug.Debug)pub impl Layer for HttpsRedirect
pub fn HttpsRedirect::new() -> Self
pub fn HttpsRedirect::wrap(Self, async (@spec.Scope, async () -> @spec.Event, async (@spec.Event) -> Unit) -> Unit) -> async (@spec.Scope, async () -> @spec.Event, async (@spec.Event) -> Unit) -> Unit
structProxyHeaders
The client and scheme a reverse proxy saw, put back in the scope, by X-Forwarded-For / X-Forwarded-Proto or by RFC 7239's Forwarded. Only a request from an address in trusted is rewritten, since anyone else could claim any address; "*" trusts every peer.
X-Forwarded-For gives the client: the rightmost address that is not trusted, every proxy before it having appended its own; when all are trusted, the leftmost. X-Forwarded-Proto gives the scheme, http or https (ws or wss for a WebSocket). Without either header, the Forwarded header is read the same way through its for and proto.
pub(all) struct ProxyHeaders {
trusted : Array[String]
} derive(Eq, @debug.Debug)pub impl Layer for ProxyHeaders
pub fn ProxyHeaders::new(trusted? : Array[String]) -> Self
pub fn ProxyHeaders::wrap(Self, async (@spec.Scope, async () -> @spec.Event, async (@spec.Event) -> Unit) -> Unit) -> async (@spec.Scope, async () -> @spec.Event, async (@spec.Event) -> Unit) -> Unit
structServerErrors
What an application's failure becomes: a 500 when the response has not started, then the error raised on to the server, which logs it. debug puts the error in the body. handler writes the answer instead: (status, headers, body) for the error.
pub(all) struct ServerErrors {
debug : Bool
handler : ((Error) -> (Int32, Array[(String, String)], String))?
}pub impl Layer for ServerErrors
pub fn ServerErrors::new(debug? : Bool, handler? : (Error) -> (Int32, Array[(String, String)], String)) -> Self
pub fn ServerErrors::wrap(Self, async (@spec.Scope, async () -> @spec.Event, async (@spec.Event) -> Unit) -> Unit) -> async (@spec.Scope, async () -> @spec.Event, async (@spec.Event) -> Unit) -> Unit
structTrustedHosts
Only the hosts listed may be asked for: a host header naming another answers 400. *.example.com allows any subdomain, * alone allows any host. With www_redirect, a request for example.com where only www.example.com is listed is sent there.
pub(all) struct TrustedHosts {
hosts : Array[String]
www_redirect : Bool
} derive(Eq, @debug.Debug)pub impl Layer for TrustedHosts
pub fn TrustedHosts::new(hosts? : Array[String], www_redirect? : Bool) -> Self
pub fn TrustedHosts::wrap(Self, async (@spec.Scope, async () -> @spec.Event, async (@spec.Event) -> Unit) -> Unit) -> async (@spec.Scope, async () -> @spec.Event, async (@spec.Event) -> Unit) -> Unit
structWrap
A middleware written as a plain function from application to application.
pub(all) struct Wrap((async (@spec.Scope, async () -> @spec.Event, async (@spec.Event) -> Unit) -> Unit) -> async (@spec.Scope, async () -> @spec.Event, async (@spec.Event) -> Unit) -> Unit)
pub impl Layer for Wrap
pub fn Wrap::wrap(Self, async (@spec.Scope, async () -> @spec.Event, async (@spec.Event) -> Unit) -> Unit) -> async (@spec.Scope, async () -> @spec.Event, async (@spec.Event) -> Unit) -> Unit
traitLayer
Something that wraps one ASGI application in another: CORS, compression, a host check, or the caller's own. Every middleware here is a record that implements it, so a caller's middleware stacks with them on equal terms.
pub(open) trait Layer {
fn wrap(Self, async (@spec.Scope, async () -> @spec.Event, async (@spec.Event) -> Unit) -> Unit) -> async (@spec.Scope, async () -> @spec.Event, async (@spec.Event) -> Unit) -> Unit
}Functions
pub fn stack(Array[&Layer], async (@spec.Scope, async () -> @spec.Event, async (@spec.Event) -> Unit) -> Unit) -> async (@spec.Scope, async () -> @spec.Event, async (@spec.Event) -> Unit) -> Unit
app inside layers, wrapped from the last to the first, so the layer given first sees the request first and the response last.
Values
pub let body_limit : BodyLimit
One mebibyte, nginx's client_max_body_size.
pub let cors : Cors
The defaults: no origin allowed, GET only, the safelisted headers only, no credentials, preflights cached for ten minutes.
pub let gzip : Gzip
The defaults: 500 octets and up, the most effort.
pub let https_redirect : HttpsRedirect
pub let proxy_headers : ProxyHeaders
The default: only a proxy on the same machine is believed.
pub let server_errors : ServerErrors
The defaults: no detail in the body, the plain 500.
pub let trusted_hosts : TrustedHosts
The defaults: any host, and the redirect to www. on.
moonasgi/dispatch
3 itemsSeveral applications behind one: by path prefix, by host name, over one shared lifespan.
Functions
pub fn hosts(Array[(String, async (@spec.Scope, async () -> @spec.Event, async (@spec.Event) -> Unit) -> Unit)], fallback? : async (@spec.Scope, async () -> @spec.Event, async (@spec.Event) -> Unit) -> Unit) -> async (@spec.Scope, async () -> @spec.Event, async (@spec.Event) -> Unit) -> Unit
One application per host name. A name matches the request's Host header without its port and without regard to case; *.example.com matches every name below example.com, not example.com itself. The first route that matches is taken. A host no route takes goes to fallback, or is answered as mount answers an unmatched path.
The lifespan reaches every application, the fallback last; see lifespan.
pub async fn lifespan(Array[async (@spec.Scope, async () -> @spec.Event, async (@spec.Event) -> Unit) -> Unit], @spec.LifespanScope, async () -> @spec.Event, async (@spec.Event) -> Unit) -> Unit
pub fn mount(Array[(String, async (@spec.Scope, async () -> @spec.Event, async (@spec.Event) -> Unit) -> Unit)], fallback? : async (@spec.Scope, async () -> @spec.Event, async (@spec.Event) -> Unit) -> Unit) -> async (@spec.Scope, async () -> @spec.Event, async (@spec.Event) -> Unit) -> Unit
One application per path prefix: an application is picked by the start of the path. A prefix matches whole segments, so /api takes /api and /api/users but not /apis; when two prefixes match, the longer wins, whatever the order they were given in. "" or "/" matches every path.
The application chosen sees the path below its prefix, / at the least, and the prefix added to root_path, the way the ASGI spec has a mounted application see its own location. A path no prefix takes goes to fallback, or is answered with 404 (HTTP) or a declined handshake (WebSocket).
The lifespan reaches every application, the fallback last; see lifespan.
moonasgi/wsgi
10 itemsA synchronous application, run on the same events as everything else.
structEnviron
What a synchronous application is told about a request (← WSGI's environ, PEP 3333): the CGI variables as fields, the request headers, the body read in full, and the URL scheme. to_map gives the same under the CGI names, for code that reads them by key.
pub(all) struct Environ {
request_method : String
script_name : String
path_info : String
query_string : String
content_type : String
content_length : String
server_name : String
server_port : String
server_protocol : String
remote_addr : String
remote_port : String
headers : Array[(String, String)]
url_scheme : String
input : Bytes
} derive(Eq, @debug.Debug)pub fn Environ::header(Self, String) -> String?
The first request header named name, matched without regard to case.
pub fn Environ::of(@spec.HttpScope, Bytes) -> Self
The environment for scope and its body. The server's name and port come from the scope, else from the Host header, else from the scheme's default; the protocol is HTTP/ and the scope's version.
pub fn Environ::to_map(Self) -> Map[String, String]
The environment under its CGI names: REQUEST_METHOD, SCRIPT_NAME, PATH_INFO and the rest, each request header as HTTP_ and its name in upper case with - as _ (repeated headers joined with ,, except that Content-Type and Content-Length keep their own names), and wsgi.url_scheme. The body is not in it.
typeStartResponse
How a synchronous application starts its response (← WSGI's start_response): the status line, such as "200 OK", and the headers. It gives back a function that writes part of the body at once, for code written against that older style; returning the body is the usual way.
pub type StartResponse = (String, Array[(String, String)]) -> (Bytes) -> Unit
typeWsgiApp
A synchronous application (← a WSGI application): the environment and start_response in, the body out, chunk by chunk. A chunk is sent as soon as the iterator yields it, so a long body streams.
pub type WsgiApp = (Environ, (String, Array[(String, String)]) -> (Bytes) -> Unit) -> Iter[Bytes] raise
Functions
pub fn to_asgi((Environ, (String, Array[(String, String)]) -> (Bytes) -> Unit) -> Iter[Bytes] raise) -> async (@spec.Scope, async () -> @spec.Event, async (@spec.Event) -> Unit) -> Unit
app as an ASGI application. The request body is read in full before app runs, since it reads it as one value. The status and headers go out with the first chunk of the body, written or yielded; a header name is lowered, as ASGI asks of a response.
An application that raises, one that never calls start_response, and one that yields a body before it does all get a 500. A WebSocket is declined, since a synchronous application has no way to hold one, and the lifespan is answered as complete, since it has nothing to start.
moonasgi/testing
32 itemsDrive a real AsgiApp in-process: requests, WebSocket sessions, the lifespan, and the events one at a time.
suberrorRefused
What a test driver could not get from the application under test.
pub(all) suberror Refused {
Timeout(limit~ : Int32)
Finished
Unexpected(@spec.Event)
Closed(status~ : Int32, reason~ : String)
Denied(@client.TestResponse)
Startup(String)
Shutdown(String)
} derive(Eq)- Timeout
- Nothing arrived within `limit` milliseconds.
- Finished
- The application returned with nothing more to send.
- Unexpected
- The application sent an event the protocol does not allow at that point.
- Closed
- The application closed the WebSocket, before accepting it or after.
- Denied
- The application declined the WebSocket handshake with an HTTP response (`websocket.http.response`).
- Startup
- The application reported `lifespan.startup.failed` with this message.
- Shutdown
- The application reported `lifespan.shutdown.failed` with this message.
structClient
An in-process client for a real AsgiApp. Each request runs the application on a fresh scope, feeds it the body, sends http.disconnect once the response is complete, and waits for the application to return, so an error it raises fails the test. @client.TestClient is the synchronous counterpart for event-folding handlers.
state is the lifespan state: lifespan hands it to the application's startup, and every request and WebSocket scope gets a shallow copy, as the lifespan sub-specification asks.
pub(all) struct Client {
app : async (@spec.Scope, async () -> @spec.Event, async (@spec.Event) -> Unit) -> Unit
root_path : String
base_headers : Array[(String, String)]
client : (String, Int32)?
server : (String, Int32?)?
extensions : @spec.Extensions
state : Map[String, Json]
timeout : Int32
}pub fn Client::new(async (@spec.Scope, async () -> @spec.Event, async (@spec.Event) -> Unit) -> Unit, root_path? : String, base_headers? : Array[(String, String)], client? : (String, Int32)?, server? : (String, Int32?)?, extensions? : @spec.Extensions, state? : Map[String, Json], timeout? : Int32) -> Self
Build a Client for app. The peers default to ("testclient", 50000) and ("testserver", 80); timeout is how long each wait for the application lasts.
structCommunicator
A running application under test: events go in with send, come out with receive, and wait waits for the application to return. An error the application raises comes out of the next send, receive or wait, after the events it sent before failing. Made by communicate, which owns the application's task.
pub struct Communicator {
// private fields
}structSession
An accepted WebSocket under test: the negotiated subprotocol and accept headers, plus methods that talk to the application.
pub struct Session {
subprotocol : String?
headers : Array[(String, String)]
// private fields
}Functions
pub async fn Client::delete(Self, String, headers? : Array[(String, String)]) -> @client.TestResponse
An in-process client for a real AsgiApp. Each request runs the application on a fresh scope, feeds it the body, sends http.disconnect once the response is complete, and waits for the application to return, so an error it raises fails the test. @client.TestClient is the synchronous counterpart for event-folding handlers.
state is the lifespan state: lifespan hands it to the application's startup, and every request and WebSocket scope gets a shallow copy, as the lifespan sub-specification asks.
pub async fn Client::get(Self, String, headers? : Array[(String, String)]) -> @client.TestResponse
An in-process client for a real AsgiApp. Each request runs the application on a fresh scope, feeds it the body, sends http.disconnect once the response is complete, and waits for the application to return, so an error it raises fails the test. @client.TestClient is the synchronous counterpart for event-folding handlers.
state is the lifespan state: lifespan hands it to the application's startup, and every request and WebSocket scope gets a shallow copy, as the lifespan sub-specification asks.
pub async fn Client::head(Self, String, headers? : Array[(String, String)]) -> @client.TestResponse
An in-process client for a real AsgiApp. Each request runs the application on a fresh scope, feeds it the body, sends http.disconnect once the response is complete, and waits for the application to return, so an error it raises fails the test. @client.TestClient is the synchronous counterpart for event-folding handlers.
state is the lifespan state: lifespan hands it to the application's startup, and every request and WebSocket scope gets a shallow copy, as the lifespan sub-specification asks.
pub async fn[X] Client::lifespan(Self, async () -> X) -> X
An in-process client for a real AsgiApp. Each request runs the application on a fresh scope, feeds it the body, sends http.disconnect once the response is complete, and waits for the application to return, so an error it raises fails the test. @client.TestClient is the synchronous counterpart for event-folding handlers.
state is the lifespan state: lifespan hands it to the application's startup, and every request and WebSocket scope gets a shallow copy, as the lifespan sub-specification asks.
pub async fn Client::patch(Self, String, body? : Bytes, headers? : Array[(String, String)]) -> @client.TestResponse
An in-process client for a real AsgiApp. Each request runs the application on a fresh scope, feeds it the body, sends http.disconnect once the response is complete, and waits for the application to return, so an error it raises fails the test. @client.TestClient is the synchronous counterpart for event-folding handlers.
state is the lifespan state: lifespan hands it to the application's startup, and every request and WebSocket scope gets a shallow copy, as the lifespan sub-specification asks.
pub async fn Client::post(Self, String, body? : Bytes, headers? : Array[(String, String)]) -> @client.TestResponse
An in-process client for a real AsgiApp. Each request runs the application on a fresh scope, feeds it the body, sends http.disconnect once the response is complete, and waits for the application to return, so an error it raises fails the test. @client.TestClient is the synchronous counterpart for event-folding handlers.
state is the lifespan state: lifespan hands it to the application's startup, and every request and WebSocket scope gets a shallow copy, as the lifespan sub-specification asks.
pub async fn Client::put(Self, String, body? : Bytes, headers? : Array[(String, String)]) -> @client.TestResponse
An in-process client for a real AsgiApp. Each request runs the application on a fresh scope, feeds it the body, sends http.disconnect once the response is complete, and waits for the application to return, so an error it raises fails the test. @client.TestClient is the synchronous counterpart for event-folding handlers.
state is the lifespan state: lifespan hands it to the application's startup, and every request and WebSocket scope gets a shallow copy, as the lifespan sub-specification asks.
pub async fn Client::request(Self, http_method~ : String, path~ : String, headers? : Array[(String, String)], body? : Bytes, http_version? : String, chunks? : Array[Bytes]) -> @client.TestResponse
An in-process client for a real AsgiApp. Each request runs the application on a fresh scope, feeds it the body, sends http.disconnect once the response is complete, and waits for the application to return, so an error it raises fails the test. @client.TestClient is the synchronous counterpart for event-folding handlers.
state is the lifespan state: lifespan hands it to the application's startup, and every request and WebSocket scope gets a shallow copy, as the lifespan sub-specification asks.
pub async fn[X] Client::websocket(Self, path~ : String, async (Session) -> X, headers? : Array[(String, String)], subprotocols? : Array[String]) -> X
An in-process client for a real AsgiApp. Each request runs the application on a fresh scope, feeds it the body, sends http.disconnect once the response is complete, and waits for the application to return, so an error it raises fails the test. @client.TestClient is the synchronous counterpart for event-folding handlers.
state is the lifespan state: lifespan hands it to the application's startup, and every request and WebSocket scope gets a shallow copy, as the lifespan sub-specification asks.
pub async fn Communicator::receive(Self, timeout? : Int32) -> @spec.Event
A running application under test: events go in with send, come out with receive, and wait waits for the application to return. An error the application raises comes out of the next send, receive or wait, after the events it sent before failing. Made by communicate, which owns the application's task.
pub async fn Communicator::receive_nothing(Self, timeout? : Int32) -> Bool
A running application under test: events go in with send, come out with receive, and wait waits for the application to return. An error the application raises comes out of the next send, receive or wait, after the events it sent before failing. Made by communicate, which owns the application's task.
pub async fn Communicator::send(Self, @spec.Event) -> Unit
A running application under test: events go in with send, come out with receive, and wait waits for the application to return. An error the application raises comes out of the next send, receive or wait, after the events it sent before failing. Made by communicate, which owns the application's task.
pub async fn Communicator::wait(Self, timeout? : Int32) -> Unit
A running application under test: events go in with send, come out with receive, and wait waits for the application to return. An error the application raises comes out of the next send, receive or wait, after the events it sent before failing. Made by communicate, which owns the application's task.
pub async fn Session::close(Self, status? : Int32, reason? : String) -> Unit
An accepted WebSocket under test: the negotiated subprotocol and accept headers, plus methods that talk to the application.
pub async fn Session::receive(Self, timeout? : Int32) -> @ws.WsMessage
An accepted WebSocket under test: the negotiated subprotocol and accept headers, plus methods that talk to the application.
pub async fn Session::receive_bytes(Self, timeout? : Int32) -> Bytes
An accepted WebSocket under test: the negotiated subprotocol and accept headers, plus methods that talk to the application.
pub async fn Session::receive_json(Self, timeout? : Int32) -> Json
An accepted WebSocket under test: the negotiated subprotocol and accept headers, plus methods that talk to the application.
pub async fn Session::receive_text(Self, timeout? : Int32) -> String
An accepted WebSocket under test: the negotiated subprotocol and accept headers, plus methods that talk to the application.
pub async fn Session::send(Self, @ws.WsMessage) -> Unit
An accepted WebSocket under test: the negotiated subprotocol and accept headers, plus methods that talk to the application.
pub async fn Session::send_bytes(Self, Bytes) -> Unit
An accepted WebSocket under test: the negotiated subprotocol and accept headers, plus methods that talk to the application.
pub async fn Session::send_json(Self, Json) -> Unit
An accepted WebSocket under test: the negotiated subprotocol and accept headers, plus methods that talk to the application.
pub async fn Session::send_text(Self, String) -> Unit
An accepted WebSocket under test: the negotiated subprotocol and accept headers, plus methods that talk to the application.
pub async fn[X] communicate(async (@spec.Scope, async () -> @spec.Event, async (@spec.Event) -> Unit) -> Unit, @spec.Scope, async (Communicator) -> X, timeout? : Int32) -> X
Values
pub let quiet : Int32
How long receive_nothing listens before it concludes nothing is coming, in milliseconds: 100 ms.
pub let timeout : Int32
How long receive and wait wait by default, in milliseconds: one second.
moonasgi/conform
14 itemsCheck the seam wiring: a conformance harness, and the message-ordering rules as code.
structConformanceReport
The outcome of the ASGI 3.0 conformance harness: how many round-trip checks ran, how many passed, and the names of any that failures. A clean run has an empty failures. Reusable beyond the test suite — a server or framework built on the seam can call run_conformance to self-verify the wiring.
pub(all) struct ConformanceReport {
total : Int32
passed : Int32
failures : Array[String]
} derive(Eq)pub fn ConformanceReport::ok(Self) -> Bool
Whether every conformance check passed (no failures recorded).
enumEventOrderError
Why an outbound event stream is malformed. ASGI pins the order an application may emit messages in — a body before the response starts, a second start, a websocket frame before the handshake is accepted, and so on are all protocol violations. validate_events walks a stream and names the first violation it finds; a well-formed stream returns None. A server can run this over its own emissions to catch a buggy app early, and the conformance harness drives a negative table through it.
pub(all) enum EventOrderError {
BodyBeforeStart
DuplicateResponseStart
EarlyHintAfterStart
BodyAfterComplete
UnexpectedTrailers
TrailersBeforeBody
MissingResponseStart
IncompleteBody
MissingTrailers
EventAfterComplete
DebugAfterStart
DuplicateDebug
PushBeforeStart
PathSendMixedWithBody
IllegalResponseHeader
FrameBeforeAccept
DuplicateAccept
EventAfterClose
DenialBodyBeforeStart
DenialAfterAccept
AcceptAfterDenial
IncompleteDenial
MissingHandshakeReply
DuplicateLifespanReply
ShutdownBeforeStartup
NonResponseEvent
} derive(Eq)pub fn EventOrderError::describe(Self) -> String
A one-line human description of the violation, used in conformance failure names and server logs.
Functions
pub fn run_conformance() -> ConformanceReport
Run the ASGI 3.0 conformance harness: a table-driven battery that drives every @spec.Event variant and every Scope field through @http.run_http, ws_run, and the @client.TestClient, asserting round-trip fidelity — a value put in comes back unchanged, and no two distinct events or scope shapes are confused — and a negative table that asserts validate_events rejects every malformed ordering (a body before the response starts, a websocket frame before the handshake, a lifespan shutdown reply before startup, and the rest) with the exact violation. Returns a ConformanceReport naming any check that failed, so the suite (and any downstream server) can assert ok().
pub fn validate_events(@spec.Scope, Array[@spec.Event]) -> EventOrderError?
Validate an outbound event stream against the ordering rules for its scope, dispatching to the http / websocket / lifespan validator. The public entry a server calls to check an application's emissions before writing them to the wire.
pub fn validate_http_response(Array[@spec.Event]) -> EventOrderError?
Validate the outbound event stream an application emits for an http request, against ASGI's http response ordering: at most one HttpResponseStart, no body or pathsend/zerocopysend before it, early hints only ahead of it, a body stream that terminates once (more_body: false), trailers only when the start promised them and only after the body completes, and nothing after the response is fully sent. HttpResponseDebug must come once and before the start; HttpResponsePush only after it; HttpResponsePathSend cannot be mixed with a body. Returns the first violation, or None for a well-formed complete response.
pub fn validate_inbound(@spec.Event) -> EventOrderError?
Check an inbound event an application is about to act on. ASGI requires exactly one of bytes or text to be set on a websocket.receive, and a frame that sets neither, or both, is a server bug the application should not have to guess about. Returns None for a well-formed event.
pub fn validate_lifespan_replies(Array[@spec.Event]) -> EventOrderError?
Validate the outbound replies an application emits for a lifespan run: a startup reply (LifespanStartupComplete / LifespanStartupFailed) before a shutdown reply (LifespanShutdownComplete / LifespanShutdownFailed), each phase answered at most once. Returns the first violation, or None.
pub fn validate_ws_response(Array[@spec.Event]) -> EventOrderError?
Validate the outbound event stream an application emits for a websocket connection, against ASGI's handshake ordering: the connect must be answered with an WebSocketAccept, a WebSocketClose, or a denial (WebSocketHttpResponseStart + body); frames are only legal once accepted; nothing follows a close or a completed denial; a denial cannot mix with an accept. Returns the first violation, or None for a well-formed stream.