std.net.http_server
Create a server, register routes, and listen. Each route maps a path to a handler(req) function. The engine calls handlers on background fibers when requests arrive.
Minimal REST API
var srv = std.net.http_server()
srv.routes([{
path: "/greet", handler: fn(req) {
var who = "world"
if (req.query.name != nil) { who = req.query.name } # /greet?name=Bob
return "hello, " + who # plain string → 200 text/plain
}
}, {
path: "/json", handler: fn(req) {
return {
status: std.net.http_codes.ok,
content_type: "application/json",
body: std.json.stringify({result: 42})
}
}
}, {
path: "*", handler: fn(req) { # catch-all
return {
status: std.net.http_codes.not_found,
content_type: "application/json",
body: std.json.stringify({error: "nope"})
}
}
}])
srv.listen(8200)
print("listening on", srv.port()) # actual port (useful when 0)
std.async.sleep(5000) # keep serving: without this the script exits and the server dies with it
srv.stop()
Handler input — the req object
| Field | Description |
|---|---|
req.method |
"GET", "POST", … |
req.path |
"/greet" (no query string) |
req.query |
Parsed query object, e.g. {name: "Bob"} for ?name=Bob |
req.headers |
Request headers object |
req.raw_headers |
Raw header block string |
req.body |
Request body string |
req.upgrade |
Lowercased Upgrade header ("websocket" for WS handshakes, "" otherwise) |
Handler output — string or response object
Return a plain string for an HTTP 200 with text/plain:
handler: fn(req) { return "hello" } # 200 text/plain
Or return a response object for full control:
| Key | Purpose | Default |
|---|---|---|
status |
HTTP status code | 200 |
content_type |
Content-Type header |
text/plain or guessed for file |
headers |
Extra response headers object | {} |
body |
Response body string | "" |
file |
Path to a file to serve | — |
File serving example:
# inside a handler:
return {file: "./public/index.html"} # content-type guessed from extension
When a response object carries both file and body, file wins and
body is ignored — never set both. (status, headers, and
content_type still apply to file responses.)
Routing
- First matching route wins — order your
routes([...])array from specific to generic. path: "*"is the catch-all; place it last.- Registering
routesreplaces the previous table — call it once with the full array. route_add({path, handler})adds one route at runtime (replaces any route with the same path; returns the route count).route_remove(path)removes every route with that exact path (returnstruewhen anything was removed). Both are safe to call while serving.
Persistent connections (keep-alive)
By default the server closes each connection after one request. set_keep_alive enables HTTP/1.1 persistence — several requests per connection:
srv.set_keep_alive({idle_timeout_ms: 30000, max_requests: 100})
# or srv.set_keep_alive(false) to go back to close-per-request
idle_timeout_ms(default30000) — quiet connections are reaped after this gap between requests (slow-loris protection stays on either way).max_requests(default100) — cap per connection, then close.ws_idle_timeout_ms(default0= never) — reap idle upgraded websockets; see below.
on_disconnect(fn({method, path, status})) fires once per HTTP connection close with the last completed exchange — an access-log hook (status 0 means the transport died mid-request). Upgraded websocket connections report via their on_close instead:
srv.on_disconnect(fn(info) {
print(info.method, info.path, "->", info.status)
})
Websocket upgrade
Any route can become a websocket endpoint by returning {websocket: true} from its handler. The request must actually be a websocket handshake (Upgrade: websocket + Sec-WebSocket-Key); otherwise the server answers 400:
srv.routes([{path: "/ws", handler: fn(req) {
if (req.upgrade != "websocket") { return {status: 400, body: "ws only"} }
return {websocket: true,
on_message: fn(id, data, binary) { srv.ws_send(id, "echo:" + data) },
on_close: fn(id) { print("ws", id, "closed") }}
}}])
on_message(id, data, is_binary)fires per message;pingframes are answered internally.srv.ws_send(id, text | {data, binary})sends a frame (returnsfalsefor unknown/gone ids);srv.ws_close(id)closes one session.- Unmasked client frames violate RFC 6455 — the server answers with a close frame and drops the connection (it stays up for everyone else).
- Works over TLS (
wss://) when the server listens with{cert, key}.
No trailing commas in object literals — after a member whose value is a
multi-line fn body, write the closing } of the handler directly followed
by the object’s }, as in the example above. A trailing comma fails with
EXPECTED_TOKEN: Expected object key but got: CLOSE_CURLY.
Lifecycle and errors
srv.listen(8200, "127.0.0.1") # port [, host] — a {cert, key} third argument serves TLS
print(srv.is_running()) # true
print(srv.port()) # actual port
srv.stop() # halt and close
print(srv.last_error()) # most recent background error or nil
- Handlers run on background fibers — the main script must
sleep/awaitor the server exits with the script. - Handler exceptions and TLS handshake failures have no caller to throw to — they are stored per-server and readable via
server.last_error()(also echoed to stderr). See Concurrency — Errors. - TLS: pass a
{cert, key}third argument tolisten()(PEM file paths) to servehttps://— requires an OpenSSL build, elseUNSUPPORTED(see Overview). - Use
port: 0to let the OS pick a free port, then sharesrv.port()with clients.
See also
- HTTP Client — request the routes you serve
- URL / DNS
- Overview — TLS note