HTTP
В примерах сервер и клиент живут в одной программе: сервер занимает свободный порт (port: 0), клиент обращается к 127.0.0.1, затем сервер останавливается.
Так же устроены тесты HTTP-кода: не нужны ни внешняя сеть, ни фиксированные порты.
Сервер с маршрутами#
Задача. Отдать JSON по пути с параметром и корректно ответить на ошибки.
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 404Server.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#
Задача. Проверять токен и добавлять заголовок ко всем ответам, не повторяя код в каждом обработчике.
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.
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 jsonreq.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 }задаёт таймауты.
Вернуться: кулинарная книга.