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