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