Skip to content

Clients

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

Pudu

Clients

A Client is a sender plus the defaults every request through it gets: a base address, default headers, a timeout (100 seconds unless set), a response buffer limit (64 MiB), and a default version and policy. Client.create wraps any sender, Client.over a transport, and Client.pooled builds a long-lived client over a pooled transport of its own. Relative addresses resolve against the base address the way links do, so a base ending in / keeps its last segment.

module Ex03Clients

import Std.Concurrent as Concurrent
import Std.Concurrent.Cancel as Cancel
import Std.Http as Http
import Std.Io as Io
import Std.Result as Result
import Std.Sync as Sync
import PuduLangHttpClient as HttpClient
import PuduLangHttpClient.Client as Client
import PuduLangHttpClient.Content as Content
import PuduLangHttpClient.Request as Request
import PuduLangHttpClient.Response as Response
import PuduLangHttpClient.Stub as Stub

fn slow(request: Request.Request, token: Cancel.Token) -> HttpClient.Outcome[Response.Response] {
  match Cancel.pause(&token, 5000) {
    case Ok(_) => Ok(Response.answer(&request, 200))
    case Err(Cancel.Requested(reason)) => Err(HttpClient.Cancelled(reason))
    case Err(Cancel.DeadlineExceeded) => Err(HttpClient.TimedOut(0))
  }
}

export fn main() -> Int {
  let stub = Stub.create()
  Stub.on(&stub, Http.Get, "/v2/items", |request: Request.Request| Ok(Response.answer(&request, 200).withText(request.uri + " as " + show(Request.header(&request, "user-agent")))))
  Stub.respond(&stub, Http.Post, "/v2/items", 201, "created")
  let client = Client.create(Stub.send(&stub)).withBaseAddress("https://api.test/v2/").withDefaultHeader("user-agent", "ex03").withTimeout(2000)
  let listed = Client.getText(&client, "items?page=2")
  let posted = Result.map(Client.post(&client, "items", Content.plain("tea")), |r: Response.Response| r.status.code)
  let pieces = Sync.cell(0)
  let streamed = Client.stream(&client, "items", fn(piece: Bytes) -> Bool {
      let _counted = Sync.set(&pieces, Result.unwrapOr(Sync.get(&pieces), 0) + piece.length())
      true
    })
  let timedOut = Client.get(&Client.create(slow).withTimeout(50), "https://api.test/")
  let pending = Client.create(slow)
  let result = Sync.cell(Ok(0))
  let worker = match Concurrent.start(fn() -> () { let _kept = Sync.set(&result, Result.map(Client.get(&pending, "https://api.test/"), |r: Response.Response| r.status.code)) }) {
    case Ok(started) => started
    case Err(problem) => panic(show(problem))
  }
  let _waited = Concurrent.sleep(30)
  Client.cancelPending(&pending)
  let _joined = Concurrent.join(&worker)
  let _a = Io.writeLine(show(listed))
  let _b = Io.writeLine(show(posted) + " streamed " + show(Result.unwrapOr(Sync.get(&pieces), 0)) + " bytes, ok: " + show(Result.isOk(&streamed)))
  let _c = Io.writeLine(show(Result.map(timedOut, |r: Response.Response| r.status.code)) + " " + show(Result.unwrapOr(Sync.get(&result), Ok(0))))
  if listed == Ok("https://api.test/v2/items?page=2 as Some(\"ex03\")") && posted == Ok(201) && Result.map(timedOut, |r: Response.Response| r.status.code) == Err(HttpClient.TimedOut(50)) { 0 } else { 1 }
}

Check it and run it:

pudu check src/Ex03Clients.pudu
pudu run src/Ex03Clients.pudu

Output:

Ok("https://api.test/v2/items?page=2 as Some(\"ex03\")")
Ok(201) streamed 41 bytes, ok: true
Err(TimedOut(50)) Err(Cancelled("the client cancelled its pending requests"))

Client.send and sendWith (with a cancellation token) send any request; get, head, post, put, patch, and delete answer the response whatever its status; getText and getBytes answer the body of a success; stream hands the body to a receiver as it arrives. The client's own timeout answers TimedOut(timeout), and cancelPending stops every request in flight while later ones run.

One client, configured once

Build a client at startup and pass the same value to every part of the program. Every copy sends through the same pooled transport, so the parts share connections — the SharedClient program hands one client to a catalog and an order service and serves both over one connection. Build it once: a function that builds a client each time builds a new pool each time. Give a long-lived client withPooledConnectionLifetime so it still sees addresses change.

Related

Clone this wiki locally