HTTP
In these examples the server and the client live in one program: the server takes a free port (port: 0), the client talks to 127.0.0.1, then the server stops.
This is also how tests of HTTP code are built: neither the external network nor fixed ports are needed.
A server with routes#
Task. Serve JSON on a path with a parameter and answer errors correctly.
use std.http { Server, ServerConfig, Router, Request, Reply, Client }
use std.ser { toJson }
data Todo { id: i64; title: str; done: bool }
fn todo(req: Request) -> Reply {
let id = (req.param("id") ?? "0").parseInt() // :id from the path is a route parameter
match id {
i64 n if n == 1 => Reply.json(toJson(&Todo{ id: 1, title: "write docs", done: true })!)
i64 _ => Reply.text("no such todo", 404)
ParseError => Reply.text("bad id", 400)
}
}
fn run() -> () | NetError | DecodeError | Panicked net, io, time {
let srv = Server.bind(ServerConfig{ port: 0, followNode: false })? // port 0: the system picks a free port
defer srv.close()
let stop = srv.handle()
let base = "http://127.0.0.1:{srv.port()}"
let router = Router.new()
.get("/health", (_: Request) => Reply.text("ok"))
.get("/todos/:id", todo)
scope {
spawn stop.serve(router) // the server runs in a sibling task
let a = Client.get("{base}/health")?
print("{a.status} {a.text()?}")
let b = Client.get("{base}/todos/1")?
print("{b.status} {b.header("content-type") ?? "-"} {b.text()?}")
print("{(Client.get("{base}/todos/2")?).status} {(Client.get("{base}/todos/x")?).status} {(Client.get("{base}/nope")?).status}")
stop.shutdown()
()
}?
}
fn main() io, net, time {
match run() {
NetError => print("net error")
DecodeError => print("decode error")
Panicked => print("panicked")
_ => {}
}
}Output
200 ok
200 application/json {"id":"1","title":"write docs","done":true}
404 400 404Server.bind(ServerConfig{ port: 0, followNode: false })listens on a free port;srv.port()tells which one.defer srv.close()releases it on any exit (the typeServeris marked@must_close; forgetting to close is a compile error,must-close).Router.new().get("/todos/:id", todo)- routes in a chain;req.param("id")returns the path parameter asstr?.Reply.json(toJson(&value)!),Reply.text(body, 404)are ready-made reply constructors;Reply.html,Reply.redirect,Reply.emptylikewise.spawn stop.serve(router)inside ascopestarts accepting requests in a sibling task;stop.shutdown()stops the server gracefully, and thescopeexits.- An unknown path gives 404 automatically;
Router.fallback(handler)lets you set your own answer.
Middleware#
Task. Check a token and add a header to all replies without repeating code in each handler.
use std.http { Server, ServerConfig, Router, Request, Reply, Client, Header, Next, requireHeader }
fn stamp(req: Request, next: Next) -> Reply net, io, time { // middleware: wrap the next handler
let reply = next(req)
reply.withHeader("x-served-by", "oxl")
}
fn run() -> () | NetError | Panicked net, io, time {
let srv = Server.bind(ServerConfig{ port: 0, followNode: false })?
defer srv.close()
let stop = srv.handle()
let base = "http://127.0.0.1:{srv.port()}"
let router = Router.new()
.get("/secret", (_: Request) => Reply.text("42"))
.layer(stamp)
.layer(requireHeader("x-token", "letmein")) // without the header: 401, the request goes no further
scope {
spawn stop.serve(router)
let denied = Client.get("{base}/secret")?
print("no token: {denied.status}")
let ok = Client.get("{base}/secret", [Header{ name: "x-token", value: "letmein" }])?
print("with token: {ok.status} {ok.header("x-served-by") ?? "-"}")
stop.shutdown()
()
}?
}
fn main() io, net, time {
match run() {
NetError => print("net error")
Panicked => print("panicked")
_ => {}
}
}Output
no token: 401 with token: 200 oxl
- Middleware is a function
(Request, Next) -> Reply: callnext(req)and possibly change the reply (withHeader), or return a reply at once without callingnext. .layer(...)wraps everything declared before it; hencerequireHeader, declared last, wraps the rest: a request withoutx-tokengets 401 and never reaches the route.- Ready-made middleware:
requireHeader,logRequests(),timing();withMiddleware(handler, mw)wraps a single handler.
JSON in a request#
Task. Accept JSON, answer JSON, return 400 on bad input.
use std.http { Server, ServerConfig, Router, Request, Reply, Client, Header }
use std.ser { fromJson, toJson }
data Sum { a: i64; b: i64 }
data Answer { sum: i64 }
fn add(req: Request) -> Reply {
match req.text() {
str body => match fromJson<Sum>(&body) {
Sum s => Reply.json(toJson(&Answer{ sum: s.a + s.b })!)
DecodeError => Reply.text("invalid json", 400)
}
DecodeError => Reply.text("invalid body", 400)
}
}
fn run() -> () | NetError | DecodeError | Panicked net, io, time {
let srv = Server.bind(ServerConfig{ port: 0, followNode: false })?
defer srv.close()
let stop = srv.handle()
let url = "http://127.0.0.1:{srv.port()}/add"
scope {
spawn stop.serve(Router.new().post("/add", add))
let hdr = [Header{ name: "content-type", value: "application/json" }]
let r = Client.postText(url, "{{\"a\":20,\"b\":22}}", hdr.clone())?
print("{r.status} {r.text()?}")
let bad = Client.postText(url, "oops", hdr)?
print("{bad.status} {bad.text()?}")
stop.shutdown()
()
}?
}
fn main() io, net, time {
match run() {
NetError => print("net error")
DecodeError => print("decode error")
Panicked => print("panicked")
_ => {}
}
}Output
200 {"sum":"42"}
400 invalid jsonreq.text()returns the body asstr | DecodeError,fromJson<Sum>(&body)parses it - two levels of errors become one 400 reply.Client.postText(url, text, headers)is the client call; the result isResponse | NetError.r.status,r.text(),r.header(name)read the reply.- Headers are a
List<Header>; the list is passed by value, so to reuse it you clone it (hdr.clone()). - By default the client uses a connection pool and HTTP/2 where the server supports it;
HttpConfig{ timeout: 2s }sets timeouts.
Back: cookbook.