main.bend
emerging-ezhttp@0.13.0.0 · 259 lines · 0 definitions
1# ezhttp: HTTP/1.1 client and server for Bend 2, with auth, cookie and CORS helpers.
2#
3# Shared message types, BYO JSON via encode/decode. TLS for the client stays
4# at the wire/runtime layer.
5import Base
6import emerging-ezhttp@0.13.0.0/src/url.bend as Url
7import emerging-ezhttp@0.13.0.0/src/pct.bend as Pct
8import emerging-ezhttp@0.13.0.0/src/http.bend as Http
9import emerging-ezhttp@0.13.0.0/src/body.bend as Body
10import emerging-ezhttp@0.13.0.0/src/client.bend as Client
11import emerging-ezhttp@0.13.0.0/src/server.bend as Server
12import emerging-ezhttp@0.13.0.0/src/cookie.bend as Cookie
13import emerging-ezhttp@0.13.0.0/src/cache.bend as Cache
14import emerging-ezhttp@0.13.0.0/src/cors.bend as Cors
15import emerging-ezhttp@0.13.0.0/src/html.bend as Html
16
17# URL parse re-export
18def parse_url(url: String) -> Url.Loc:
19 Url.parse(url)
20
21# percent-encode a path (RFC 3986 §2.1), as Python's urllib.parse.quote(text):
22# unreserved characters and "/" stay, every other character becomes %XX per
23# UTF-8 byte, upper-case hex (SPEC.md HTTP-URL)
24def quote_path(text: String) -> String:
25 Pct.quote(True{}, text)
26
27# percent-encode a path segment, query key or value (RFC 3986 §2.1), as
28# urllib.parse.quote(text, safe=""): like quote_path, but "/" becomes %2F
29def quote_component(text: String) -> String:
30 Pct.quote(False{}, text)
31
32# response parse re-export
33def parse_response(raw: String) -> Http.Reply:
34 Http.parse(raw)
35
36# request parse re-export (server inbound)
37def parse_request(raw: String) -> Http.Request:
38 Http.ask(raw)
39
40# format a response message (server outbound)
41def format_response(status: U32, headers: List<&2, Http.Header>, body: String)
42 -> String:
43 Http.respond(status, headers, body)
44
45# empty request body
46def empty_body() -> Body.Body:
47 Body.body.empty()
48
49# UTF-8 text request body
50def text_body(text: String) -> Body.Body:
51 Body.body.text(text)
52
53# octet-list request body
54def octets_body(bs: List<&2, U32>) -> Body.Body:
55 Body.body.octets(bs)
56
57# BYO JSON (or any codec): encode a value with the caller's to_text
58def encode(~A: Data, ~to_text: A -> String, value: A) -> Body.Body:
59 Body.body.encode(A, to_text, value)
60
61# BYO JSON (or any codec): decode body text with the caller's from_text
62def decode(
63 ~A: Data,
64 ~from_text: String -> Maybe<&2, A>,
65 text: String
66) -> Maybe<&2, A>:
67 Body.body.decode(A, from_text, text)
68
69# Bearer token header helper
70def bearer(token: String) -> Http.Header:
71 Client.client.bearer(token)
72
73# Basic credentials header (RFC 7617)
74def basic(user: String, password: String) -> Http.Header:
75 Client.client.basic(user, password)
76
77# Set-Cookie header from a parsed cookie (RFC 6265)
78def set_cookie(cookie: Cookie.Cookie) -> Http.Header:
79 Cookie.cookie.set(cookie)
80
81# parse one Set-Cookie field value
82def parse_cookie(line: String) -> Maybe<&2, Cookie.Cookie>:
83 Cookie.cookie.parse(line)
84
85# Cookie request header for a list of cookies
86def cookie_header(cs: List<&2, Cookie.Cookie>) -> Http.Header:
87 Cookie.cookie.request(cs)
88
89# parse a Cache-Control field value (RFC 9111)
90def cache_control(value: String) -> Cache.Cc:
91 Cache.cache.parse(value)
92
93# freshness of a Cache-Control value at an age in seconds
94def fresh(value: String, age: Nat) -> Bool:
95 Cache.cache.fresh(Cache.cache.parse(value), age)
96
97# CORS preflight or simple headers on a handler reply
98def cors_reply(cfg: Cors.Cfg, req: Http.Request, reply: Http.Reply) -> Http.Reply:
99 Cors.cors.on(cfg, req, reply)
100
101# escape text for HTML as markupsafe and Jinja's autoescape do: & < > " ' as
102# & < > " ', every other character as itself
103def html_escape(text: String) -> String:
104 Html.escape(text)
105
106# GET with no extra headers
107def http.get(url: String) -> IO(Client.Response):
108 Client.client.get(url, [])
109
110# GET with headers
111def http.get_with(url: String, headers: List<&2, Http.Header>)
112 -> IO(Client.Response):
113 Client.client.get(url, headers)
114
115# GET with headers and a timeout in milliseconds that bounds connecting,
116# sending and receiving; with `follow`, redirects are followed (GET only, at
117# most 10 hops, see src/redirect.bend)
118def http.get_opts(
119 url: String,
120 headers: List<&2, Http.Header>,
121 timeout_ms: U32,
122 follow: Bool
123) -> IO(Client.Response):
124 Client.client.get.opts(url, headers, timeout_ms, follow)
125
126# a request with any method, headers, a body and a timeout in milliseconds
127# that bounds connecting, sending and receiving; with `follow`, a GET follows
128# redirects (at most 10 hops, see src/redirect.bend) and every other method
129# answers a redirect as its response
130def http.request_opts(
131 method: String,
132 url: String,
133 headers: List<&2, Http.Header>,
134 body: Body.Body,
135 timeout_ms: U32,
136 follow: Bool
137) -> IO(Client.Response):
138 Client.client.request.opts(method, url, headers, Body.body.payload(body),
139 timeout_ms, follow)
140
141# POST with headers, a body and a timeout in milliseconds; redirects are not
142# followed
143def http.post_opts(
144 url: String,
145 headers: List<&2, Http.Header>,
146 body: Body.Body,
147 timeout_ms: U32
148) -> IO(Client.Response):
149 http.request_opts("POST", url, headers, body, timeout_ms, False{})
150
151# PUT with headers, a body and a timeout in milliseconds; redirects are not
152# followed
153def http.put_opts(
154 url: String,
155 headers: List<&2, Http.Header>,
156 body: Body.Body,
157 timeout_ms: U32
158) -> IO(Client.Response):
159 http.request_opts("PUT", url, headers, body, timeout_ms, False{})
160
161# DELETE with headers, a body and a timeout in milliseconds; redirects are
162# not followed
163def http.delete_opts(
164 url: String,
165 headers: List<&2, Http.Header>,
166 body: Body.Body,
167 timeout_ms: U32
168) -> IO(Client.Response):
169 http.request_opts("DELETE", url, headers, body, timeout_ms, False{})
170
171# whether a response is an exchange that ran out of time
172def http.timed_out(r: Client.Response) -> Bool:
173 Client.client.timed_out(r)
174
175# HEAD with no extra headers
176def http.head(url: String) -> IO(Client.Response):
177 Client.client.head(url, [])
178
179# HEAD with headers
180def http.head_with(url: String, headers: List<&2, Http.Header>)
181 -> IO(Client.Response):
182 Client.client.head(url, headers)
183
184# POST with a body and no extra headers
185def http.post(url: String, body: Body.Body) -> IO(Client.Response):
186 Client.client.post(url, [], body)
187
188# POST with headers and a body
189def http.post_with(
190 url: String,
191 headers: List<&2, Http.Header>,
192 body: Body.Body
193) -> IO(Client.Response):
194 Client.client.post(url, headers, body)
195
196# PUT with a body and no extra headers
197def http.put(url: String, body: Body.Body) -> IO(Client.Response):
198 Client.client.put(url, [], body)
199
200# PUT with headers and a body
201def http.put_with(url: String, headers: List<&2, Http.Header>, body: Body.Body)
202 -> IO(Client.Response):
203 Client.client.put(url, headers, body)
204
205# DELETE with a body and no extra headers
206def http.delete(url: String, body: Body.Body) -> IO(Client.Response):
207 Client.client.delete(url, [], body)
208
209# DELETE with headers and a body
210def http.delete_with(
211 url: String,
212 headers: List<&2, Http.Header>,
213 body: Body.Body
214) -> IO(Client.Response):
215 Client.client.delete(url, headers, body)
216
217# OPTIONS with no extra headers
218def http.options(url: String) -> IO(Client.Response):
219 Client.client.options(url, [])
220
221# OPTIONS with headers
222def http.options_with(url: String, headers: List<&2, Http.Header>)
223 -> IO(Client.Response):
224 Client.client.options(url, headers)
225
226# listen on host:port and serve up to `limit` connections
227def http.serve(
228 ~handle: Http.Request -> Http.Reply,
229 host: String,
230 port: U32,
231 limit: U32
232) -> IO(Unit):
233 Server.server.serve(handle, host, port, limit)
234
235# listen on host:port and serve up to `limit` connections with a handler that
236# does IO; a failed connection is logged and the next one is served
237def http.serve_io(
238 ~handle: Http.Request -> IO(Http.Reply),
239 host: String,
240 port: U32,
241 limit: U32
242) -> IO(Unit):
243 Server.server.serve_io(handle, host, port, limit)
244
245# listen on host:port and serve with an IO handler until the process stops
246@unsafe def http.serve_forever(
247 ~handle: Http.Request -> IO(Http.Reply),
248 host: String,
249 port: U32
250) -> IO(Unit):
251 Server.server.serve_forever(handle, host, port)
252
253# listen on host:port, serve one connection, then stop
254def http.serve_once(
255 ~handle: Http.Request -> Http.Reply,
256 host: String,
257 port: U32
258) -> IO(Unit):
259 Server.server.once(handle, host, port)