src/client.bend
emerging-ezhttp@0.18.0.0 · 172 lines · 0 definitions
1# ezhttp/src/client: fetch a URL. URL and HTTP parsing joined to the wire effect.
2# Answers a structured Response (status + headers + body), not a hub-style
3# "0\n…" string contract.
4import Base
5import emerging-ezhttp@0.18.0.0/src/url.bend as Url
6import emerging-ezhttp@0.18.0.0/src/http.bend as Http
7import emerging-ezhttp@0.18.0.0/src/body.bend as Body
8import emerging-ezhttp@0.18.0.0/src/wire.bend as Wire
9import emerging-ezhttp@0.18.0.0/src/wirespec.bend as Spec
10import emerging-ezhttp@0.18.0.0/src/redirect.bend as Redirect
11import emerging-ezhttp@0.18.0.0/src/auth.bend as Auth
12
13# structured client result: transport/parse error, or an HTTP response with
14# its status line's reason phrase (SPEC.md HTTP-CLI-11)
15type Response is Data: # noqa: L001 the wire effect keeps src/client.bend out of law files (HTTP-TRUST-5)
16 Err{why: String}
17 Response{status: U32, headers: List<&2, Http.Header>, body: String,
18 reason: String}
19
20# a torn or successful HTTP parse as a client Response, with the reason
21# phrase read from the same text
22def from.reply(r: Http.Reply, reason: String) -> Response:
23 match r:
24 case Http.Torn{why}:
25 Err{why}
26 case Http.Reply{status, headers, body}:
27 Response{status, headers, body, reason}
28
29# an exchange's answer as a client Response, read by the request's method
30# (wirespec.bend wire.heard: interim 1xx heads skipped, no body for HEAD,
31# 1xx, 204 or 304; a failed exchange keeps its reason)
32def http.heard(+method: String, +raw: String) -> Response:
33 from.reply(Spec.wire.heard(method, raw), Spec.wire.phrase(raw))
34
35# an exchange as wirespec.bend wrote it, with the body's tail of octets: sent
36# and read by its own method, or the URL's reason
37def http.send(ex: Spec.Exchange, tail: List<&2, U32>) -> IO(Response):
38 match ex:
39 case Spec.NoUrl{why}:
40 IO.pure(Response, Err{why})
41 case Spec.Exchange{secure, host, port, +method, req}:
42 do IO<Response>:
43 +raw : String <- Wire.wire.talk(secure, host, port, method, req, tail)
44 IO.pure(Response, http.heard(method, raw))
45
46# a URL that came apart (or not) into an HTTP exchange with this body
47def http.loc(method: String, l: Url.Loc, headers: List<&2, Http.Header>,
48 +body: Body.Body) -> IO(Response):
49 http.send(Spec.wire.exchange.body(method, l, headers, body),
50 Spec.wire.tail(body))
51
52# an exchange as wirespec.bend wrote it, bounded by a timeout, sent and read
53# by its own method
54def http.send.timed(ms: U32, ex: Spec.Exchange, tail: List<&2, U32>)
55 -> IO(Response):
56 match ex:
57 case Spec.NoUrl{why}:
58 IO.pure(Response, Err{why})
59 case Spec.Exchange{secure, host, port, +method, req}:
60 do IO<Response>:
61 +raw : String <- Wire.wire.talk.timed(secure, ms, host, port, method,
62 req, tail)
63 IO.pure(Response, http.heard(method, raw))
64
65# a request with this method, headers and body, bounded by a timeout in
66# milliseconds
67def client.timed(method: String, url: String, headers: List<&2, Http.Header>,
68 +body: Body.Body, ms: U32) -> IO(Response):
69 http.send.timed(ms, Spec.wire.exchange.body(method, Url.parse(url), headers,
70 body), Spec.wire.tail(body))
71
72# GET bounded by a timeout in milliseconds
73def client.get.timed(url: String, headers: List<&2, Http.Header>, ms: U32)
74 -> IO(Response):
75 client.timed("GET", url, headers, Body.Empty{}, ms)
76
77# the next hop after a response (redirect.bend); a failed exchange stops
78def client.hop(left: Nat, url: String, r: Response) -> Redirect.Hop:
79 match r:
80 case Err{why}:
81 Redirect.Stop{}
82 case Response{status, headers, body, reason}:
83 Redirect.redirect.plan(left, url, status, headers)
84
85# a response, or the GET its hop asks for
86def client.after(hop: Redirect.Hop, r: Response, go: String -> IO(Response))
87 -> IO(Response):
88 match hop:
89 case Redirect.Stop{}:
90 IO.pure(Response, r)
91 case Redirect.Go{next}:
92 go(next)
93
94# GET, following redirects while `left` hops remain; `body` goes with this
95# hop only. The answer is the last response, which is a redirect only when
96# the hops ran out or its Location is not one redirect.bend follows
97def client.follow(+left: Nat, +url: String, +headers: List<&2, Http.Header>,
98 body: Body.Body, +ms: U32) -> IO(Response):
99 match left:
100 case 0n:
101 client.timed("GET", url, headers, body, ms)
102 case 1n+p:
103 do IO<Response>:
104 +r : Response <- client.timed("GET", url, headers, body, ms)
105 client.after(client.hop(left, url, r), r,
106 next => client.follow(p, next, headers, Body.Empty{}, ms))
107
108# a request once it is known whether it follows redirects
109def client.request.go(go: Bool, method: String, url: String,
110 headers: List<&2, Http.Header>, body: Body.Body, ms: U32) -> IO(Response):
111 match go:
112 case True{}:
113 client.follow(Redirect.redirect.max(), url, headers, body, ms)
114 case False{}:
115 client.timed(method, url, headers, body, ms)
116
117# a request with a method, headers, body and timeout; it follows redirects
118# when `follow` is set and the method is GET (redirect.bend
119# redirect.follows), and answers its first response otherwise
120def client.request.opts(+method: String, url: String,
121 headers: List<&2, Http.Header>, body: Body.Body, ms: U32, follow: Bool)
122 -> IO(Response):
123 client.request.go(Redirect.redirect.follows(method, follow), method, url,
124 headers, body, ms)
125
126# GET with headers and a timeout, following redirects when `follow` is set
127def client.get.opts(url: String, headers: List<&2, Http.Header>, ms: U32,
128 follow: Bool) -> IO(Response):
129 client.request.opts("GET", url, headers, Body.Empty{}, ms, follow)
130
131# whether a response is the failure of an exchange that ran out of time
132def client.timed_out(r: Response) -> Bool:
133 match r:
134 case Err{why}:
135 Spec.wire.timed_out(why)
136 case Response{status, headers, body, reason}:
137 False{}
138
139# GET
140def client.get(url: String, headers: List<&2, Http.Header>) -> IO(Response):
141 http.loc("GET", Url.parse(url), headers, Body.Empty{})
142
143# HEAD (RFC 9110 §9.3.2)
144def client.head(url: String, headers: List<&2, Http.Header>) -> IO(Response):
145 http.loc("HEAD", Url.parse(url), headers, Body.Empty{})
146
147# POST
148def client.post(url: String, headers: List<&2, Http.Header>, body: Body.Body)
149 -> IO(Response):
150 http.loc("POST", Url.parse(url), headers, body)
151
152# PUT
153def client.put(url: String, headers: List<&2, Http.Header>, body: Body.Body)
154 -> IO(Response):
155 http.loc("PUT", Url.parse(url), headers, body)
156
157# DELETE
158def client.delete(url: String, headers: List<&2, Http.Header>, body: Body.Body)
159 -> IO(Response):
160 http.loc("DELETE", Url.parse(url), headers, body)
161
162# OPTIONS
163def client.options(url: String, headers: List<&2, Http.Header>) -> IO(Response):
164 http.loc("OPTIONS", Url.parse(url), headers, Body.Empty{})
165
166# Authorization: Bearer … (auth.bend)
167def client.bearer(token: String) -> Http.Header:
168 Auth.auth.bearer(token)
169
170# Authorization: Basic … (auth.bend; RFC 7617, RFC 4648 Base64)
171def client.basic(user: String, password: String) -> Http.Header:
172 Auth.auth.basic(user, password)