HTTP: Serving
● Intermediate4 min
A server is one function from Request to Response. Route by r.method and r.path; answer with http.text or http.json_response.
use std.http
use std.net
fn shop(r: Request) -> Response {
if r.method == "GET" and r.path == "/" {
return http.text(200, "Welcome to the shop")
}
if r.method == "GET" and r.path.starts_with("/items/") {
const id = r.path.slice(7, r.path.len())
return http.text(200, "item {id}")
}
if r.method == "POST" and r.path == "/orders" {
return http.text(201, "order placed: {r.body.text() ?? ""}")
}
http.text(404, "nothing at {r.path}")
}
fn main() ! Error {
const listener = try net.listen("127.0.0.1:0")
const _server = spawn http.serve(listener, shop)
const base = "http://{listener.address()}"
for path in ["/", "/items/42", "/basket"] {
const page = try http.get("{base}{path}")
print("GET {path}: {page.status} {page.body.text() ?? ""}")
}
const made = try http.post("{base}/orders", "2 pens".bytes())
print("POST /orders: {made.status} {made.body.text() ?? ""}")
}The program ran and printed:
kavach run routes.kav
GET /: 200 Welcome to the shopGET /items/42: 200 item 42GET /basket: 404 nothing at /basketPOST /orders: 201 order placed: 2 pens
- Each connection runs in a task of its own, so one slow client doesn't hold up the others.
- The status says what happened:
200found,201made,404not found,400a bad request.
Who sent it
r.peer is the address of whoever opened the connection, for limits per client. A request made by hand has "":
use std.http
use std.net
fn who(r: Request) -> Response {
const local = r.peer.starts_with("127.0.0.1:")
http.text(200, "a request from this machine: {local}")
}
fn main() ! Error {
const listener = try net.listen("127.0.0.1:0")
const _server = spawn http.serve(listener, who)
const r = try http.get("http://{listener.address()}/")
print(r.body.text() ?? "")
const made = Request(method: "GET", path: "/", headers: [], body: Bytes([]))
print("by hand: \"{made.peer}\"")
}The program ran and printed:
kavach run peer.kav
a request from this machine: trueby hand: ""
Behind a proxy, peer is the proxy's address. Trust an address the proxy forwards in a header only when the request came from a proxy you list.
With mutual TLS, r.client_cert holds the names on the client's certificate, for allowing callers by name, and none otherwise. See TLS.
Headers
Headers are a list of Header(name:, value:), in the order they are sent, names in lower case. A name may come more than once: two set-cookie lines stay two headers.
use std.http
use std.net
fn login(r: Request) -> Response {
const client = r.header("User-Agent") ?? "unknown"
Response(
status: 200,
headers: [
Header(name: "set-cookie", value: "session=abc; HttpOnly"),
Header(name: "set-cookie", value: "theme=dark"),
],
body: "hello, {client}".bytes(),
)
}
fn main() ! Error {
const listener = try net.listen("127.0.0.1:0")
const _server = spawn http.serve(listener, login)
const r = try http.get("http://{listener.address()}/")
print(r.body.text() ?? "")
for cookie in r.headers_named("set-cookie") {
print(cookie)
}
print(r.header("x-missing") ?? "no x-missing")
}The program ran and printed:
kavach run headers.kav
hello, unknownsession=abc; HttpOnlytheme=darkno x-missing
r.header(name)gives the first value of a name, ornone. The name can be in any case.r.headers_named(name)gives every value of a name, in order.
Traces
http.serve runs each handler in a span named by the request's method (GET), never by its path, which can hold ids and tokens. When the request has a W3C traceparent header, the span continues that trace, and http.request and the other calls send the current span's. So traces join across services with no code:
use std.http
use std.log
use std.net
fn handler(r: Request) -> Response {
// The handler runs in a span that continues the caller's trace.
// The span is named `GET`; the handler knows its route, so it says it.
log.name_span("GET /orders/\{id}")
http.text(200, log.trace_id() ?? "none")
}
fn main() ! Error uses Net, Console {
const listener = try net.listen("127.0.0.1:0")
const _server = spawn http.serve(listener, handler)
const url = "http://{listener.address()}/orders/7"
log.span("checkout") {
const mine = log.trace_id() ?? ""
const got = try http.get(url)
print("one trace across both: {(got.body.text() ?? "") == mine}")
}
}The program ran and printed:
kavach run traces.kav
one trace across both: trueA handler that knows its route names the span with it, with log.name_span. The route's pattern is one of a few names; the raw path would be a new one for every order. \{ keeps the brace as text, since { starts interpolation.
A server the internet reaches directly sets follow_trace: false in its ServeLimits. Each request then starts a trace of its own, so a client can't choose trace ids or join its requests to someone else's trace:
use std.http
use std.log
use std.net
fn whose(r: Request) -> Response {
http.text(200, log.trace_id() ?? "none")
}
fn main() ! Error uses Net, Console {
const listener = try net.listen("127.0.0.1:0")
const limits = ServeLimits(follow_trace: false)
const _server = spawn http.serve(listener, whose, limits: limits)
const url = "http://{listener.address()}/"
log.span("caller") {
const mine = log.trace_id() ?? ""
const got = try http.get(url)
const theirs = got.body.text() ?? ""
print("a trace of its own: {theirs.len() == 32 and theirs != mine}")
}
}The program ran and printed:
kavach run own-trace.kav
a trace of its own: trueSee Logging for spans, and for sending them to a collector.
Limits
A server faces clients that are slow, large or hostile, so http.serve holds each one to ServeLimits. Every field has a safe default, so http.serve(listener, handler) is already protected. Change only the ones you need:
use std.http
use std.net
fn upload(r: Request) -> Response {
http.text(200, "got {r.body.len()} bytes")
}
fn main() ! Error {
const listener = try net.listen("127.0.0.1:0")
const _server = spawn http.serve(listener, upload, limits: ServeLimits(max_body_bytes: 10))
const url = "http://{listener.address()}/"
const small = try http.post(url, "hello".bytes())
print("{small.status}: {small.body.text() ?? ""}")
const big = try http.post(url, "x".repeat(50).bytes())
print(big.status)
}The program ran and printed:
kavach run limits.kav
200: got 5 bytes413
pub type ServeLimits {
pub max_header_bytes: Int = 16_384 // 431 past it
pub max_body_bytes: Int = 10_000_000 // 413 past it, refused by `content-length` before the body is read
pub buffered_body: Int = 1_000_000 // larger bodies are read by the handler, `r.read_body()`
pub header_timeout: Duration = 10.seconds // 408 when a request's line and headers take longer
pub min_body_rate: Int = 1_000 // bytes a second, after a second's grace: 408 when slower
pub idle_timeout: Duration = 5.seconds // a kept-open connection waiting for its next request
pub max_connections: Int = 10_000 // 503 past it
pub max_in_flight: Int = 1_000 // requests being answered at once: 503 past it
pub retry_after: Int = 1 // what `retry-after` says on a 503, in seconds
pub drain_timeout: Duration = 10.seconds // on a stop signal, how long to finish the requests under way
pub follow_trace: Bool = true // continue a request's `traceparent`; `false`: a trace of its own
}
const server = spawn http.serve(listener, upload, limits: ServeLimits(max_body_bytes: 100_000_000))- A request that breaks a limit gets its status and the connection is closed. The handler never sees it.
- A client that sends its headers a byte at a time holds a task for at most
header_timeout. - A request with
expect: 100-continuegets100 Continueonce its size is known to fit, before it sends the body. - A 503 says
retry-after, so well-behaved clients wait before trying again.
Stopping
On Ctrl+C or SIGTERM (how Docker and Kubernetes stop a container), http.serve answers new connections with 503, closes those waiting for their next request, and finishes the requests under way, for at most drain_timeout. Then the program ends, without cutting a response off. A second signal ends it at once. See When a program stops.
A client that leaves
While a request is answered, http.serve keeps reading its connection. When the client closes it, the handler's task is cancelled, so a request nobody waits for stops using the server. A handler that panics gets 500, and the server goes on.
HTTP/2 without TLS
http.serve answers HTTP/2 too, from a client that starts with the HTTP/2 preface without TLS, as services calling each other do. One connection carries many requests at once, and each runs its handler in a task of its own, so a slow request holds up no other:
use std.http
use std.http.h2
use std.net
use std.time
/// Asks for `path`, and prints the answer as soon as it comes.
fn show(c: H2Client, path: String) ! H2Error uses Net, Console {
const reply = try c.request([
Header(name: ":method", value: "GET"),
Header(name: ":scheme", value: "http"),
Header(name: ":authority", value: "localhost"),
Header(name: ":path", value: path),
], Bytes([]))
print("got {path}: {reply.body.text() ?? "?"}")
}
fn main() ! Error uses Clock {
const listener = try net.listen("127.0.0.1:0")
const _server = spawn http.serve(listener, r => {
if r.path == "/report" {
time.sleep(300.ms) // a slow request
}
print("answered {r.path}")
http.text(200, "done")
})
const c = try h2.connect(try net.connect(listener.address()))
const slow = spawn show(c, "/report")
const fast = spawn show(c, "/ping")
try slow.wait()
try fast.wait()
c.close()
}The program ran and printed:
kavach run h2-at-once.kav
answered /pinggot /ping: doneanswered /reportgot /report: done
/reportwas asked first, but/pingwas answered first: its handler didn't wait for the slow one.- On the client, each task gets its answer as soon as it comes, whatever order the requests were sent in.
- At most 100 requests are answered at once on a connection, and the server tells the client so.
- The handler sees the same
Requestand gives the sameResponseas over HTTP/1.1.
Large bodies
A body up to buffered_body (1 MB unless you say) is read whole into r.body. A larger one is left for the handler to read as it comes, a piece at a time, with r.read_body(), which gives none once it has all come. An upload of any size then needs no more memory than a piece:
use std.http
use std.net
fn upload(r: Request) -> Response uses Net {
let read = 0
while true {
const piece = r.read_body() catch _ => return http.text(400, "upload cut off")
read += (piece ?? break).len()
}
http.text(200, "in r.body: {r.body.len()}, read as it came: {read}")
}
fn main() ! Error {
const listener = try net.listen("127.0.0.1:0")
const _server = spawn http.serve(listener, upload, limits: ServeLimits(buffered_body: 100))
const url = "http://{listener.address()}/"
for size in [5, 3000] {
const r = try http.post(url, "x".repeat(size).bytes())
print("{size} bytes sent: {r.body.text() ?? ""}")
}
}The program ran and printed:
kavach run upload.kav
5 bytes sent: in r.body: 5, read as it came: 03000 bytes sent: in r.body: 0, read as it came: 3000
- The 5-byte body fits in
buffered_body: 100, so it is all inr.body, andread_bodyhas nothing more. - The 3000-byte body is not in
r.body: the handler reads it withread_body. - A read fails with a
NetErrorwhen the client goes away during the upload.
Sending a file
http.file(path) answers with a file as it is read, so a file of any size is sent without being held in memory. Its content-type comes from the file's extension, and its content-length from its size:
use std.fs
use std.http
use std.net
fn download(r: Request) -> Response uses Fs.read {
http.file("report.txt") catch _ => http.text(404, "no such file")
}
fn main() ! Error {
try fs.write_text("report.txt", "sales up\n".repeat(1000))
const listener = try net.listen("127.0.0.1:0")
const _server = spawn http.serve(listener, download)
const r = try http.get("http://{listener.address()}/")
print(r.header("content-type") ?? "?")
print(r.header("content-length") ?? "?")
print(r.body == (try fs.read_bytes("report.txt")))
try fs.remove("report.txt")
}The program ran and printed:
kavach run file.kav
text/plain; charset=utf-89000true
To send something else as it is read, give a Response a reader from fs.chunks as its stream: Response(status: 200, headers: [], body: Bytes([]), stream: reader).
Forms with files
An HTML form with files sends a multipart/form-data body. http.form(r) reads it a part at a time, and each part's data as it comes, so a large upload is never all in memory:
fn upload(r: Request) -> Response ! HttpError uses Net, Console {
const form = try http.form(r)
let received = 0
while true {
const part = (try form.next()) ?? break
if part.filename == none {
print("{part.name} = {try part.text()}")
continue
}
while true {
received += ((try part.read()) ?? break).len()
}
}
http.text(200, "received {received} bytes")
}| Written | Gives |
|---|---|
form.next() -> FormPart? ! HttpError | The next part, or none after the last; data of the part before not read yet is skipped |
part.name, part.filename, part.content_type | The field's name, the file's name (String?, none for a plain field) and its content-type (text/plain when not given) |
part.read() -> Bytes? ! HttpError | The part's next piece of data, or none at its end |
part.text() -> String ! HttpError | The rest of the part as text: for small fields |
A form that isn't multipart/form-data, has no boundary, or is cut off fails with BadMessage.