Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Send HTTP requests

std.http.client sends HTTP/1.1 requests and keeps finished connections open for the next request to the same server. Every call takes a deadline and a cancellation token, so a slow server cannot hold the caller longer than it allows.

This program runs a small server on a loopback port and sends it three requests from another thread:

import std.bytes
import std.http
import std.http.client
import std.http.server
import std.net
import std.time
from std.http.server import Exchange
from std.net import TcpStream
from std.sync import CancellationToken

pub enum Route:
    Home
    Old
    Echo

pub class Site:
    pub def handle(self, route: &Route, exchange: &mut Exchange[TcpStream]) -> Result[(), AllocError | std.http.HttpError | std.bytes.BytesError]:
        match route:
            case Route.Home:
                exchange.send_text(200, "home\n")?
            case Route.Old:
                mut head = std.http.response(301, "Moved Permanently")?
                head.add("Location", "/")?
                exchange.respond(head, &std.bytes.empty()?)?
            case Route.Echo:
                mut body = std.bytes.empty()?
                exchange.read_all(&mut body)?
                exchange.send(200, "text/plain", &body)?
        Ok(())

type VisitError = AllocError | IoError | std.bytes.BytesError | std.http.client.ClientError | RangeError

def visit(port: u16, stop: CancellationToken) -> Result[(), VisitError]:
    with stop.cancel_on_exit():
        never = CancellationToken()?
        deadline = std.time.deadline_after(&std.time.monotonic_now(), &std.time.seconds(5u32))?
        base = f"http://127.0.0.1:{port}"?
        client = std.http.client.client(std.http.client.config()?)?

        home = client.get(&f"{base}/"?, &deadline, &never)?
        print(home.status, home.body.to_text()?.strip()?)?

        moved = client.get(&f"{base}/old"?, &deadline, &never)?
        print(moved.status, moved.redirects, moved.url.endswith("/"))?

        mut post = std.http.client.request("POST", &f"{base}/echo"?)?
        post.add("Content-Type", "text/plain")?
        post.body = std.bytes.from_text("ping")?
        echoed = client.fetch(post, &deadline, &never)?
        print(echoed.body.to_text()?)?

        stats = client.stats()
        print(stats.opened, stats.reused)?
    Ok(())

def main() -> Result[(), SpawnError[()] | std.http.server.ServerError | std.net.NetError | AllocError | VisitError]:
    mut routes = std.http.server.router[Route]()?
    routes.add("GET", "/", Route.Home)?
    routes.add("GET", "/old", Route.Old)?
    routes.add("POST", "/echo", Route.Echo)?
    listener = std.net.listen(&std.net.loopback_v4(0u16), 16)?
    port = listener.local_address()?.port
    stop = CancellationToken()?
    with spawn(visit, port, stop.share()).drop_job()? as task:
        drop(std.http.server.serve(listener, &routes, &Site(), &std.http.server.config(), &stop)?)
        task.join()?
    Ok(())
200 home
200 1 True
ping
1 3

client(config()) makes a client for plain http URLs with the default limits and timeouts. get and fetch send a request, read the whole response body into body, and put the connection back in the client’s pool. All four requests here, counting the redirect, used one connection: opened is 1 and reused is 3.

The client follows the 301 itself. redirects counts the hops and url is the address that finally answered. When a redirect leaves the original server, the client drops Authorization and Cookie so that credentials meant for one server are not sent to another.

A request holds its method, URL, extra header fields and body. The client writes Host and Content-Length itself and refuses requests that try to set them.

deadline covers the whole exchange: connecting, sending, waiting for the answer and reading the body. The config adds a limit for each phase, such as head_timeout for the wait between sending a request and its response starting. If either runs out, the call returns Net(TimedOut).

A request that fails because a reused connection was closed by the server is sent again on a new connection, but only for methods that are safe to repeat, such as GET and PUT. A POST is never sent twice; its error is returned so the program can decide what to do.

For a body that should be processed incrementally, send returns a response after its head arrives. Use read_body to read chunks, or read_all_progress to append into your own buffer while retaining the count appended before an error. Both preserve the full ClientError, including cancellation and TLS failures. response.total() counts successfully read payload bytes; response.trailers() is complete after response.is_done(). Give a completed response to client.finish to allow reuse. The typed stream lesson explains how to compose it with transfers through typed_response without a stored-error side channel.