Skip to content

The Transport

Chris Michael edited this page Oct 6, 2026 · 1 revision

Pudu

The Transport

The transport is the primary handler: it keeps connections per origin and reuses them, follows redirects, keeps cookies, decompresses gzip, goes through proxies, and bounds what a response may cost. Its options are built one decision at a time from Transport.defaults().

module Ex06Transport

import Std.Compress.Gzip as Gzip
import Std.Concurrent as Concurrent
import Std.Http as Http
import Std.Http.Server as Server
import Std.Http.Server.Reply as Reply
import Std.Http.Server.Route as Route
import Std.Io as Io
import Std.Result as Result
import PuduLangHttpClient as HttpClient
import PuduLangHttpClient.Client as Client
import PuduLangHttpClient.Cookies as Cookies
import PuduLangHttpClient.Response as Response
import PuduLangHttpClient.Transport as Transport

fn serve() -> (Server.Running, Str) {
  let running = match Server.start("127.0.0.1", 0) {
    case Ok(found) => found
    case Err(problem) => panic(show(problem))
  }
  let server = Server.server(&Route.routing(&[
      Route.get("/old", |_request: Route.Request| Http.Response{status: Http.status(301), headers: [("location", "/new")], body: "", binaryBody: None}),
      Route.get("/new", |_request: Route.Request| Http.Response{..Reply.text(200, "moved here"), headers: [("set-cookie", "visited=yes; Path=/")]}),
      Route.get("/report", |_request: Route.Request| Http.Response{..Reply.bytes(200, "text/plain", &Gzip.compressText("a long report", &Gzip.config())), headers: [("content-type", "text/plain"), ("content-encoding", "gzip")]}),
      Route.get("/big", |_request: Route.Request| Reply.text(200, "0123456789"))
    ]))
  let _serving = Concurrent.start(fn() -> () { let _ran = Server.run(&server, &running, 0) })
  (running, "http://127.0.0.1:" + show(Server.portOf(&running)) + "/")
}

export fn main() -> Int {
  let (running, base) = serve()
  let jar = Cookies.jar()
  let options = Transport.defaults()
    .withMaxConnectionsPerServer(4)
    .withPooledConnectionLifetime(300000)
    .withPooledConnectionIdleTimeout(30000)
    .withConnectTimeout(2000)
    .withMaxRedirects(5)
    .withDecompression()
    .withCookies(jar)
    .withMaxResponseHeadersLength(16384)
  let transport = Transport.create(options)
  let client = Client.over(&transport).withBaseAddress(base)
  let moved = Result.map(Client.get(&client, "old"), |r: Response.Response| (r.status.code, r.request.uri))
  let report = Client.getText(&client, "report")
  let limited = Client.getText(&client.withMaxResponseContentBufferSize(4), "big")
  let publicOnly = Client.over(&Transport.create(Transport.defaults().withAddressPolicy(Transport.PublicOnly([])))).withBaseAddress(base)
  let refused = Client.getText(&publicOnly, "new")
  let stats = Transport.statistics(&transport)
  let _a = Io.writeLine(show(moved) + " " + show(report))
  let _b = Io.writeLine(show(limited) + " " + show(Cookies.header(&jar, base + "new")))
  let _c = Io.writeLine(show(stats) + " " + show(Result.isErr(&refused)))
  Transport.close(&transport)
  let _stopped = Server.stop(&running)
  if report == Ok("a long report") && limited == Err(HttpClient.ContentTooLarge(4)) && stats.opened == 1 && Cookies.header(&jar, base) == Some("visited=yes") { 0 } else { 1 }
}

Check it and run it:

pudu check src/Ex06Transport.pudu
pudu run src/Ex06Transport.pudu

Output:

Ok((200, "http://127.0.0.1:49581/new")) Ok("a long report")
Err(ContentTooLarge(4)) Some("visited=yes")
Statistics{opened: 1, reused: 3, closed: 1, active: 0, idle: 0} true
Option Default What it does
withPooledConnectionLifetime forever closes a pooled connection once it has lived this long
withPooledConnectionIdleTimeout one minute closes a pooled connection unused this long
withMaxConnectionsPerServer no limit requests wait for a free connection, bounded by their deadline
withConnectTimeout the request's deadline gives up on opening a connection
withMaxRedirects, withoutRedirects 50 follows 300–308 redirects; https is never followed to http and credentials never cross origins
withDecompression off asks for gzip and decompresses buffered bodies within the buffer limit
usingCookies, withCookies off stores and sends cookies in the transport's own jar or a shared one
withProxy none sends plain requests through the proxy and tunnels secured ones with CONNECT
withAddressPolicy any address PublicOnly(permitted) refuses private and loopback hosts, on every hop
withMaxResponseHeadersLength 64 KiB refuses a longer head
withMaxResponseContentLength 64 MiB refuses a longer buffered body when the request sets no limit

Transport.statistics counts connections opened, reused, closed, in use, and idle, and Transport.close closes idle connections and stops keeping new ones. A pooled connection that the server closed while idle is replaced and the request sent once more. Cookies.save and Cookies.restore keep a jar's persistent cookies across runs; Proxy.at(address) with withCredentials, bypassing, and bypassingLocal describes a proxy. The transport speaks HTTP/1.0 and 1.1.

Related

Clone this wiki locally