hairpin.bend
bend-kit-hairpin@0.5.1.0 · 449 lines · 0 definitions
1# 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
2import Base
3import bend-kit-http@0.32.0.0/http.bend as Http
4import bend-kit-time@0.1.2.1/time.bend as Time
5# Http takes time 0.1.2.1 Instants; the monotonic Nat clock comes from 0.1.3.0.
6import bend-kit-time@0.1.3.0/time.bend as Clock
7# By hash, as http imports them: bend-kit-url@0.4.1.0, -json@0.5.0.1, -bytes@0.3.0.0.
8import bend-kit-url@0.4.1.0/url.bend as Url
9import bend-kit-json@0.5.0.1/json.bend as Json
10import bend-kit-bytes@0.3.0.0/bytes.bend as Bytes
11import bend-kit-retry@0.1.0.0/retry.bend as Retry
12
13# base: "" for none. headers: lowercase names. ms: the step timeout. policy: attempts, backoff, and deadline of each request.
14type Opts is Data:
15 Opts{base: String, headers: Map<&2, List<&2, String>>, ms: U32, mode: Http.Mode, policy: Retry.Policy, cert: Maybe<&2, Http.Cert>}
16
17# breaker: the circuit breaker of every request on this client.
18type Client is Type:
19 Client{opts: Opts, pool: Http.Pool, jar: Maybe<&2, Http.Jar>, breaker: Retry.Breaker}
20
21# ErrHttp: the last attempt failed. ErrDenied: no attempt started, for why.
22type Err is Data:
23 ErrHttp{error: Http.Err}
24 ErrDenied{why: Retry.Deny}
25
26# One attempt, the first included; backoff from 500 ms, doubled to at most 30 s; no deadline.
27def policy() -> Retry.Policy:
28 Retry.Policy{1n, 500n, 30000, None{}}
29
30# A pool of up to cap idle sockets per origin, no base, no default headers, 30 s steps, follow redirects,
31# no retries, and no circuit breaker.
32def new.with(+cap: U32) -> Client:
33 Client{Opts{"", Http.empty(), 30000, Http.ModeFollow{}, policy(), None{}}, Http.pool.new.with(cap), None{}, Retry.BreakerOff{}}
34
35def new() -> Client:
36 new.with(8)
37
38def base(c: Client, url: String) -> Client:
39 Client{Opts{b, h, ms, mode, p, cert}, pool, j, br} = c
40 Client{Opts{url, h, ms, mode, p, cert}, pool, j, br}
41
42# A default header. It replaces an earlier default of the same name; a request header of that name replaces it.
43def header(c: Client, k: String, v: String) -> Client:
44 Client{Opts{b, h, ms, mode, p, cert}, pool, j, br} = c
45 Client{Opts{b, Http.set(h, String.to_lower(k), v), ms, mode, p, cert}, pool, j, br}
46
47def timeout(c: Client, ms: U32) -> Client:
48 Client{Opts{b, h, old, mode, p, cert}, pool, j, br} = c
49 Client{Opts{b, h, ms, mode, p, cert}, pool, j, br}
50
51def redirect(c: Client, mode: Http.Mode) -> Client:
52 Client{Opts{b, h, ms, old, p, cert}, pool, j, br} = c
53 Client{Opts{b, h, ms, mode, p, cert}, pool, j, br}
54
55# Up to n retries after the first attempt: n + 1 attempts in all.
56def retry(c: Client, n: U32) -> Client:
57 Client{Opts{b, h, ms, mode, Retry.Policy{old, base, cap, d}, cert}, pool, j, br} = c
58 Client{Opts{b, h, ms, mode, Retry.Policy{1n+U32.to_nat(n), base, cap, d}, cert}, pool, j, br}
59
60# No attempt of a request starts ms or more after the request starts.
61def deadline(c: Client, ms: U32) -> Client:
62 Client{Opts{b, h, t, mode, Retry.Policy{a, base, cap, old}, cert}, pool, j, br} = c
63 Client{Opts{b, h, t, mode, Retry.Policy{a, base, cap, Some{U32.to_nat(ms)}}, cert}, pool, j, br}
64
65# A circuit breaker in front of every request, starting closed.
66def circuit(c: Client, circ: Retry.Circuit) -> Client:
67 Client{o, pool, j, old} = c
68 Client{o, pool, j, Retry.Breaker.new(circ)}
69
70# The breaker, for inspection.
71def breaker.of(c: Client) -> Client & Retry.Breaker:
72 Client{o, pool, j, +br} = c
73 (Client{o, pool, j, br}, br)
74
75# Paths to a PEM client chain and its private key, presented to every HTTPS origin.
76def cert(c: Client, chain: String, key: String) -> Client:
77 Client{Opts{b, h, ms, mode, p, old}, pool, j, br} = c
78 Client{Opts{b, h, ms, mode, p, Some{Http.Cert{chain, key}}}, pool, j, br}
79
80# Every request stores and sends cookies through jar. Its clock moves to Time.now() before each try.
81def jar(c: Client, jar: Http.Jar) -> Client:
82 Client{o, pool, old, br} = c
83 Client{o, pool, Some{jar}, br}
84
85# The jar, if the client has one.
86def jar.of(c: Client) -> Client & Maybe<&2, Http.Jar>:
87 Client{o, pool, +j, br} = c
88 (Client{o, pool, j, br}, j)
89
90def close(c: Client) -> IO(Unit):
91 Client{o, pool, j, br} = c
92 Http.pool.close(pool)
93
94def target.pick(none: Bool, b: String, ref: String) -> Maybe<&2, String>:
95 match none:
96 case True{}:
97 Some{ref}
98 case False{}:
99 Http.resolve(b, ref)
100
101# ref against the base (RFC 3986 §5.2); with no base, ref as given. None when the base is not absolute.
102def target(+b: String, ref: String) -> Maybe<&2, String>:
103 target.pick(String.is_empty(b), b, ref)
104
105def target.abs(m: Maybe<&2, String>) -> Maybe<&2, Url.Abs>:
106 match m:
107 case None{}:
108 None{}
109 case Some{s}:
110 Url.absolute(s)
111
112# The defaults under the request headers, names lowercased; a request header replaces a default of its name.
113# Host, Connection, Content-Length, and Transfer-Encoding are the client's own, so both sides drop them.
114def merge(defaults: Map<&2, List<&2, String>>, headers: Map<&2, List<&2, String>>) -> Map<&2, List<&2, String>>:
115 Http.req.put(Map.to_list(&2, List<&2, String>, headers), Http.req.put(Map.to_list(&2, List<&2, String>, defaults), Http.empty()))
116
117def jar.now(j: Maybe<&2, Http.Jar>) -> IO(Maybe<&2, Http.Jar>):
118 match j:
119 case None{}:
120 IO.pure(Maybe<&2, Http.Jar>, None{})
121 case Some{jar}:
122 do IO<Maybe<&2, Http.Jar>>:
123 now : Time.Instant <- Time.now()
124 return Some{Http.jar.at(jar, now)}
125
126def once.final(+enc: String, x: Http.Pool & Result<&1, &1, Http.Err, Http.Res> & Maybe<&2, Http.Jar>) ->
127 IO(Http.Pool & Maybe<&2, Http.Jar> & Result<&1, &1, Http.Err, Http.Res>):
128 (p, r, j) = x
129 do IO<Http.Pool & Maybe<&2, Http.Jar> & Result<&1, &1, Http.Err, Http.Res>>:
130 d : Result<&1, &1, Http.Err, Http.Res> <- Http.fetch.final(enc, r)
131 return (p, j, d)
132
133# One try: the redirect loop on the pool, with the jar and the certificate, then the body decoded.
134def once(+ms: U32, +mode: Http.Mode, +cert: Maybe<&2, Http.Cert>, +enc: String, p: Http.Pool, j: Maybe<&2, Http.Jar>,
135 +method: String, +u: Maybe<&2, Url.Abs>, +headers: Map<&2, List<&2, String>>, body: Bytes.Bytes) ->
136 IO(Http.Pool & Maybe<&2, Http.Jar> & Result<&1, &1, Http.Err, Http.Res>):
137 do IO<Http.Pool & Maybe<&2, Http.Jar> & Result<&1, &1, Http.Err, Http.Res>>:
138 now : Maybe<&2, Http.Jar> <- jar.now(j)
139 x : Http.Pool & Result<&1, &1, Http.Err, Http.Res> & Maybe<&2, Http.Jar> <-
140 Http.pool.hops(mode, ms, Http.Hops{p, Http.fetch.start(method, headers, body, u, enc), cert, now, ""})
141 once.final(enc, x)
142
143def hint.ms(m: Maybe<&2, U32>) -> Maybe<&2, Nat>:
144 match m:
145 case None{}:
146 None{}
147 case Some{ms}:
148 Some{U32.to_nat(ms)}
149
150# m is Http.retry.judge's verdict for an idempotent request: Some{Retry-After} when another try may help.
151# A non-idempotent request with that verdict is Unsafe: the breaker counts it, but it never repeats.
152def outcome(+idem: Bool, m: Maybe<&2, String>, now: Time.Instant) -> Retry.Outcome:
153 match m:
154 case None{}:
155 Retry.Settled{}
156 case Some{+v}:
157 Bool.pick(Retry.Outcome, idem, Retry.Transient{hint.ms(Http.retry.after(v, now))}, Retry.Unsafe{})
158
159def lift(-R: Type, r: Result<&1, &1, Http.Err, R>) -> Result<&1, &1, Err, R>:
160 match r:
161 case Fail{e}:
162 Fail{ErrHttp{e}}
163 case Done{res}:
164 Done{res}
165
166# The live state of one request between attempts. R: a response or a stream. last: the result of the last
167# attempt, None before the first.
168type Step<-R: Type> is Type:
169 Step{pool: Http.Pool, jar: Maybe<&2, Http.Jar>, breaker: Retry.Breaker, budget: Retry.Budget, body: Bytes.Bytes,
170 last: Maybe<&1, Result<&1, &1, Err, R>>, plan: Retry.Plan}
171
172# The clock, read only when a decision depends on it; 0 otherwise, which no such decision reads.
173def clock(need: Bool) -> IO(Nat):
174 match need:
175 case True{}:
176 Clock.mono.ms()
177 case False{}:
178 IO.pure(Nat, 0n)
179
180def breaker.on(br: Retry.Breaker) -> Bool:
181 match br:
182 case Retry.BreakerOff{}:
183 False{}
184 case Retry.BreakerOn{c, s}:
185 True{}
186
187def transient(o: Retry.Outcome) -> Bool:
188 match o:
189 case Retry.Transient{after}:
190 True{}
191 case Retry.Settled{}:
192 False{}
193 case Retry.Unsafe{}:
194 False{}
195
196def some(m: Maybe<&2, Nat>) -> Bool:
197 match m:
198 case None{}:
199 False{}
200 case Some{t}:
201 True{}
202
203def deadline.some(b: Retry.Budget) -> Bool:
204 Retry.Budget{l, d} = b
205 some(d)
206
207# The wall clock dates a Retry-After, so it is read only for a transient failure.
208def outcome.io(+idem: Bool, m: Maybe<&2, String>) -> IO(Retry.Outcome):
209 match m:
210 case None{}:
211 IO.pure(Retry.Outcome, Retry.Settled{})
212 case Some{v}:
213 do IO<Retry.Outcome>:
214 wall : Time.Instant <- Time.now()
215 return outcome(idem, Some{v}, wall)
216
217# Jitter for the backoff, drawn only when a retry may follow.
218def jitter.io(need: Bool) -> IO(Nat):
219 match need:
220 case True{}:
221 do IO<Nat>:
222 rnd : Result<&1, &1, U32 & String, U32> <- IO.random_u32()
223 return U32.to_nat(Http.retry.jitter(rnd))
224 case False{}:
225 IO.pure(Nat, 0n)
226
227def settle.at(-R: Type, +now: Nat, +rnd: Nat, +p: Retry.Policy, +n: Nat, +o: Retry.Outcome, br: Retry.Breaker, +b: Retry.Budget,
228 body: Bytes.Bytes, pool: Http.Pool, jar: Maybe<&2, Http.Jar>, r: Result<&1, &1, Http.Err, R>) -> Step<R>:
229 Step{pool, jar, Retry.Breaker.done(br, now, Retry.failed(o)), b, body, Some{lift(R, r)}, Retry.plan(o, p, b, n, now, rnd)}
230
231# The attempt has ended: tell the breaker, and plan the next step.
232def settle.outcome(-R: Type, +p: Retry.Policy, +n: Nat, +o: Retry.Outcome, +br: Retry.Breaker, b: Retry.Budget, body: Bytes.Bytes,
233 pool: Http.Pool, jar: Maybe<&2, Http.Jar>, r: Result<&1, &1, Http.Err, R>) -> IO(Step<R>):
234 do IO<Step<R>>:
235 now : Nat <- clock(Bool.or(breaker.on(br), transient(o)))
236 rnd : Nat <- jitter.io(transient(o))
237 return settle.at(R, now, rnd, p, n, o, br, b, body, pool, jar, r)
238
239def settle.judged(-R: Type, +p: Retry.Policy, +idem: Bool, +n: Nat, br: Retry.Breaker, b: Retry.Budget, body: Bytes.Bytes,
240 pool: Http.Pool, jar: Maybe<&2, Http.Jar>, j: Result<&1, &1, Http.Err, R> & Maybe<&2, String>) -> IO(Step<R>):
241 (r, m) = j
242 do IO<Step<R>>:
243 o : Retry.Outcome <- outcome.io(idem, m)
244 settle.outcome(R, p, n, o, br, b, body, pool, jar, r)
245
246# The default judge: Http.retry.judge, as if the request were idempotent; outcome then applies idem.
247def judge(r: Result<&1, &1, Http.Err, Http.Res>) -> Result<&1, &1, Http.Err, Http.Res> & Maybe<&2, String>:
248 Http.retry.judge(True{}, r)
249
250def settle(~R: Type, ~judge: Result<&1, &1, Http.Err, R> -> Result<&1, &1, Http.Err, R> & Maybe<&2, String>,
251 +p: Retry.Policy, +idem: Bool, +n: Nat, br: Retry.Breaker, b: Retry.Budget, body: Bytes.Bytes,
252 x: Http.Pool & Maybe<&2, Http.Jar> & Result<&1, &1, Http.Err, R>) -> IO(Step<R>):
253 (pool, jar, r) = x
254 settle.judged(R, p, idem, n, br, b, body, pool, jar, judge(r))
255
256# The inputs of one attempt. u: the target, None when it is not absolute. headers: the merged headers.
257type Call is Data:
258 Call{enc: String, method: String, u: Maybe<&2, Url.Abs>, headers: Map<&2, List<&2, String>>}
259
260# One attempt that reads a whole response: the redirect loop on the pool, with the jar and the certificate.
261def try.res(o: Opts, call: Call, pool: Http.Pool, jar: Maybe<&2, Http.Jar>, body: Bytes.Bytes) ->
262 IO(Http.Pool & Maybe<&2, Http.Jar> & Result<&1, &1, Http.Err, Http.Res>):
263 Opts{base, h, +ms, +mode, p, +cert} = o
264 Call{+enc, +method, +u, +headers} = call
265 once(ms, mode, cert, enc, pool, jar, method, u, headers, body)
266
267# One attempt that opens a stream on its own connection. It follows redirects and decodes the body by enc.
268# The pool, the jar, and the certificate are not used.
269def try.stream(o: Opts, call: Call, pool: Http.Pool, jar: Maybe<&2, Http.Jar>, body: Bytes.Bytes) ->
270 IO(Http.Pool & Maybe<&2, Http.Jar> & Result<&1, &1, Http.Err, Http.Stream>):
271 Opts{base, h, +ms, mode, p, cert} = o
272 Call{+enc, method, u, headers} = call
273 do IO<Http.Pool & Maybe<&2, Http.Jar> & Result<&1, &1, Http.Err, Http.Stream>>:
274 r : Result<&1, &1, Http.Err, Http.Stream> <- Http.stream.hops(ms, Http.stream.start(method, Http.accept.enc(headers, enc), body, u))
275 s : Result<&1, &1, Http.Err, Http.Stream> <- Http.stream.init(enc, r)
276 return (pool, jar, s)
277
278def close.res(r: Http.Res) -> IO(Unit):
279 IO.pure(Unit, Unit{})
280
281def close.last(~R: Type, ~close: R -> IO(Unit), last: Maybe<&1, Result<&1, &1, Err, R>>) -> IO(Unit):
282 match last:
283 case None{}:
284 IO.pure(Unit, Unit{})
285 case Some{r}:
286 match r:
287 case Fail{e}:
288 IO.pure(Unit, Unit{})
289 case Done{x}:
290 close(x)
291
292def attempt.run(~R: Type, ~judge: Result<&1, &1, Http.Err, R> -> Result<&1, &1, Http.Err, R> & Maybe<&2, String>,
293 ~try: Opts -> Call -> Http.Pool -> Maybe<&2, Http.Jar> -> Bytes.Bytes -> IO(Http.Pool & Maybe<&2, Http.Jar> & Result<&1, &1, Http.Err, R>),
294 +o: Opts, +call: Call, +idem: Bool, +n: Nat, pool: Http.Pool, jar: Maybe<&2, Http.Jar>, br: Retry.Breaker,
295 b: Retry.Budget, r: Bytes.Bytes & Bytes.Bytes) -> IO(Step<R>):
296 Opts{base, h, ms, mode, +p, cert} = o
297 (body, spare) = r
298 do IO<Step<R>>:
299 x : Http.Pool & Maybe<&2, Http.Jar> & Result<&1, &1, Http.Err, R> <- try(o, call, pool, jar, spare)
300 settle(~R, ~judge, p, idem, n, br, b, body, x)
301
302def denied(-R: Type, why: Retry.Deny, last: Maybe<&1, Result<&1, &1, Err, R>>) -> Maybe<&1, Result<&1, &1, Err, R>>:
303 match last:
304 case None{}:
305 Some{Fail{ErrDenied{why}}}
306 case Some{r}:
307 Some{r}
308
309# A refused stream closes only when the next attempt is admitted, so a denied attempt still returns it.
310def attempt.gate(~R: Type, ~judge: Result<&1, &1, Http.Err, R> -> Result<&1, &1, Http.Err, R> & Maybe<&2, String>,
311 ~try: Opts -> Call -> Http.Pool -> Maybe<&2, Http.Jar> -> Bytes.Bytes -> IO(Http.Pool & Maybe<&2, Http.Jar> & Result<&1, &1, Http.Err, R>),
312 ~close: R -> IO(Unit),
313 x: Retry.Breaker & Retry.Budget & Retry.Gate, +o: Opts, +call: Call, +idem: Bool, +n: Nat, pool: Http.Pool,
314 jar: Maybe<&2, Http.Jar>, body: Bytes.Bytes, last: Maybe<&1, Result<&1, &1, Err, R>>) -> IO(Step<R>):
315 (br, b, g) = x
316 match g:
317 case Retry.GateNo{why}:
318 IO.pure(Step<R>, Step{pool, jar, br, b, body, denied(R, why, last), Retry.PlanStop{}})
319 case Retry.GateGo{}:
320 do IO<Step<R>>:
321 close.last(~R, ~close, last)
322 attempt.run(~R, ~judge, ~try, o, call, idem, n, pool, jar, br, b, Bytes.slice(body, 0, 4294967295))
323
324# Ask the gate; when it admits, make attempt n (0 first) of this layer. With no deadline and no breaker,
325# admission does not read the clock.
326def attempt(~R: Type, ~judge: Result<&1, &1, Http.Err, R> -> Result<&1, &1, Http.Err, R> & Maybe<&2, String>,
327 ~try: Opts -> Call -> Http.Pool -> Maybe<&2, Http.Jar> -> Bytes.Bytes -> IO(Http.Pool & Maybe<&2, Http.Jar> & Result<&1, &1, Http.Err, R>),
328 ~close: R -> IO(Unit),
329 +o: Opts, +call: Call, +idem: Bool, +n: Nat, s: Step<R>) -> IO(Step<R>):
330 Step{pool, jar, +br, +b, body, last, plan} = s
331 do IO<Step<R>>:
332 now : Nat <- clock(Bool.or(breaker.on(br), deadline.some(b)))
333 attempt.gate(~R, ~judge, ~try, ~close, Retry.admit(br, b, now), o, call, idem, n, pool, jar, body, last)
334
335def pause(ms: Nat) -> IO(Unit):
336 match ms:
337 case 0n:
338 IO.pure(Unit, Unit{})
339 case 1n+k:
340 IO.sleep(U32.from_nat(1n+k))
341
342# left: attempts this layer may still make. n: attempts made so far. Each turn follows the plan of the last step.
343def go(~R: Type, ~judge: Result<&1, &1, Http.Err, R> -> Result<&1, &1, Http.Err, R> & Maybe<&2, String>,
344 ~try: Opts -> Call -> Http.Pool -> Maybe<&2, Http.Jar> -> Bytes.Bytes -> IO(Http.Pool & Maybe<&2, Http.Jar> & Result<&1, &1, Http.Err, R>),
345 ~close: R -> IO(Unit),
346 left: Nat, +n: Nat, +o: Opts, +call: Call, +idem: Bool, s: Step<R>) -> IO(Step<R>):
347 match left:
348 case 0n:
349 IO.pure(Step<R>, s)
350 case 1n+rest:
351 Step{pool, jar, br, b, body, last, plan} = s
352 match plan:
353 case Retry.PlanStop{}:
354 IO.pure(Step<R>, Step{pool, jar, br, b, body, last, Retry.PlanStop{}})
355 case Retry.PlanWait{ms}:
356 do IO<Step<R>>:
357 pause(ms)
358 next : Step<R> <- attempt(~R, ~judge, ~try, ~close, o, call, idem, n, Step{pool, jar, br, b, body, last, Retry.PlanStop{}})
359 go(~R, ~judge, ~try, ~close, rest, 1n+n, o, call, idem, next)
360
361def result.of(-R: Type, m: Maybe<&1, Result<&1, &1, Err, R>>) -> Result<&1, &1, Err, R>:
362 match m:
363 case None{}:
364 Fail{ErrDenied{Retry.DenySpent{}}}
365 case Some{r}:
366 r
367
368def request.done(-R: Type, +o: Opts, s: Step<R>) -> Client & Retry.Budget & Result<&1, &1, Err, R>:
369 Step{pool, jar, br, b, body, last, plan} = s
370 (Client{o, pool, jar, br}, b, result.of(R, last))
371
372def attempts(p: Retry.Policy) -> Nat:
373 Retry.Policy{a, base, cap, d} = p
374 a
375
376# The budget of one plain request: the policy's attempts, and its deadline from now. The clock is read only for a deadline.
377def budget(c: Client) -> IO(Client & Retry.Budget):
378 Client{+o, pool, jar, br} = c
379 Opts{base, h, ms, mode, Retry.Policy{+a, bs, cap, +d}, cert} = o
380 do IO<Client & Retry.Budget>:
381 now : Nat <- clock(some(d))
382 return (Client{o, pool, jar, br}, Retry.Budget.new(a, now, d))
383
384def run(~R: Type, ~judge: Result<&1, &1, Http.Err, R> -> Result<&1, &1, Http.Err, R> & Maybe<&2, String>,
385 ~try: Opts -> Call -> Http.Pool -> Maybe<&2, Http.Jar> -> Bytes.Bytes -> IO(Http.Pool & Maybe<&2, Http.Jar> & Result<&1, &1, Http.Err, R>),
386 ~close: R -> IO(Unit),
387 c: Client, b: Retry.Budget, +idem: Bool, enc: String, method: String, url: String,
388 headers: Map<&2, List<&2, String>>, body: Bytes.Bytes) -> IO(Client & Retry.Budget & Result<&1, &1, Err, R>):
389 Client{+o, pool, jar, br} = c
390 Opts{+base, +h, ms, mode, +p, cert} = o
391 +call = {Call{enc, method, target.abs(target(base, url)), merge(h, headers)} : Call}
392 do IO<Client & Retry.Budget & Result<&1, &1, Err, R>>:
393 s : Step<R> <- go(~R, ~judge, ~try, ~close, attempts(p), 0n, o, call, idem, Step{pool, jar, br, b, body, None{}, Retry.PlanWait{0n}})
394 return request.done(R, o, s)
395
396# One request inside a caller's retry layer, with every choice explicit. Every attempt takes from b, the
397# caller's shared budget, and obeys its deadline; the client's own policy caps the attempts of this call.
398# The budget left comes back beside the result: pass it to the next call, so nested layers never reset or
399# multiply it. The client's own deadline does not apply here; b's does.
400# judge: Some{Retry-After} when another attempt may help. idem: the request is safe to repeat, so a judged
401# failure is retried; else the breaker counts it and it never repeats. enc: the Accept-Encoding codings.
402def request.as(~judge: Result<&1, &1, Http.Err, Http.Res> -> Result<&1, &1, Http.Err, Http.Res> & Maybe<&2, String>,
403 c: Client, b: Retry.Budget, +idem: Bool, +enc: String, +method: String, url: String,
404 headers: Map<&2, List<&2, String>>, body: Bytes.Bytes) -> IO(Client & Retry.Budget & Result<&1, &1, Err, Http.Res>):
405 run(~Http.Res, ~judge, ~try.res, ~close.res, c, b, idem, enc, method, url, headers, body)
406
407# A streamed request under the same budget, deadline, backoff, and breaker as request.as. The stream opens its
408# own connection outside the pool and follows redirects; the jar and the certificate do not apply. judge reads
409# the status and headers through Http.stream.res. A refused stream closes before the next attempt; the last
410# one comes back open, so the caller can read its body.
411def open.as(~judge: Result<&1, &1, Http.Err, Http.Stream> -> Result<&1, &1, Http.Err, Http.Stream> & Maybe<&2, String>,
412 c: Client, b: Retry.Budget, +idem: Bool, +enc: String, +method: String, url: String,
413 headers: Map<&2, List<&2, String>>, body: Bytes.Bytes) -> IO(Client & Retry.Budget & Result<&1, &1, Err, Http.Stream>):
414 run(~Http.Stream, ~judge, ~try.stream, ~Http.stream.close, c, b, idem, enc, method, url, headers, body)
415
416# request.as with judge, the idempotency of method, and every coding Http decodes.
417def request.in(c: Client, b: Retry.Budget, +method: String, url: String, headers: Map<&2, List<&2, String>>,
418 body: Bytes.Bytes) -> IO(Client & Retry.Budget & Result<&1, &1, Err, Http.Res>):
419 do IO<Client & Retry.Budget & Result<&1, &1, Err, Http.Res>>:
420 enc : String <- Http.codings()
421 request.as(~judge, c, b, Http.pool.idempotent(method), enc, method, url, headers, body)
422
423def request.drop(x: Client & Retry.Budget & Result<&1, &1, Err, Http.Res>) -> Client & Result<&1, &1, Err, Http.Res>:
424 (c, b, r) = x
425 (c, r)
426
427def request.budget(+method: String, url: String, headers: Map<&2, List<&2, String>>, body: Bytes.Bytes,
428 x: Client & Retry.Budget) -> IO(Client & Retry.Budget & Result<&1, &1, Err, Http.Res>):
429 (c, b) = x
430 request.in(c, b, method, url, headers, body)
431
432# url resolves against the base; headers go over the defaults. Redirects follow the client's mode, at most 20.
433# Each request gets a fresh budget from budget(c). Before each attempt the deadline, the
434# budget, and the breaker must admit it. GET, HEAD, OPTIONS, TRACE, PUT, and DELETE try again after a connect
435# error, a timeout, or 408, 429, 500, 502, 503, or 504, as Http.retry.judge says, on the same pool.
436# When the attempts run out, the last result comes back; when none started, ErrDenied says why.
437def request(c: Client, +method: String, url: String, headers: Map<&2, List<&2, String>>, body: Bytes.Bytes) ->
438 IO(Client & Result<&1, &1, Err, Http.Res>):
439 do IO<Client & Result<&1, &1, Err, Http.Res>>:
440 y : Client & Retry.Budget <- budget(c)
441 x : Client & Retry.Budget & Result<&1, &1, Err, Http.Res> <- request.budget(method, url, headers, body, y)
442 return request.drop(x)
443
444def get(c: Client, url: String) -> IO(Client & Result<&1, &1, Err, Http.Res>):
445 request(c, "GET", url, Http.empty(), Bytes.new(0))
446
447# POST v as compact JSON, with content-type: application/json.
448def post.json(c: Client, url: String, v: Json.Val) -> IO(Client & Result<&1, &1, Err, Http.Res>):
449 request(c, "POST", url, Http.set(Http.empty(), "content-type", "application/json"), Json.encode.bytes(v))