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)