src/client.bend

emerging-ezhttp@0.17.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.17.0.0/src/url.bend as Url
6import emerging-ezhttp@0.17.0.0/src/http.bend as Http
7import emerging-ezhttp@0.17.0.0/src/body.bend as Body
8import emerging-ezhttp@0.17.0.0/src/wire.bend as Wire
9import emerging-ezhttp@0.17.0.0/src/wirespec.bend as Spec
10import emerging-ezhttp@0.17.0.0/src/redirect.bend as Redirect
11import emerging-ezhttp@0.17.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:
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)