mooncat
An ASGI 3.0 server for MoonBit. It accepts a connection, turns each request into a scope, a receive and a send, and drives your application — over HTTP/1.1, HTTP/2, HTTP/3 and WebSocket, on one port.
moon add moonbitstack/mooncat
- ASGI 3.0the interface an app is written to
- h1 h2 h3HTTP over TCP and over UDP
- 12000the default port
- native · wasmserves where the async sockets are
What it doesthe whole job, in one picture
mooncat owns the half of a web server that is not the protocol: the listening socket, the connection, the request that arrives on it in pieces, and the answer that leaves in pieces. It hands the application a scope describing the request and two callables — one to read the body, one to write the response. Everything else, from routing to templates, belongs to the application.
One port, every version
The listener decides as the connection opens. Under TLS the choice is made in the handshake; in the clear it is the first bytes on the wire. Either way the request lands in the same place.
| On the wire | Decided by | What the application sees |
|---|---|---|
HTTP/1.1 | the request line, in the clear | an http scope; the body one piece at a time |
HTTPS | a TLS handshake with h2 and http/1.1 offered | the same scope, with scheme https and the certificate in extensions |
h2c | the connection preface PRI * HTTP/2.0 | the same scope, once per stream |
HTTP/2 | ALPN answering h2 | one scope per stream, each stream in its own task |
HTTP/3 | the same port number on UDP | one scope per stream, advertised by alt-svc |
ws · wss | Upgrade: websocket, at the start | a websocket scope: connect, accept, receive, send, close |
Quick start
moon add moonbitstack/mooncat moon add moonbitstack/moonasgi
An application is a function of three arguments. It answers with two events:
/// A server that greets whoever connects. 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\n", more_body=false)) } @mooncat.serve(app, host="127.0.0.1", port=12000) }
moon run src/main --target native curl -i http://127.0.0.1:12000/
$ curl --noproxy '*' -i http://127.0.0.1:12000/ HTTP/1.1 200 OK content-type: text/plain; charset=utf-8 server: mooncat date: Mon, 02 Nov 2026 10:11:23 GMT content-length: 19 hello from mooncat
A recorded runmoon run src/examples/09-tour
The tour example starts real servers on real sockets, drives them with real clients, prints what came back and stops. Nothing below is typed by hand.
$ moon run src/examples/09-tour --target native # configuration, as the command line reads it bind 127.0.0.1:12001 keep-alive 30000 ms retire after 100 trusted proxies 127.0.0.1, 10.0.0.0 tls false # lifespan: startup and shutdown bracket the listener order startup, serving, shutdown # an app on a socket, answered by a real client INFO: Waiting for application startup. INFO: Application startup complete. INFO: Started server process on http://127.0.0.1:12031 INFO: 127.0.0.1:56255 - "GET /echo/moonbit HTTP/1.1" 200 OK GET /echo/moonbit 200 moonbit INFO: 127.0.0.1:56256 - "GET / HTTP/1.1" 200 OK GET / 200 root server header mooncat date header true # keep-alive, and the request limit that retires a server three requests 200, 200, 200 the fourth refused, the server retired # a trusted proxy is believed, an untrusted one is not believed https from 203.0.113.7 ignored http from 127.0.0.1 # websocket: connect, echo, close sent 'one', got echo one sent 'two', got echo two # http/2 over cleartext listening h2c on 127.0.0.1:12036 try it with curl --http2-prior-knowledge http://127.0.0.1:12036/echo/h2 # graceful shutdown drains what is in flight stop asked for while it was still running the slow request 200 slow
The packages
A program that answers only over TCP links none of the UDP half. The split follows what a package costs to carry, not how it reads.
| Package | What it is for |
|---|---|
mooncat | the entry points, re-exported, so @mooncat.serve is all a small program needs |
mooncat/config | Config, the access and error log, the HTTP date |
mooncat/lifespan | startup before the listener, shutdown after it |
mooncat/proxy | the client and scheme a trusted proxy reports |
mooncat/serve | the accept loop, HTTP/1.1, HTTP/2, WebSocket, TLS, workers |
mooncat/cli | the command line, and run(app) as a whole program |
mooncat/quic | HTTP/3: the UDP socket, the datagram loop, and the streams |
Serving runs where the async sockets do — the native and wasm backends — and the test suite runs on both. The QUIC half is native only.