Pudu programming language
Menu
Package

@chrismichaelps / pudu-lang-httpclient

Named HTTP clients for Pudu: a client factory, delegating handlers, pooled keep-alive connections, handler lifetimes, logging, and resilience

0.1.1Apache-2.01

InstallClose

README.md

Markdown175 lines9.8 KB

GitHub ↗

pudu-lang-httpclient

Pudu

Pudu | Documentation | Packages | API docs | Contributing

Named HTTP clients for Pudu. A factory holds the configuration of every client a program uses — base address, default headers, timeout, delegating handlers, and the transport underneath — and hands out cheap clients over handler pipelines it owns, pools, and renews. Every request answers an Outcome: the response, or a Failure saying why there is none. Nothing is thrown.

import PuduLangHttpClient.Client as Client
import PuduLangHttpClient.Factory as Factory
import PuduLangHttpClient.Factory.Builder as Builder

fn main() -> Int {
  let factory = match Factory.build([
      Builder.defaults().withDefaultHeader("user-agent", "shop/1.0"),
      Builder.named("github").withBaseAddress("https://api.github.com/").withDefaultHeader("accept", "application/vnd.github+json").withTimeout(10000)
    ]) {
    case Ok(built) => built
    case Err(invalid) => panic(Factory.explain(&invalid))
  }
  let client = Factory.createClient(&factory, "github")
  match Client.getText(&client, "repos/chrismichaelps/pudu-lang") {
    case Ok(_) => 0
    case Err(_) => 1
  }
}

Installing

pudu install @chrismichaelps/pudu-lang-httpclient

It needs Pudu 0.1.3 or later. Every module the package ships is PuduLangHttpClient or under it, so it takes no module name from the program that installs it.

Concepts

ConceptModuleWhat it is
Outcome and failurePuduLangHttpClientOutcome[T] is Result[T, Failure]. A failure names its cause: an invalid request, name resolution, connection, secured connection, proxy tunnel, protocol, a response cut short, a head or body over its limit, too many redirects, a refused destination, an unsupported version, a timeout, a cancellation, an unsuccessful status, an unreadable body, a resilience rejection, or a crash.
Requests and responsesRequest, Response, Content, OptionsVerbs, headers, versions with a policy, typed options for handlers, bodies of text, bytes, forms, multipart parts, JSON documents, any value that derives Json.Encode, or a producer streamed in chunks; ensureSuccess, readers, trailers, and retryAfter.
ClientClient, Client.Jsoncreate, over a transport, or pooled; base address, default headers, timeout, response buffer limit, default version, cancelPending; send, get, getText, getBytes, stream, post, put, patch, delete, head, and getFromJson, postAsJson, putAsJson, patchAsJson, deleteFromJson.
EndpointsEndpointDeclarative endpoints: a method and a template such as repos/:owner/:name, filled from arguments and read as a derived value.
FactoryFactory, Factory.BuilderNamed clients, defaults for every client, typed clients, the handler factory (createHandler), rotatingHandler, handler lifetimes, validation that reports every problem at once, and dispose.
HandlersHandlerSend and Handler, composed outermost first; before, after, and your own functions.
TransportTransport, Cookies, ProxyThe primary handler: pooled keep-alive connections, lifetimes, idle timeouts, a per-server limit, a connect timeout, redirects, gzip, cookie jars that save and restore, proxies with tunnels, an address policy, and head and body limits.
EventsEventsPlain listeners for requests started, completed, and failed, and pipelines created, expired, and closed; every event derives Json.Encode.
TestingStubA primary handler answering from rules and recording every request.

One client, configured once, used everywhere

Build a client at startup with its defaults and hand the same value to every part of the program that needs it. A client is an immutable value; every copy sends through the same pooled transport, so the parts share connections:

let transport = Transport.create(Transport.defaults().withPooledConnectionLifetime(120000))
let http = Client.over(&transport).withBaseAddress("https://shop.test/").withDefaultHeader("user-agent", "shop/1.0").withTimeout(10000)
let catalog = Catalog{http: http}
let orders = Orders{http: http}

Client.pooled(options) does the first two steps in one call. Build the client once and pass it on: a function that builds a client each time it is called builds a new pool each time. Give a long-lived client a pooled connection lifetime so it still sees addresses change. When several differently configured clients are needed, give each a name in a factory, set what they share with Builder.defaults(), and ask for them by name wherever they are used.

Configuring a named client

A builder reads in the order decisions are made. Builders of the same name are combined, and Builder.defaults() applies to every client first.

Builder methodWhat it sets
withBaseAddress, withDefaultHeader, withTimeout, withMaxResponseContentBufferSize, withDefaultVersion, configureClientthe client
withHandler, withHandlerFactory, configureHandlersdelegating handlers, made afresh for each pipeline, and their final order
withTransport, configureTransport, withPrimarythe primary handler: a pooled transport and its options, or your own
withHandlerLifetimehow long a pipeline is reused before the next is built (two minutes; Defaults.INFINITE to keep it)
observedBy, withoutObservers, redactingHeaders, redactingWhenobservers placed outside and inside every handler, and which header values they never see

Pipelines and lifetimes

A request passes through the observers' outer handlers, the added handlers in order, the observers' inner handlers, and then the primary handler; the response comes back the same way in reverse.

The factory reuses a name's pipeline — and so its pooled connections — until the handler lifetime runs out, then builds the next one. Clients are cheap: make one where it is needed. A client made from an expired pipeline keeps working, and the expired pipeline's connections are closed after one more lifetime. Code that keeps a sender for a long time uses Factory.rotatingHandler, or a transport with withPooledConnectionLifetime and an infinite handler lifetime.

Handlers

ModulePackageWhat it adds
Handlers.Loggingpudu-lang-logan observer writing events where a request enters and leaves the pipeline (PuduLangHttpClient.<name>.LogicalHandler) and the primary handler (…ClientHandler), with redacted headers at the verbose level
Handlers.Resiliencepudu-lang-resiliencehandler, selecting per request, fromRegistry, onTransient, standard (rate limiter, total timeout, retry honouring retry-after, circuit breaker, attempt timeout), and standardHedging
Handlers.Propagation—headers of the operation in progress copied onto outgoing requests
Handlers.Authorization—basic credentials, or bearer tokens fetched again once when the server answers 401
Handlers.Metrics—counts by status class, failures, requests in flight, and durations

Events

let listeners = Events.listeners()
  .onRequestFailed(|event: Events.RequestFailed| audit(event.client, event.failure))
  .onPipelineChanged(|event: Events.PipelineChanged| track(event.client, event.generation))
let factory = Factory.buildWith(&Factory.Options{listeners: listeners}, builders)

Listeners are plain functions; the package carries events to them through a mediator of its own, which no program has to build or import.

Cancellation and timeouts

A client links the caller's token, its own pending token, and its timeout. Every socket operation waits at most what remains of the deadline, and the token is checked between phases and while waiting for a connection. A timeout of the client's own answers TimedOut(timeout); a cancelled token answers Cancelled(reason).

Defaults that fail safe

Credentials are dropped when a redirect changes origin, and https is never followed to http. Cookies are off unless a transport asks for them. Observers never see authorization, cookie, set-cookie, or proxy-authorization values. Response heads and bodies are bounded (64 KiB and 64 MiB) and refused rather than truncated. Programs that send requests to addresses users supply set withAddressPolicy(Transport.PublicOnly([])).

Limits

The transport speaks HTTP/1.0 and HTTP/1.1; an HTTP/2 request is sent as 1.1 under the default OrLower policy and refused under Exact. Client certificates are not offered, because Pudu 0.1.3 cannot present one.

Examples

examples/ holds runnable programs: QuickStart, SharedClient, TypedClients, Endpoints, Handlers, Resilience, Pooling, and Testing. Each starts its own local server.

pudu run examples/QuickStart.pudu

Developing

pudu install --locked                                 # the log, resilience, and mediator packages
pudu test test                                        # every suite
pudu fmt --check src test tools examples && pudu lint src test tools examples
pudu run tools/Mutate.pudu --domain --suites test/PuduLangHttpClient/Domain --threshold 100

The design lives in the wiki vault: one page per source file, the decisions behind the transport and the factory, and the Pudu grammar rules the code follows. The API docs walk through every module.

License

Apache License 2.0.