Sign in

    mooncat

    mooncat — a native ASGI 3.0 server for MoonBit (← uvicorn), built on moonbitlang/async and the moonasgi SEAM. HTTP/1.1, HTTP/2, WebSocket, TLS and HTTP/3, a uvicorn-shaped command line, and a process model with graceful shutdown; the protocols themselves are moonhttp, moontls and moonquic.

    asgi
    server
    uvicorn
    http
    websocket
    moonbit
    native
    Download zip
    Version
    0.18.1
    License
    Apache-2.0
    Last updated
    3 hours ago
    Downloads
    173

    #mooncat

    A native ASGI 3.0 server for MoonBit — ← uvicorn.

    Check and Test License mooncakes

    Moved on mooncakes from Lfan-ke/mooncat to moonbitstack/mooncat.

    mooncat runs a moonasgi application over a real network socket. It sits between moonbitlang/async's native HTTP transport and the ASGI SEAM: it accepts connections, turns each request into a Scope + Receive + Send, and drives your app — exactly the role uvicorn plays for Python.

    flowchart LR net["moonbitlang/async<br/>(sockets · TLS · HTTP/1.1)"] --> cat["**mooncat**<br/>accept loop"] cat -->|"Scope / Receive / Send"| asgi(["moonasgi SEAM"]) asgi --> app["your app<br/>(moonapi, …)"]

    #Quickstart

    serve is native-only — it opens a socket — and it is async, so the entry point is async fn main and the target is --target native:

    async fn main {
    let app : @moonasgi.AsgiApp = (_scope, _receive, send) => {
    send(
    @moonasgi.Event::HttpResponseStart(
    status=200,
    headers=[("content-type", "text/plain; charset=utf-8")],
    trailers=false,
    ),
    )
    send(
    @moonasgi.Event::HttpResponseBody(body=b"Hello from mooncat!", more_body=false),
    )
    }
    @mooncat.serve(app, host="127.0.0.1", port=12000) catch {
    err => println("mooncat stopped: \{err}")
    }
    }

    moon run src/main --target native curl http://127.0.0.1:12000

    moon.mod needs moonbitstack/mooncat and moonbitstack/moonasgi; the package importing them needs both in its moon.pkg. moon run examples/00-hello--target native is the same program in this repository.

    serve blocks in a keep-alive accept loop until its task is cancelled. Each request is bridged faithfully: the method/path/query/headers become an http Scope, the request body streams through Receive, and HttpResponseStart / HttpResponseBody events are written back via the connection.

    #Command line

    uvicorn main:app --port 9000 names the app by string because Python imports by string. MoonBit links it in, so that half does not carry over — but the half a user types does. @cli.run(app) is the whole body of a server program:

    async fn main {
    @cli.run(app) catch {
    err => println("mooncat: \{err}")
    }
    }

    moon run src/main --target native -- --port 9000 --log-level debug moon run src/main --target native -- --help

    --help lists every flag. The names and meanings are uvicorn's, including --host, --backlog, --root-path, --limit-concurrency, --limit-max-requests, --timeout-keep-alive, --timeout-graceful-shutdown, --h11-max-incomplete-event-size, --forwarded-allow-ips, --ws-max-size, --ssl-certfile with --ssl-keyfile, and the --no- form of every switch. mooncat adds --pfx-file (SChannel takes PKCS#12, not PEM), --h2c, --max-connections, --dual-stack and --reuse-addr.

    A uvicorn flag mooncat cannot honour says why, rather than calling it a typo:

    $ moon run src/main --target native -- --workers 4 mooncat: --workers: mooncat serves from one process; there is no fork here

    That covers --reload, --workers, --uds, --fd, --factory, --app-dir, --interface, --loop, --http, --ws, --log-config, --env-file, the three WebSocket timing knobs, and the --ssl-* options beyond the two above. Each answers with its own reason. Silently ignoring them is worse: somebody who writes --workers 4 would find out at load time.

    cli is a subpackage because reading argv is a program's business, not a library's — mooncat embedded in something larger never touches the command line. @mooncat.serve(app, port=12000) is unchanged, and both ways of starting a server stand.

    @cli.parse(argv) on its own returns the Plan — a Config, whether to wrap it in TLS, whether to speak h2c — without opening anything, which is how it is tested and how a program can look at the plan before serving.

    #HTTPS / TLS

    serve_tls serves the same ASGI app over TLS (← uvicorn's --ssl-certfile / --ssl-keyfile):

    // All three are required arguments; `serve_tls` reads the pair its platform
    // accepts and ignores the other, so one call compiles everywhere.
    @mooncat.serve_tls(
    app,
    certificate_file="certs/cert.pem", // PEM cert, OpenSSL platforms
    private_key_file="certs/key.pem", // PEM key, OpenSSL platforms
    pfx_file="certs/dev.pfx", // PKCS#12, Windows and SChannel
    port=12000,
    )

    Each accepted connection completes a real TLS handshake (@tls.Tls), then moonhttp/http1's RFC 9112 codec — which frames single-shot replies with Content-Length and streamed replies with Transfer-Encoding: chunked, and refuses the request-smuggling framings rather than resolving them — drives the moonasgi app over the encrypted stream. A pipelining client's second request, or a body that arrived with its head, is held for what comes next rather than dropped, with keep-alive and the lifespan protocol intact. Proven end-to-end by a real @http TLS client doing GET over https:// and asserting 200 + body in CI. Regenerate the throwaway localhost test cert with scripts/gen-cert.sh.

    Design boundaries (honest, not stubs):

    • The certificate is supplied as PEM on OpenSSL platforms (Linux/macOS) and as a PKCS#12 .pfx on Windows (SChannel), because that is what each moonbitlang/async TLS backend accepts; serve_tls selects the right form at compile time.
    • moonbitlang/async exposes its server-side TLS constructor as #internal ("for internal testing only") — it is the only such entry point, and the async suite itself serves HTTPS through it — so mooncat opts into that one alert (warnings = "-alert_internal" in moon.pkg) and will migrate the moment a public API lands.
    • WebSocket over TLS (wss://) works: an upgrade on a TLS connection goes through mooncat's own HTTP/1.1 and WebSocket code over the encrypted stream, with the negotiated subprotocol echoed in the 101. A test does the upgrade with a real TLS client and checks the echoed frame.

    #Process model

    serve_graceful runs the app under uvicorn's process model — a graceful shutdown and a --reload file watcher:

    let handle = @mooncat.ShutdownHandle::new()
    g.spawn(() => @mooncat.serve_graceful(app, @mooncat.Config::new(port=12000), handle~))
    // … later, from anywhere:
    handle.shutdown() // stop accepting → drain in-flight → lifespan shutdown → close

    handle.shutdown() blocks until the port is free again. The sequence is fixed and verified end-to-end in CI: a slow request that is still in flight when shutdown fires is allowed to finish (the client still sees 200) before lifespan shutdown runs, and the listener is closed only afterwards — a real socket integration test pins the ordering, and a mutation test (running lifespan shutdown before the drain) turns it red. A shutdown can also come from a signal: wire @signal.set_global_cancellation_signals and mooncat runs lifespan shutdown + close under protect_from_cancel on the way down.

    Every entry point (serve, serve_config, serve_tls_config, serve_graceful) runs this one loop, so they share the same limits, keep-alive, pipelining and WebSocket bridge; serve_graceful only adds the handle. A request body reaches the app as it arrives, one http.request event per piece with more_body until the last, so an upload is never held whole in memory; a body the app leaves unread closes the connection rather than being read as the next request. A head that cannot be parsed is answered 400 (431 when it passes max_head_size) with uvicorn's Invalid HTTP request received. before the connection closes. A real @websocket client doing a text + binary round-trip against a serve_graceful server verifies the bridge in CI.

    reload_watch(dir, on_reload) watches a source tree with @fs.Watcher and fires on_reload on each change — hook it to a ShutdownHandle to turn a file save into a graceful restart.

    Multi-worker boundary: uvicorn's --workers binds the port once in the parent and hands the socket to N worker processes started with multiprocessing's spawn context. moonbitlang/async can start a process but cannot give it a listening socket, and its event loop permits only one outstanding accept per listener (a second concurrent accept on the same handle aborts), so N in-process acceptor tasks aren't expressible either. mooncat serves from one acceptor that spawns a concurrent handler per connection, the same concurrency a single uvicorn worker gives on its event loop. Multiple workers land when the async layer can pass a listening socket to a child process.

    #HTTP/2 (h2c)

    serve_h2c serves the same ASGI app over HTTP/2 cleartext — the prior-knowledge, no-TLS HTTP/2 profile a client reaches with curl --http2-prior-knowledge or a gRPC client:

    @mooncat.serve_h2c(app, host="127.0.0.1", port=12000)

    The framing is moonhttp/http2 (RFC 9113) and the header compression moonhttp/hpack (RFC 7541) — the same two that carry real gRPC in moonrpc, which is why neither lives in either server. mooncat binds it to ASGI: it reads the client connection preface + SETTINGS, HPACK-decodes each request's HEADERS block into an Http Scope (the :method / :path / :scheme / :authority pseudo-headers plus the ordinary headers), streams request DATA to the app as the Receive body, and encodes the response — a HEADERS frame with the :status pseudo-header, then DATA — back over the connection. Requests multiplex on their own stream ids, and the response DATA is split to the peer's maximum frame size and clamped to the connection- and stream-level send windows, resuming on WINDOW_UPDATE (RFC 9113 §6.9).

    Proven end to end in CI by a real HTTP/2 (h2c) client built on @socket.Tcp and the same frame + HPACK codecs: it GETs a route and reads 200 + body, POSTs a body the app echoes, drives two multiplexed streams on one connection, and — with a deliberately small advertised window — checks the server clamps each DATA frame to the granted window. The h2c greet test reuses the hand-written ASGI handler from the HTTP/1.1 test: /greet/moon returns JSON with 200, and an unmatched path returns 404.

    h2c rather than h2-over-TLS because the moonbitlang/async TLS layer exposes no ALPN, so the protocol can't be negotiated on a TLS connection yet; h2c is the direct, ALPN-free path. The underlying dependencies provide QUIC and HTTP/3 protocol components, but mooncat does not currently expose an ASGI-over-HTTP/3 server.

    #Packages

    PackageWhatWhat it costs to link
    mooncatRe-exports the four below, so @mooncat.serve works as beforeall four
    mooncat/configConfig, access and error logging, the HTTP datemoonlog, moondate
    mooncat/lifespanthe ASGI lifespan protocolmoonasgi
    mooncat/proxyforwarded headers from a trusted proxy—
    mooncat/serveThe accept loop, workers and reload, HTTP/1.1, h2c, WebSocket, TLSmoonasgi, moonhttp's http1 / http2 / hpack / ws / upgrade, moonbitlang/async
    mooncat/cliuvicorn's flags, and run(app)the above, plus core's argparse
    mooncat/quicQUIC and HTTP/3: the UDP socket, the datagram loop, the handshake QUIC does inlinemoonquic, moontls, mooncred's certificate reader, moonhttp/http3

    QUIC is separate because of that third column. It performs its own TLS 1.3 handshake rather than riding a record layer, so it is the only half of mooncat that needs those four; a server answering HTTP/1.1 links none of them.

    #Status

    v0 — HTTP/1.1 request → ASGI Scope/Receive/Send → response is working and verified by a real socket round-trip in CI (a server task answers a live @http.get). Landed and CI-verified alongside it:

    • the lifespan protocol — the app runs once under a Lifespan scope, startup is driven before the listener binds and shutdown on the way out, even under cancellation;
    • the full WebSocket frame↔Event bridge — a Connection: upgrade + Upgrade: websocket handshake becomes a websocket Scope driven through the moonasgi SEAM: receive() emits websocket.connect then real inbound text/binary frames as WebSocketReceive (fragments reassemble) and a peer close as WebSocketDisconnect(code); send() turns WebSocketAccept into the deferred 101 handshake, WebSocketSendText/WebSocketSendBytes into message frames, and WebSocketClose(code, reason) into a close frame. WebSocketAccept(subprotocol=..) echoes the choice in the 101's Sec-WebSocket-Protocol. Rejecting before accept answers 403; pings are answered and, as in uvicorn, not surfaced to the app. The receive side enforces RFC 6455 rather than trusting the peer: a set RSV bit, a reserved opcode, an unmasked client frame and a malformed control frame each fail the connection with 1002, a payload past ws.max_size with 1009 — checked before the payload is read, so a 64-bit length is never truncated into a short allocation — invalid UTF-8 in a text message with 1007, and an inbound close is echoed per §5.5.1. Proven by a real @websocket client doing a full text + binary round-trip and a clean close in CI;
    • a Config in the shape of uvicorn's, and honoured rather than merely recorded: dual_stack, reuse_addr, per-server response headers, max_connections, allow_failure, timeout_keep_alive (an idle keep-alive connection is dropped), limit_max_requests (the server stops itself), limit_concurrency (uvicorn's 503 rather than a queue), max_head_size (uvicorn's h11_max_incomplete_event_size), date_header, proxy_headers / forwarded_allow_ips, and ws.max_size. backlog is the one field that does nothing: @socket.TcpServer calls listen() itself with a fixed depth and exposes no way to change it. Chunked request/response framing is handled by the codec.

    mooncat also serves a small hand-written ASGI handler end to end: the HTTP/1.1 integration test answers /greet/<name> with JSON over a live socket, and verifies both 200 and a 404 route miss. A matching h2c test exercises the same handler over HTTP/2.

    HTTPS/TLS serving, the graceful-shutdown + --reload process model, and HTTP/2 (h2c) serving landed too — see the sections above. Roadmap, transliterated from uvicorn feature-by-feature: further TLS detail (ciphers/mTLS), h2 over TLS once the async layer exposes ALPN, multiple workers once it can pass a listening socket to a child process, backlog once TcpServer takes one, and the remaining WebSocket knobs — ws-ping-interval / ws-ping-timeout / ws-max-queue need a reader that can be interrupted between messages and resumed, and ws-per-message-deflate needs a raw DEFLATE codec the async gzip package does not expose.

    #Native only

    The moonbitlang/async HTTP server is native-only — there is no JS server backend — so mooncat builds and runs on the native target.

    #License

    Apache-2.0.

    #What this server does not implement

    The algorithms are not here. AES, GCM, SHA-1, SHA-256, HMAC, HKDF, X25519, ECDSA and the DER codec live in mooncrypt; X.509 lives in mooncred; the levels and the sink behind the log lines are moonlog's; base64 is moonbase's.

    The protocol implementations come from mooncrypt, mooncred, moontls, moonhttp, and moonquic. mooncat owns the server-side integration: socket listeners, ASGI request and event bridging, graceful process handling, and TLS and QUIC endpoint binding. It does not currently provide an ASGI-over-HTTP/3 server.

    #Listening

    The TCP serving entry points default to port 12000. HTTP, HTTPS, WebSocket, and h2c are separate server modes; each binds its own listener, so they cannot all share that port at the same time. Config::new(port=...) changes the TCP port. quic_serve binds the UDP address passed by the caller; UDP and TCP use separate port spaces, so they may use the same number.

    Config

    Server configuration (← uvicorn Config): the bind address, the listen backlog, and the HTTP/1.1 transport knobs the moonbitlang/async server exposes. Constructed once and handed to serve_config.

    backlog mirrors uvicorn's listen(2) backlog. The current moonbitlang/async transport does not expose a backlog setter on its TCP listener — TcpServer calls listen() itself with a fixed depth and offers no way to re-listen — so the value is recorded on the config for parity and future use but is not applied to the socket. This is a transport-capability gap, not a behavioural choice, and it is the one field here that does nothing.

    The remaining fields map one-to-one onto knobs mooncat does honour: dual_stack / reuse_addr on the listening socket, server_headers stamped onto every response (uvicorn's Server: header lives here), max_connections for the parallel-client ceiling, and allow_failure for whether a handler error tears the whole server down.

    Lifespan

    Drives an ASGI application's lifespan protocol (← uvicorn LifespanOn).

    A single long-lived invocation of the app runs under a Lifespan scope. The server and that invocation exchange messages through two async queues: the server pushes lifespan.startup / lifespan.shutdown onto inbox (which backs the app's Receive), and the app pushes its *.complete / *.failed replies onto outbox (which the app's Send writes to). state seeds the lifespan scope and is shared into request scopes by the caller.

    LifespanError

    Raised when an application reports lifespan.startup.failed or lifespan.shutdown.failed, carrying the failure message the app supplied.

    Logger

    How much the server says, and where it goes.

    ShutdownHandle

    A handle for driving a graceful shutdown of serve_graceful from outside the serving task (← uvicorn's Server.should_exit / handle_exit). Hand one to serve_graceful, then call shutdown() from any other task to stop the server: it stops accepting, waits for in-flight requests to drain, runs the ASGI lifespan shutdown, and closes the listener, in that order.

    Two async queues carry the handshake. request receives the shutdown trigger (a signal delivered through the runtime's global cancellation reaches the server the same way — see serve_graceful). done is posted once the server has finished the whole shutdown sequence, so shutdown() can block until the port is actually free.

    TlsCert

    TLS certificate material for HTTPS serving (← uvicorn's ssl_certfile / ssl_keyfile). The backend the moonbitlang/async TLS layer uses is platform-specific, so both forms are carried:

    • certificate_file + private_key_file — PEM files, used by the OpenSSL backend (Linux / macOS, the CI platforms);
    • pfx_file — a PKCS#12 bundle, used by the SChannel backend (Windows).

    serve_tls picks the right pair at compile time. Supply whichever your deployment targets; a self-signed certs/ pair for tests is generated with scripts/gen-cert.sh.

    WsConfig

    The WebSocket knobs uvicorn exposes on its own config (--ws-max-size).

    max_size is the ceiling on a received payload, in bytes: it bounds a single frame and, across continuation frames, a reassembled message, and a peer that announces more fails the connection with 1009. It is the knob with teeth on the receive side — an unbounded length is exactly what a hostile client sends.

    uvicorn's remaining WebSocket knobs are not here rather than here-and-ignored: --ws-ping-interval / --ws-ping-timeout need a heartbeat task alongside a reader that can be interrupted between messages and resumed, --ws-max-queue needs that same reader pumping a bounded queue, and neither is expressible while a native blocking read cannot be cancelled and resumed mid-stream; --ws-per-message-deflate needs a raw DEFLATE codec, which the async library's gzip package (gzip-framed streams only) does not provide. Accepting the extension without one would corrupt every frame.

    http_date

    fn http_date(ms : Int64) -> String

    Format a Unix-epoch millisecond count as an HTTP-date in the IMF-fixdate form RFC 7231 §7.1.1.1 makes mandatory for a Date header: Sun, 06 Nov 1994 08:49:37 GMT, always GMT, always fixed-width.

    The calendar and the format are moondate's; what is here is the millisecond clock mooncat reads, brought down to the whole second the header is written to.

    http_date_now

    fn http_date_now() -> String

    The current instant as an HTTP-date, reformatted only when the epoch second has moved on.

    port

    let port : Int

    The default port for the TCP serving entry points.

    HTTP/1.1, including WebSocket upgrades, uses one listener. TLS and h2c are separate server modes with their own listeners, so they cannot all bind this port at once. quic_serve takes an explicit UDP address; it may use the same number because UDP and TCP have separate port spaces.

    proxy_rewrite

    fn proxy_rewrite(headers : Map[String, String], client : (String, Int)?, scheme : String, trusted~ : Array[String], websocket? : Bool) -> ((String, Int)?, String)

    Rewrite a request's client and scheme from a trusted proxy's forwarding headers (← uvicorn's ProxyHeadersMiddleware).

    Nothing is rewritten unless the peer itself is trusted: these headers are attacker-controlled, and believing them from an arbitrary client lets that client name any address it likes for a rate limiter, an allowlist or an audit log. Behind a proxy the peer is the proxy, which is why forwarded_allow_ips defaults to 127.0.0.1 and not to *.

    X-Forwarded-For yields the chain's leftmost entry with port 0, since a forwarded chain carries no port. X-Forwarded-Proto replaces the scheme, mapped to wss/ws for a WebSocket scope, whose scheme names differ from the https/http the header spells.

    read_frame

    async fn read_frame(reader : &
    Reader
    , client? : Bool, limit? : Int) ->
    Frame

    One frame off a stream (RFC 6455 §5.2).

    The header is read in the order the format is laid out — two bytes, then the extended length, then the mask — and @ws.check runs on the header before the payload is read, so an enormous announced length is refused rather than allocated.

    client is the server's position by default: a client's frames must be masked (§5.1), so reading a server's frames takes client=false. Raises @ws.Refused carrying the status the connection is then failed with, and passes the stream's end through.

    read_message

    One complete message off a stream, with the control frames between its fragments handled: a ping is answered on writer, a pong is dropped, and a close is echoed before the message comes back as that close.

    This is the server's receive path, so every frame must arrive masked. The reply frames go out unmasked because a server must not mask (§5.1).

    reload_watch

    async fn reload_watch(path : String, on_reload : async () -> Unit) -> Unit

    Watch path for source changes and fire on_reload on each batch of events (← uvicorn's --reload file watcher). Backed by @fs.Watcher, which debounces and reports child-file events, so a save to any file under path triggers one reload. on_reload is where a supervisor re-execs the server; wiring it to a ShutdownHandle turns a file save into a graceful restart. Loops until its task is cancelled, always closing the watcher.

    serve

    async fn serve(app : async (
    Scope
    , async () ->
    Event
    , async (
    Event
    ) -> Unit) -> Unit, host? : String, port? : Int, backlog? : Int) -> Unit

    Serve a moonasgi ASGI application over native HTTP/1.1 + WebSocket (← uvicorn). Convenience wrapper over serve_config that builds a Config from host / port / backlog. Blocks in a keep-alive accept loop until the running task is cancelled.

    serve_config

    Serve a moonasgi ASGI application under an explicit Config.

    Runs the full lifespan protocol around the accept loop: the app is invoked once under a Lifespan scope and startup() is driven before the listener is bound, and shutdown() is driven on the way out — even when the serving task is cancelled, via protect_from_cancel. Each accepted request is bridged by dispatch, which diverts WebSocket upgrades to the echo path.

    serve_graceful

    Serve a moonasgi application with a uvicorn-style process model: a graceful shutdown path over a single acceptor that spawns a concurrent handler per connection.

    The lifespan protocol runs startup before the listener binds and shutdown on the way out. A single acceptor task spawns one handler task per connection, so requests are served concurrently, each through mooncat's HTTP/1.1 codec and, on an upgrade, its WebSocket bridge. serve, serve_config and serve_tls_config run this same loop; this entry point adds the handle.

    Shutdown is triggered by handle.shutdown() / handle.request_stop(), by reaching Config::limit_max_requests, or by a signal the runtime turns into global cancellation (see the boundary note below). All three converge on one sequence, run under protect_from_cancel so a signal can't abort it midway: stop accepting (cancel the acceptor), drain in-flight requests (bounded by Config::graceful_timeout), run the ASGI lifespan shutdown, then close the listener.

    The acceptor honours the connection ceilings: max_connections gates accepting, so a full server leaves the next client in the listen queue rather than accepting it and sitting on it, and allow_failure decides whether one bad connection is contained or brings the server down. limit_concurrency is the other kind of ceiling — a request arriving past it is answered 503 rather than queued.

    Multi-worker boundary

    uvicorn's --workers binds the port once in the parent (SO_REUSEADDR, an inheritable socket) and hands that socket to N worker processes it starts with multiprocessing's spawn context. moonbitlang/async can start a process but cannot give it a listening socket, and its event loop allows only one outstanding accept per listener handle (wait_read guards on a single waiter), so even N in-process acceptor tasks on one shared listener aren't expressible — a second concurrent accept on the same listener aborts. mooncat therefore serves from one acceptor that spawns a concurrent handler per connection, which is the concurrency a single uvicorn worker provides on its event loop. Multiple workers land when the async layer can pass a listening socket to a child process.

    Signal boundary

    The only signal hook moonbitlang/async exposes is @signal.set_global_cancellation_signals, which cancels the whole task tree on SIGINT/SIGTERM. mooncat catches that cancellation and still runs lifespan shutdown and closes the listener under protect_from_cancel; but a signal also cancels the in-flight handler tasks, so drain-before-close is only fully honoured on the programmatic ShutdownHandle path. That matches uvicorn's own escalation: a first signal drains, a second forces exit.

    serve_h2c

    async fn serve_h2c(app : async (
    Scope
    , async () ->
    Event
    , async (
    Event
    ) -> Unit) -> Unit, host? : String, port? : Int) -> Unit

    Serve a moonasgi ASGI application over HTTP/2 cleartext (h2c) — the prior-knowledge, no-TLS HTTP/2 profile (RFC 7540 §3.4) — reusing moonrpc's self-built HTTP/2 + HPACK engine as the transport. Convenience wrapper over serve_h2c_config that builds a Config from host / port. Blocks in the accept loop until the running task is cancelled.

    h2c rather than h2-over-TLS because the moonbitlang/async TLS layer exposes no ALPN, so the protocol can't be negotiated on a TLS connection yet; h2c is the direct, ALPN-free path a client reaches with prior knowledge (as curl --http2-prior-knowledge or a gRPC client does).

    serve_h2c_config

    Serve a moonasgi ASGI application over h2c under an explicit Config. Mirrors serve_config / serve_tls_config: the ASGI lifespan protocol runs around the accept loop (startup before the listener binds, shutdown on the way out — even under cancellation, guarded by protect_from_cancel), and each accepted connection is driven by drive_h2c over the self-built HTTP/2 transport.

    serve_tls

    async fn serve_tls(app : async (
    Scope
    , async () ->
    Event
    , async (
    Event
    ) -> Unit) -> Unit, certificate_file~ : String, private_key_file~ : String, pfx_file~ : String, host? : String, port? : Int, backlog? : Int) -> Unit

    Serve a moonasgi ASGI application over native HTTP/1.1 over TLS (HTTPS, ← uvicorn's --ssl-certfile/--ssl-keyfile). Convenience wrapper over serve_tls_config that builds the Config from host/port/backlog and the certificate paths.

    Runs the full ASGI lifespan protocol around the accept loop (startup before the listener binds, shutdown on exit — even under cancellation), then, for every accepted connection, completes a TLS handshake and drives the app through the encrypted HTTP/1.1 codec.

    A WebSocket upgrade on one of these connections is served too (wss://): it goes through the same self-built HTTP/1.1 path, so the 101, the subprotocol echo and the frames all travel over the encrypted stream.

    serve_tls_config

    Serve a moonasgi ASGI application over HTTPS under an explicit Config and TlsCert. Mirrors serve_config: lifespan startup is driven before the listener binds and shutdown on the way out (guarded by protect_from_cancel so it still runs when the serving task is cancelled), and each accepted connection completes a TLS handshake and then runs the same keep-alive loop as a plain one.