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)