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