moonasgiASGI 3.0

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.

How this page is made

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 items

The 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 items

The 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 items

The 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 items

The 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 items

TestClient: 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 items

Application-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 items

Several 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 items

A 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 items

Drive 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 items

Check 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.