A native ASGI server for MoonBit.
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/
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 mooncatmoon add moonbitstack/mooncat
moon add moonbitstack/moonasgi| 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 1.3 handshake, h2 and http/1.1 offered by ALPN | 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 a task of its own |
| HTTP/3 | the same port number on UDP | one scope per stream, advertised with alt-svc |
| ws, wss | Upgrade: websocket on an ordinary request | a websocket scope: connect, accept, receive, send, close |
| Function | What it starts |
|---|---|
| serve(app, host=, port=, backlog=) | HTTP/1.1 and HTTP/2, one listener |
| serve_config(app, config) | the same, under an explicit Config |
| serve_tls(app, certificate_file~, private_key_file~, ..) | HTTPS, and the clear on the same port unless cleartext=false |
| serve_tls_config(app, config, cert) · serve_tls_server(app, config, server) | HTTPS with a certificate or a @tls.Server you built |
| serve_h2c(app, host=, port=) · serve_h2c_config(app, config) | HTTP/2 in the clear, and nothing else |
| serve_graceful(app, config, handle=) | the same loop, with a ShutdownHandle that drains before it closes |
| @quic.serve_config(app, config, cert, ..) | HTTP/1.1, HTTP/2 and HTTP/3 under one certificate |
| @cli.run(app) | a whole server program, configured from the command line |
async fn main {
let app : @moonasgi.AsgiApp = (_scope, _receive, send) => {
send(@moonasgi.Event::HttpResponseStart(status=200, headers=[], trailers=false))
send(@moonasgi.Event::HttpResponseBody(body=b"configured\n", more_body=false))
}
let config = @mooncat.Config::new(
port=12000,
limit_concurrency=64,
limit_max_requests=1000,
server_headers=Map([("server", "mooncat")]),
)
@mooncat.serve_config(app, config)
}async fn main {
let app : @moonasgi.AsgiApp = (_scope, _receive, send) => {
send(@moonasgi.Event::HttpResponseStart(status=200, headers=[], trailers=false))
send(@moonasgi.Event::HttpResponseBody(body=b"ok", more_body=false))
}
@cli.run(app)
}$ ./app --help
Usage: mooncat [options]
Serve an ASGI application over HTTP/1.1, HTTP/2, HTTPS or HTTP/3.
Options:
-h, --help Show help information.
-V, --version Show version information.
--[no-]date-header stamp a Date header on every response
--[no-]server-header stamp a Server header
--[no-]proxy-headers believe X-Forwarded-* from the allowed peers
--[no-]access-log log one line per request
--dual-stack accept IPv4 and IPv6 on one socket
--h2c speak HTTP/2 in the clear instead of HTTP/1.1
--http3 serve HTTP/3 on UDP beside HTTPS, on the same port
--host <host> address to bind [env: MOONCAT_HOST]
--port <port> port to bind [env: MOONCAT_PORT]
--limit-concurrency <limit-concurrency> refuse with 503 above this many in flight
--timeout-keep-alive <timeout-keep-alive> seconds an idle keep-alive connection is held
--lifespan <lifespan> auto, on or off
--ssl-certfile <ssl-certfile> PEM certificate; serves HTTPS
--ssl-keyfile <ssl-keyfile> PEM private key; serves HTTPS
$ ./app --uds /tmp/app.sock
mooncat: --uds: the async transport binds TCP only| Field | Default | What it does |
|---|---|---|
| host, port | "127.0.0.1", 12000 | where the listener binds |
| max_connections | none | above this, a client waits at the socket |
| limit_concurrency | none | above this, the request is answered 503 |
| limit_max_requests | none | the server retires once it has served this many |
| timeout_keep_alive | 5000 ms | an idle keep-alive connection is closed after this |
| timeout_handshake | 60000 ms | a TLS handshake that has not finished is dropped |
| max_head_size | 16384 | a request head past this is answered 431 |
| server_headers | {"server": "mooncat"} | added to every response |
| date_header | true | stamp a date on every response |
| root_path | "" | the prefix something in front has stripped |
| lifespan | Auto | Auto, On or Off |
| cleartext | true | on a TLS port, serve connections that start without a handshake |
| proxy_headers | true | believe forwarded headers from forwarded_allow_ips |
| workers | 1 | processes on one port, on Linux |
| ws | 16 MiB messages, 32 queued, 20 s pings | the WebSocket side |
| h2 | 100 streams, 64 KiB fields, 16 KiB frames, 1 MiB windows | the HTTP/2 side |
| Package | What is in it | What it costs to link |
|---|---|---|
| mooncat | the entry points, re-exported | all of them |
| mooncat/config | Config, the access and error log, the HTTP date | the log, the date |
| mooncat/lifespan | the lifespan protocol | — |
| mooncat/proxy | forwarded headers from a trusted proxy | — |
| mooncat/serve | the accept loop, HTTP/1.1, HTTP/2, WebSocket, TLS, workers | the transport and the protocol codecs |
| mooncat/cli | the command line, and run(app) | the package above, plus the argument parser |
| mooncat/quic | HTTP/3: the UDP socket, the datagram loop, the streams | QUIC and its TLS |
moon run src/examples/01-hello --target native
moon run src/examples/09-tour --target nativefn http_date(ms : Int64) -> Stringfn http_date_now() -> Stringlet port : Intasync fn reload_watch(path : String, on_reload : async () -> Unit) -> Unitasync fn serve_graceful(app : async (Scope, async () -> Event, async (Event) -> Unit) -> Unit, config : Config, handle? : ShutdownHandle) -> Unit