OxL Справочник OxL
РусскийEnglish

HTTP

В примерах сервер и клиент живут в одной программе: сервер занимает свободный порт (port: 0), клиент обращается к 127.0.0.1, затем сервер останавливается. Так же устроены тесты HTTP-кода: не нужны ни внешняя сеть, ни фиксированные порты.

Сервер с маршрутами#

Задача. Отдать JSON по пути с параметром и корректно ответить на ошибки.

oxl✓ проверяется в 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 из пути — это параметр маршрута
  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 })?  // порт 0 — система выберет свободный
  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)  // сервер работает в соседней задаче
    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")
    _ => {}
  }
}
Вывод
200 ok
200 application/json {"id":"1","title":"write docs","done":true}
404 400 404
  • Server.bind(ServerConfig{ port: 0, followNode: false }) слушает свободный порт; srv.port() сообщает, какой именно. defer srv.close() освобождает его при любом выходе (тип Server помечен @must_close, забыть закрыть — ошибка компиляции, must-close).
  • Router.new().get("/todos/:id", todo) — маршруты цепочкой; req.param("id") возвращает параметр пути как str?.
  • Reply.json(toJson(&value)!), Reply.text(body, 404) — готовые конструкторы ответа; Reply.html, Reply.redirect, Reply.empty — аналогично.
  • spawn stop.serve(router) внутри scope запускает приём запросов в соседней задаче; stop.shutdown() мягко завершает сервер, и scope выходит.
  • Неизвестный путь даёт 404 автоматически; Router.fallback(handler) позволяет задать свой ответ.

Middleware#

Задача. Проверять токен и добавлять заголовок ко всем ответам, не повторяя код в каждом обработчике.

oxl✓ проверяется в CI
use std.http { Server, ServerConfig, Router, Request, Reply, Client, Header, Next, requireHeader }

fn stamp(req: Request, next: Next) -> Reply  net, io, time {  // middleware: обернуть следующий обработчик
  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"))  // без заголовка — 401, дальше запрос не идёт
  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")
    _ => {}
  }
}
Вывод
no token: 401
with token: 200 oxl
  • Middleware — функция (Request, Next) -> Reply: вызвать next(req) и, возможно, изменить ответ (withHeader) либо вернуть ответ сразу, не вызывая next.
  • .layer(...) оборачивает всё, что объявлено до него; поэтому requireHeader, объявленный последним, оборачивает остальное: запрос без x-token получает 401 и до маршрута не доходит.
  • Готовые middleware: requireHeader, logRequests(), timing(); withMiddleware(handler, mw) оборачивает один обработчик.

JSON в запросе#

Задача. Принять JSON, ответить JSON, на плохой ввод вернуть 400.

oxl✓ проверяется в 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")
    _ => {}
  }
}
Вывод
200 {"sum":"42"}
400 invalid json
  • req.text() возвращает тело как str | DecodeError, fromJson<Sum>(&body) разбирает его — два уровня ошибок превращаются в один ответ 400.
  • Client.postText(url, text, headers) — клиентский вызов; результат — Response | NetError. r.status, r.text(), r.header(name) читают ответ.
  • Заголовки — List<Header>; список передаётся по значению, поэтому для повторного использования его клонируют (hdr.clone()).
  • Клиент по умолчанию использует пул соединений и HTTP/2, где сервер его поддерживает; HttpConfig{ timeout: 2s } задаёт таймауты.

Вернуться: кулинарная книга.

См. также

OxL 1.0.0-beta · Версия документации 1.0.0-beta© OxwaldLink · сгенерировано из Стандарта, исходников std и руководства