~/bend-docscommunity

hairpin.bend relies on unsafe/foreign

raw source on the hub · import bend-kit-hairpin@0.3.0.0/hairpin.bend as Hairpin

Hairpin: an HTTP client with a base URL, default headers, a pool, cookies, mTLS, redirects, and proven retries. Source: https://github.com/paymog/bend-kit/tree/main/hairpin

7 imports
import Base
import bend-kit-http@0.32.0.0/http.bend as Http
import bend-kit-time@0.1.2.1/time.bend as Time
import 0x1f2d80f53f971b16c6de6a65cb1918ae/url.bend as Url
import 0x584fc27920487ceab242392391418d7f/json.bend as Json
import 0x49814d83de8f70993a43e1002be29ecd/bytes.bend as Bytes
import ./retry.bend as Retry

Types

type Opts source · line 12 · raw

Data

base: "" for none. headers: lowercase names. ms: the step timeout. policy: attempts, backoff, and deadline of each request.

type Client source · line 16 · raw

Type

breaker: the circuit breaker of every request on this client.

type Err source · line 20 · raw

Data

ErrHttp: the last attempt failed. ErrDenied: no attempt started, for why.

type Step source · line 175 · raw

Type

The live state of one request between attempts. last: the result of the last attempt, None before the first.

Definitions

def policy source · line 25 · raw

0x5f997a9351e7133e43ba9611e734f8a6/retry.Policy

One attempt, the first included; backoff from 500 ms, doubled to at most 30 s; no deadline.

def new.with source · line 30 · raw

@+cap:U32 -> Client

A pool of up to cap idle sockets per origin, no base, no default headers, 30 s steps, follow redirects, no retries, and no circuit breaker.

def new source · line 33 · raw

Client

def base source · line 36 · raw

@c:Client -> @url:String -> Client

def timeout source · line 45 · raw

@c:Client -> @ms:U32 -> Client

def redirect source · line 49 · raw

@c:Client -> @mode:0x6a946b742ab6f76f0ba5b90b14da52ad/http.Mode -> Client

def retry source · line 54 · raw

@c:Client -> @n:U32 -> Client

Up to n retries after the first attempt: n + 1 attempts in all.

def deadline source · line 59 · raw

@c:Client -> @ms:U32 -> Client

No attempt of a request starts ms or more after the request starts.

def circuit source · line 64 · raw

@c:Client -> @circ:0x5f997a9351e7133e43ba9611e734f8a6/retry.Circuit -> Client

A circuit breaker in front of every request, starting closed.

def breaker.of source · line 69 · raw

@c:Client -> Pair(Client, 0x5f997a9351e7133e43ba9611e734f8a6/retry.Breaker)

The breaker, for inspection.

def cert source · line 74 · raw

@c:Client -> @chain:String -> @key:String -> Client

Paths to a PEM client chain and its private key, presented to every HTTPS origin.

def jar source · line 79 · raw

@c:Client -> @jar:0x6a946b742ab6f76f0ba5b90b14da52ad/http.Jar -> Client

Every request stores and sends cookies through jar. Its clock moves to Time.now() before each try.

def jar.of source · line 84 · raw

@c:Client -> Pair(Client, Maybe<&2, 0x6a946b742ab6f76f0ba5b90b14da52ad/http.Jar>)

The jar, if the client has one.

def close source · line 88 · raw

@c:Client -> IO(Unit)

def ms.of source · line 92 · raw

@r:Pair(U32, Pair(U32, U32)) -> Nat

def now.ms source · line 97 · raw

IO(Nat)

Milliseconds on the monotonic clock that Retry budgets and breakers use.

def target.pick source · line 102 · raw

@none:Bool -> @b:String -> @ref:String -> Maybe<&2, String>

def target source · line 110 · raw

@+b:String -> @ref:String -> Maybe<&2, String>

ref against the base (RFC 3986 §5.2); with no base, ref as given. None when the base is not absolute.

def target.abs source · line 113 · raw

@m:Maybe<&2, String> -> Maybe<&2, 0x1f2d80f53f971b16c6de6a65cb1918ae/url.Abs>

def merge source · line 122 · raw

@defaults:Map<&2, List<&2, String>> -> @headers:Map<&2, List<&2, String>> -> Map<&2, List<&2, String>>

The defaults under the request headers, names lowercased; a request header replaces a default of its name. Host, Connection, Content-Length, and Transfer-Encoding are the client's own, so both sides drop them.

def jar.now source · line 125 · raw

@j:Maybe<&2, 0x6a946b742ab6f76f0ba5b90b14da52ad/http.Jar> -> IO(Maybe<&2, 0x6a946b742ab6f76f0ba5b90b14da52ad/http.Jar>)

def once.final source · line 134 · raw

@+enc:String -> @x:Pair(0x6a946b742ab6f76f0ba5b90b14da52ad/http.Pool, Pair(Result<&1, &1, 0x6a946b742ab6f76f0ba5b90b14da52ad/http.Err, 0x6a946b742ab6f76f0ba5b90b14da52ad/http.Res>, Maybe<&2, 0x6a946b742ab6f76f0ba5b90b14da52ad/http.Jar>)) -> IO(Pair(0x6a946b742ab6f76f0ba5b90b14da52ad/http.Pool, Pair(Maybe<&2, 0x6a946b742ab6f76f0ba5b90b14da52ad/http.Jar>, Result<&1, &1, 0x6a946b742ab6f76f0ba5b90b14da52ad/http.Err, 0x6a946b742ab6f76f0ba5b90b14da52ad/http.Res>)))

def once source · line 142 · raw

@+ms:U32 -> @+mode:0x6a946b742ab6f76f0ba5b90b14da52ad/http.Mode -> @+cert:Maybe<&2, 0x6a946b742ab6f76f0ba5b90b14da52ad/http.Cert> -> @+enc:String -> @p:0x6a946b742ab6f76f0ba5b90b14da52ad/http.Pool -> @j:Maybe<&2, 0x6a946b742ab6f76f0ba5b90b14da52ad/http.Jar> -> @+method:String -> @+u:Maybe<&2, 0x1f2d80f53f971b16c6de6a65cb1918ae/url.Abs> -> @+headers:Map<&2, List<&2, String>> -> @body:0x49814d83de8f70993a43e1002be29ecd/bytes.Bytes -> IO(Pair(0x6a946b742ab6f76f0ba5b90b14da52ad/http.Pool, Pair(Maybe<&2, 0x6a946b742ab6f76f0ba5b90b14da52ad/http.Jar>, Result<&1, &1, 0x6a946b742ab6f76f0ba5b90b14da52ad/http.Err, 0x6a946b742ab6f76f0ba5b90b14da52ad/http.Res>)))

One try: the redirect loop on the pool, with the jar and the certificate, then the body decoded.

def hint.ms source · line 151 · raw

@m:Maybe<&2, U32> -> Maybe<&2, Nat>

def outcome source · line 160 · raw

@+idem:Bool -> @m:Maybe<&2, String> -> @now:0xe00bbf72c3ff9dc6d7b077524522b3a8/time.Instant -> 0x5f997a9351e7133e43ba9611e734f8a6/retry.Outcome

m is Http.retry.judge's verdict for an idempotent request: Some{Retry-After} when another try may help. A non-idempotent request with that verdict is Unsafe: the breaker counts it, but it never repeats.

def lift source · line 167 · raw

@r:Result<&1, &1, 0x6a946b742ab6f76f0ba5b90b14da52ad/http.Err, 0x6a946b742ab6f76f0ba5b90b14da52ad/http.Res> -> Result<&1, &1, Err, 0x6a946b742ab6f76f0ba5b90b14da52ad/http.Res>

def clock source · line 180 · raw

@need:Bool -> IO(Nat)

The clock, read only when a decision depends on it; 0 otherwise, which no such decision reads.

def breaker.on source · line 187 · raw

@br:0x5f997a9351e7133e43ba9611e734f8a6/retry.Breaker -> Bool

def transient source · line 194 · raw

@o:0x5f997a9351e7133e43ba9611e734f8a6/retry.Outcome -> Bool

def some source · line 203 · raw

@m:Maybe<&2, Nat> -> Bool

def deadline.some source · line 210 · raw

@b:0x5f997a9351e7133e43ba9611e734f8a6/retry.Budget -> Bool

def outcome.io source · line 215 · raw

@+idem:Bool -> @m:Maybe<&2, String> -> IO(0x5f997a9351e7133e43ba9611e734f8a6/retry.Outcome)

The wall clock dates a Retry-After, so it is read only for a transient failure.

def jitter.io source · line 225 · raw

@need:Bool -> IO(Nat)

Jitter for the backoff, drawn only when a retry may follow.

def settle.at source · line 234 · raw

@+now:Nat -> @+rnd:Nat -> @+p:0x5f997a9351e7133e43ba9611e734f8a6/retry.Policy -> @+n:Nat -> @+o:0x5f997a9351e7133e43ba9611e734f8a6/retry.Outcome -> @br:0x5f997a9351e7133e43ba9611e734f8a6/retry.Breaker -> @+b:0x5f997a9351e7133e43ba9611e734f8a6/retry.Budget -> @body:0x49814d83de8f70993a43e1002be29ecd/bytes.Bytes -> @pool:0x6a946b742ab6f76f0ba5b90b14da52ad/http.Pool -> @jar:Maybe<&2, 0x6a946b742ab6f76f0ba5b90b14da52ad/http.Jar> -> @r:Result<&1, &1, 0x6a946b742ab6f76f0ba5b90b14da52ad/http.Err, 0x6a946b742ab6f76f0ba5b90b14da52ad/http.Res> -> Step

def settle.outcome source · line 239 · raw

@+p:0x5f997a9351e7133e43ba9611e734f8a6/retry.Policy -> @+n:Nat -> @+o:0x5f997a9351e7133e43ba9611e734f8a6/retry.Outcome -> @+br:0x5f997a9351e7133e43ba9611e734f8a6/retry.Breaker -> @b:0x5f997a9351e7133e43ba9611e734f8a6/retry.Budget -> @body:0x49814d83de8f70993a43e1002be29ecd/bytes.Bytes -> @pool:0x6a946b742ab6f76f0ba5b90b14da52ad/http.Pool -> @jar:Maybe<&2, 0x6a946b742ab6f76f0ba5b90b14da52ad/http.Jar> -> @r:Result<&1, &1, 0x6a946b742ab6f76f0ba5b90b14da52ad/http.Err, 0x6a946b742ab6f76f0ba5b90b14da52ad/http.Res> -> IO(Step)

The attempt has ended: tell the breaker, and plan the next step.

def settle.judged source · line 246 · raw

@+p:0x5f997a9351e7133e43ba9611e734f8a6/retry.Policy -> @+idem:Bool -> @+n:Nat -> @br:0x5f997a9351e7133e43ba9611e734f8a6/retry.Breaker -> @b:0x5f997a9351e7133e43ba9611e734f8a6/retry.Budget -> @body:0x49814d83de8f70993a43e1002be29ecd/bytes.Bytes -> @pool:0x6a946b742ab6f76f0ba5b90b14da52ad/http.Pool -> @jar:Maybe<&2, 0x6a946b742ab6f76f0ba5b90b14da52ad/http.Jar> -> @j:Pair(Result<&1, &1, 0x6a946b742ab6f76f0ba5b90b14da52ad/http.Err, 0x6a946b742ab6f76f0ba5b90b14da52ad/http.Res>, Maybe<&2, String>) -> IO(Step)

def judge source · line 254 · raw

@r:Result<&1, &1, 0x6a946b742ab6f76f0ba5b90b14da52ad/http.Err, 0x6a946b742ab6f76f0ba5b90b14da52ad/http.Res> -> Pair(Result<&1, &1, 0x6a946b742ab6f76f0ba5b90b14da52ad/http.Err, 0x6a946b742ab6f76f0ba5b90b14da52ad/http.Res>, Maybe<&2, String>)

The default judge: Http.retry.judge, as if the request were idempotent; outcome then applies idem.

def denied source · line 273 · raw

@why:0x5f997a9351e7133e43ba9611e734f8a6/retry.Deny -> @last:Maybe<&1, Result<&1, &1, Err, 0x6a946b742ab6f76f0ba5b90b14da52ad/http.Res>> -> Maybe<&1, Result<&1, &1, Err, 0x6a946b742ab6f76f0ba5b90b14da52ad/http.Res>>

def pause source · line 301 · raw

@ms:Nat -> IO(Unit)

def result.of source · line 326 · raw

@m:Maybe<&1, Result<&1, &1, Err, 0x6a946b742ab6f76f0ba5b90b14da52ad/http.Res>> -> Result<&1, &1, Err, 0x6a946b742ab6f76f0ba5b90b14da52ad/http.Res>

def request.done source · line 333 · raw

@+o:Opts -> @s:Step -> Pair(Client, Pair(0x5f997a9351e7133e43ba9611e734f8a6/retry.Budget, Result<&1, &1, Err, 0x6a946b742ab6f76f0ba5b90b14da52ad/http.Res>))

def attempts source · line 337 · raw

@p:0x5f997a9351e7133e43ba9611e734f8a6/retry.Policy -> Nat

def budget source · line 342 · raw

@c:Client -> IO(Pair(Client, 0x5f997a9351e7133e43ba9611e734f8a6/retry.Budget))

The budget of one plain request: the policy's attempts, and its deadline from now. The clock is read only for a deadline.

def request.in source · line 367 · raw

@c:Client -> @b:0x5f997a9351e7133e43ba9611e734f8a6/retry.Budget -> @+method:String -> @url:String -> @headers:Map<&2, List<&2, String>> -> @body:0x49814d83de8f70993a43e1002be29ecd/bytes.Bytes -> IO(Pair(Client, Pair(0x5f997a9351e7133e43ba9611e734f8a6/retry.Budget, Result<&1, &1, Err, 0x6a946b742ab6f76f0ba5b90b14da52ad/http.Res>)))

request.as with judge, the idempotency of method, and every coding Http decodes.

def request.drop source · line 373 · raw

@x:Pair(Client, Pair(0x5f997a9351e7133e43ba9611e734f8a6/retry.Budget, Result<&1, &1, Err, 0x6a946b742ab6f76f0ba5b90b14da52ad/http.Res>)) -> Pair(Client, Result<&1, &1, Err, 0x6a946b742ab6f76f0ba5b90b14da52ad/http.Res>)

def request.budget source · line 377 · raw

@+method:String -> @url:String -> @headers:Map<&2, List<&2, String>> -> @body:0x49814d83de8f70993a43e1002be29ecd/bytes.Bytes -> @x:Pair(Client, 0x5f997a9351e7133e43ba9611e734f8a6/retry.Budget) -> IO(Pair(Client, Pair(0x5f997a9351e7133e43ba9611e734f8a6/retry.Budget, Result<&1, &1, Err, 0x6a946b742ab6f76f0ba5b90b14da52ad/http.Res>)))

def request source · line 387 · raw

@c:Client -> @+method:String -> @url:String -> @headers:Map<&2, List<&2, String>> -> @body:0x49814d83de8f70993a43e1002be29ecd/bytes.Bytes -> IO(Pair(Client, Result<&1, &1, Err, 0x6a946b742ab6f76f0ba5b90b14da52ad/http.Res>))

url resolves against the base; headers go over the defaults. Redirects follow the client's mode, at most 20. Each request gets a fresh budget from budget(c). Before each attempt the deadline, the budget, and the breaker must admit it. GET, HEAD, OPTIONS, TRACE, PUT, and DELETE try again after a connect error, a timeout, or 408, 429, 500, 502, 503, or 504, as Http.retry.judge says, on the same pool. When the attempts run out, the last result comes back; when none started, ErrDenied says why.

def get source · line 394 · raw

@c:Client -> @url:String -> IO(Pair(Client, Result<&1, &1, Err, 0x6a946b742ab6f76f0ba5b90b14da52ad/http.Res>))

def post.json source · line 398 · raw

@c:Client -> @url:String -> @v:0x584fc27920487ceab242392391418d7f/json.Val -> IO(Pair(Client, Result<&1, &1, Err, 0x6a946b742ab6f76f0ba5b90b14da52ad/http.Res>))

POST v as compact JSON, with content-type: application/json.

Templates

template settle source · line 257 · raw

@-judge:(@_:Result<&1, &1, 0x6a946b742ab6f76f0ba5b90b14da52ad/http.Err, 0x6a946b742ab6f76f0ba5b90b14da52ad/http.Res> -> Pair(Result<&1, &1, 0x6a946b742ab6f76f0ba5b90b14da52ad/http.Err, 0x6a946b742ab6f76f0ba5b90b14da52ad/http.Res>, Maybe<&2, String>)) -> @+p:0x5f997a9351e7133e43ba9611e734f8a6/retry.Policy -> @+idem:Bool -> @+n:Nat -> @br:0x5f997a9351e7133e43ba9611e734f8a6/retry.Breaker -> @b:0x5f997a9351e7133e43ba9611e734f8a6/retry.Budget -> @body:0x49814d83de8f70993a43e1002be29ecd/bytes.Bytes -> @x:Pair(0x6a946b742ab6f76f0ba5b90b14da52ad/http.Pool, Pair(Maybe<&2, 0x6a946b742ab6f76f0ba5b90b14da52ad/http.Jar>, Result<&1, &1, 0x6a946b742ab6f76f0ba5b90b14da52ad/http.Err, 0x6a946b742ab6f76f0ba5b90b14da52ad/http.Res>)) -> IO(Step)

template attempt.run source · line 263 · raw

@-judge:(@_:Result<&1, &1, 0x6a946b742ab6f76f0ba5b90b14da52ad/http.Err, 0x6a946b742ab6f76f0ba5b90b14da52ad/http.Res> -> Pair(Result<&1, &1, 0x6a946b742ab6f76f0ba5b90b14da52ad/http.Err, 0x6a946b742ab6f76f0ba5b90b14da52ad/http.Res>, Maybe<&2, String>)) -> @+o:Opts -> @+enc:String -> @+idem:Bool -> @+method:String -> @+u:Maybe<&2, 0x1f2d80f53f971b16c6de6a65cb1918ae/url.Abs> -> @+headers:Map<&2, List<&2, String>> -> @+n:Nat -> @pool:0x6a946b742ab6f76f0ba5b90b14da52ad/http.Pool -> @jar:Maybe<&2, 0x6a946b742ab6f76f0ba5b90b14da52ad/http.Jar> -> @br:0x5f997a9351e7133e43ba9611e734f8a6/retry.Breaker -> @b:0x5f997a9351e7133e43ba9611e734f8a6/retry.Budget -> @r:Pair(0x49814d83de8f70993a43e1002be29ecd/bytes.Bytes, 0x49814d83de8f70993a43e1002be29ecd/bytes.Bytes) -> IO(Step)

template attempt.gate source · line 280 · raw

@-judge:(@_:Result<&1, &1, 0x6a946b742ab6f76f0ba5b90b14da52ad/http.Err, 0x6a946b742ab6f76f0ba5b90b14da52ad/http.Res> -> Pair(Result<&1, &1, 0x6a946b742ab6f76f0ba5b90b14da52ad/http.Err, 0x6a946b742ab6f76f0ba5b90b14da52ad/http.Res>, Maybe<&2, String>)) -> @x:Pair(0x5f997a9351e7133e43ba9611e734f8a6/retry.Breaker, Pair(0x5f997a9351e7133e43ba9611e734f8a6/retry.Budget, 0x5f997a9351e7133e43ba9611e734f8a6/retry.Gate)) -> @+o:Opts -> @+enc:String -> @+idem:Bool -> @+method:String -> @+u:Maybe<&2, 0x1f2d80f53f971b16c6de6a65cb1918ae/url.Abs> -> @+headers:Map<&2, List<&2, String>> -> @+n:Nat -> @pool:0x6a946b742ab6f76f0ba5b90b14da52ad/http.Pool -> @jar:Maybe<&2, 0x6a946b742ab6f76f0ba5b90b14da52ad/http.Jar> -> @body:0x49814d83de8f70993a43e1002be29ecd/bytes.Bytes -> @last:Maybe<&1, Result<&1, &1, Err, 0x6a946b742ab6f76f0ba5b90b14da52ad/http.Res>> -> IO(Step)

template attempt source · line 293 · raw

@-judge:(@_:Result<&1, &1, 0x6a946b742ab6f76f0ba5b90b14da52ad/http.Err, 0x6a946b742ab6f76f0ba5b90b14da52ad/http.Res> -> Pair(Result<&1, &1, 0x6a946b742ab6f76f0ba5b90b14da52ad/http.Err, 0x6a946b742ab6f76f0ba5b90b14da52ad/http.Res>, Maybe<&2, String>)) -> @+o:Opts -> @+enc:String -> @+idem:Bool -> @+method:String -> @+u:Maybe<&2, 0x1f2d80f53f971b16c6de6a65cb1918ae/url.Abs> -> @+headers:Map<&2, List<&2, String>> -> @+n:Nat -> @s:Step -> IO(Step)

Ask the gate; when it admits, make attempt n (0 first) of this layer. With no deadline and no breaker, admission does not read the clock.

template go source · line 309 · raw

@-judge:(@_:Result<&1, &1, 0x6a946b742ab6f76f0ba5b90b14da52ad/http.Err, 0x6a946b742ab6f76f0ba5b90b14da52ad/http.Res> -> Pair(Result<&1, &1, 0x6a946b742ab6f76f0ba5b90b14da52ad/http.Err, 0x6a946b742ab6f76f0ba5b90b14da52ad/http.Res>, Maybe<&2, String>)) -> @left:Nat -> @+n:Nat -> @+o:Opts -> @+enc:String -> @+idem:Bool -> @+method:String -> @+u:Maybe<&2, 0x1f2d80f53f971b16c6de6a65cb1918ae/url.Abs> -> @+headers:Map<&2, List<&2, String>> -> @s:Step -> IO(Step)

left: attempts this layer may still make. n: attempts made so far. Each turn follows the plan of the last step.

template request.as source · line 355 · raw

@-judge:(@_:Result<&1, &1, 0x6a946b742ab6f76f0ba5b90b14da52ad/http.Err, 0x6a946b742ab6f76f0ba5b90b14da52ad/http.Res> -> Pair(Result<&1, &1, 0x6a946b742ab6f76f0ba5b90b14da52ad/http.Err, 0x6a946b742ab6f76f0ba5b90b14da52ad/http.Res>, Maybe<&2, String>)) -> @c:Client -> @b:0x5f997a9351e7133e43ba9611e734f8a6/retry.Budget -> @+idem:Bool -> @+enc:String -> @+method:String -> @url:String -> @headers:Map<&2, List<&2, String>> -> @body:0x49814d83de8f70993a43e1002be29ecd/bytes.Bytes -> IO(Pair(Client, Pair(0x5f997a9351e7133e43ba9611e734f8a6/retry.Budget, Result<&1, &1, Err, 0x6a946b742ab6f76f0ba5b90b14da52ad/http.Res>)))

One request inside a caller's retry layer, with every choice explicit. Every attempt takes from b, the caller's shared budget, and obeys its deadline; the client's own policy caps the attempts of this call. The budget left comes back beside the result: pass it to the next call, so nested layers never reset or multiply it. The client's own deadline does not apply here; b's does. judge: Some{Retry-After} when another attempt may help. idem: the request is safe to repeat, so a judged failure is retried; else the breaker counts it and it never repeats. enc: the Accept-Encoding codings.