OxL Reference

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.

oxl✓ checked in CI
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 404
  • Server.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 type Server is 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 as str?.
  • Reply.json(toJson(&value)!), Reply.text(body, 404) are ready-made reply constructors; Reply.html, Reply.redirect, Reply.empty likewise.
  • spawn stop.serve(router) inside a scope starts accepting requests in a sibling task; stop.shutdown() stops the server gracefully, and the scope exits.
  • 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.

oxl✓ checked in CI
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: call next(req) and possibly change the reply (withHeader), or return a reply at once without calling next.
  • .layer(...) wraps everything declared before it; hence requireHeader, declared last, wraps the rest: a request without x-token gets 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.

oxl✓ checked in CI
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 json
  • req.text() returns the body as str | 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 is Response | 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.

See also

OxL 1.0.0-beta · Documentation version 1.0.0-beta© OxwaldLink · generated from the Standard, the std sources and the guides