Skip to content

Requests And Responses

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

Pudu

Requests And Responses

A Request is a method, an address, a version with a policy, headers, an optional body, typed options for handlers, and an optional receiver for a streamed response. Requests are built one decision at a time with the Request.Shaping methods (withHeader, setHeader, removeHeader, withContent, withVersion, withOption, withReceiver, withBearer, accepting).

Bodies come from Content: plain, text, bytes, octets, json, jsonOf for any value that derives Json.Encode, form, multipart and multipartWith with field and file parts, and streamed for a producer sent in chunks. Responses carry the status, version, headers, body, trailers, and the final request after redirects; Response.text, bytes, json, and readAs read the body, and ensureSuccess turns a status outside 2xx into a failure. This program sends through a stub primary handler so it runs without a network:

module Ex02Messages

import Std.Concurrent.Cancel as Cancel
import Std.Http as Http
import Std.Io as Io
import Std.Json as Json
import Std.Result as Result
import PuduLangHttpClient as HttpClient
import PuduLangHttpClient.Content as Content
import PuduLangHttpClient.Domain.Version as Version
import PuduLangHttpClient.Options as Options
import PuduLangHttpClient.Request as Request
import PuduLangHttpClient.Response as Response
import PuduLangHttpClient.Stub as Stub

type Order = { sku: Str, quantity: Int } derives Json.Encode, Json.Decode

export fn main() -> Int {
  let tenant = Options.textKey("tenant")
  let request = Request.post("https://shop.test/orders", Content.jsonOf(&Order{sku: "tea", quantity: 2}))
    .withHeader("x-request-id", "r-1")
    .withBearer("t0k3n")
    .accepting("application/json")
    .withVersion(Http.Http11, Version.OrLower)
    .withOption(&tenant, "acme")
  let stub = Stub.create()
  Stub.on(&stub, Http.Post, "/orders", fn(received: Request.Request) -> HttpClient.Outcome[Response.Response] {
      let body = match received.content {
        case Some(found) => found
        case None => Content.empty()
      }
      Ok(Response.answer(&received, 201).withHeader("location", "/orders/9").withContent(body))
    })
  let sent = Stub.send(&stub)(request, Cancel.token())
  let created = Result.andThen(sent, |response: Response.Response| Response.readAs[Order](&response))
  let located = Result.map(sent, |response: Response.Response| Response.header(&response, "Location"))
  let ensured = Result.andThen(Stub.send(&stub)(Request.get("https://shop.test/missing"), Cancel.token()), Response.ensureSuccess)
  let _a = Io.writeLine(show(created) + " at " + show(located))
  let _b = Io.writeLine(show(Request.option(&request, &tenant)) + " " + show(Request.header(&request, "content-type")))
  let _c = Io.writeLine(show(Result.map(ensured, |r: Response.Response| r.status.code)))
  if created == Ok(Order{sku: "tea", quantity: 2}) && ensured == Err(HttpClient.Unsuccessful(404, "Not Found")) { 0 } else { 1 }
}

Check it and run it:

pudu check src/Ex02Messages.pudu
pudu run src/Ex02Messages.pudu

Output:

Ok(Order{sku: "tea", quantity: 2}) at Ok(Some("/orders/9"))
Some("acme") Some("application/json; charset=utf-8")
Err(Unsuccessful(404, "Not Found"))

Options.textKey, intKey, boolKey, and key make typed keys whose values ride on the request for handlers to read with Request.option. Request.header and Response.header look among the message's own headers and then its body's. The Failure variants are InvalidRequest, NameResolution, Connection, SecureConnection, ProxyTunnel, Protocol, ResponseEnded, ContentTooLarge, HeadersTooLarge, TooManyRedirects, NotPermitted, VersionUnsupported, TimedOut, Cancelled, Unsuccessful, Unreadable, Rejected, and Crashed; isTransient, isTimeout, isCancellation, and statusOf classify them.

Related

Clone this wiki locally