Skip to content

Handlers And Pipelines

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

Pudu

Handlers And Pipelines

A handler is a function of the request, the cancellation token, and the rest of the pipeline. It may change the request, answer without calling the rest, or change the response on its way back. Handler.before and Handler.after build the common cases; Handler.compose and Handler.around compose them yourself. In a factory's pipeline the order is: observers' outer handlers, the added handlers in order, the observers' inner handlers in reverse, then the primary handler.

module Ex05Handlers

import Std.Concurrent.Cancel as Cancel
import Std.Http as Http
import Std.Io as Io
import Std.Result as Result
import PuduLangHttpClient as HttpClient
import PuduLangHttpClient.Client as Client
import PuduLangHttpClient.Factory as Factory
import PuduLangHttpClient.Factory.Builder as Builder
import PuduLangHttpClient.Handler as Handler
import PuduLangHttpClient.Request as Request
import PuduLangHttpClient.Response as Response
import PuduLangHttpClient.Stub as Stub

fn tagging(name: Str) -> Handler.Handler { Handler.before(|request: Request.Request| request.withHeader("x-trace", name)) }

fn refusingDeletes(request: Request.Request, token: Cancel.Token, next: Handler.Send) -> HttpClient.Outcome[Response.Response] {
  if request.method == Http.Delete { return HttpClient.invalid("deletes are disabled") }
  next(request, token)
}

fn trail() -> Builder.Observer {
  Builder.Observer {
    outer: |seen: Builder.Observation| tagging("outer:" + seen.name),
    inner: |_seen: Builder.Observation| tagging("inner")
  }
}

export fn main() -> Int {
  let stub = Stub.create()
  Stub.when(&stub, |_request: Request.Request| true, |request: Request.Request| Ok(Response.answer(&request, 200).withText(request.headers.map(|pair: (Str, Str)| pair[1]).join(" > "))))
  let factory = match Factory.build([
      Builder.named("api").withBaseAddress("https://api.test/").withPrimary(|| Stub.send(&stub))
        .observedBy(trail())
        .withHandler(tagging("first"))
        .withHandlerFactory(|| tagging("made per pipeline"))
        .withHandler(refusingDeletes)
        .withHandler(Handler.after(|response: Response.Response| response.withHeader("x-seen", "yes")))
    ]) {
    case Ok(built) => built
    case Err(invalid) => panic(Factory.explain(&invalid))
  }
  let client = Factory.createClient(&factory, "api")
  let order = Client.getText(&client, "items")
  let refused = Client.delete(&client, "items/1")
  let seen = Result.map(Client.get(&client, "items"), |r: Response.Response| Response.header(&r, "x-seen"))
  let _a = Io.writeLine(show(order))
  let _b = Io.writeLine(show(Result.map(refused, |r: Response.Response| r.status.code)) + " " + show(seen))
  if order == Ok("outer:api > first > made per pipeline > inner") && seen == Ok(Some("yes")) { 0 } else { 1 }
}

Check it and run it:

pudu check src/Ex05Handlers.pudu
pudu run src/Ex05Handlers.pudu

Output:

Ok("outer:api > first > made per pipeline > inner")
Err(InvalidRequest("deletes are disabled")) Ok(Some("yes"))

withHandler adds a handler shared by every pipeline the factory builds for the name; withHandlerFactory makes one afresh for each pipeline, for handlers holding state of their own. configureHandlers receives every added handler, defaults' first, and answers the final list.

An observer is two handler factories given the client's name and a redaction predicate: its outer handler sees what the caller sent and got, its inner handler what went on the wire after retries and rewrites.

Related

Clone this wiki locally