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