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)