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