src/server.bend

emerging-ezhttp@0.20.0.0 · 551 lines · 0 definitions

1# ezhttp/src/server: listen, accept, and serve each connection on its own
2# fork: parse one request, write one response (Connection: close). Sockets
3# are Base's (listen) and bend-kit-wire's (accept with a deadline, recv,
4# send.words), whose effects park on the event loop, so connections overlap.
5# A `_with` serve hands the handler a context value and the request as text
6# with its body's octets, and takes a reply as text, packed octets or a
7# stream of octet chunks; the plain serves are those with a text handler.
8# Every decision after an effect is step.bend's or http.bend's (SPEC.md
9# HTTP-SRV); this module runs the effects in order (HTTP-TRUST-2).
10import Base
11import bend-kit-wire@0.4.6.1/wire.bend as W
12import bend-kit-bytes@0.3.2.0/bytes.bend as Bytes
13import emerging-ezhttp@0.20.0.0/src/http.bend as Http
14import emerging-ezhttp@0.20.0.0/src/cors.bend as Cors
15import emerging-ezhttp@0.20.0.0/src/step.bend as Step
16import emerging-ezhttp@0.20.0.0/src/wirespec.bend as Spec
17
18# what a `_with` handler gets: the request as text (step.text) and its
19# body's octets as they arrived (step.octets)
20type Req is Type:  # noqa: L001 it holds Bytes; step.text and step.octets fill it (HTTP-SRV-9)
21  Req{method: String, target: String, headers: List<&2, Http.Header>,
22    body: String, bytes: Bytes.Bytes}
23
24# what a `_with` handler answers: a text body (written as UTF-8), packed
25# octets written unchanged, or a stream of octet chunks from a channel,
26# written chunked until the channel is closed (SPEC.md HTTP-SRV-6, 7)
27type Answer is Type:  # noqa: L001 it holds Bytes and a channel; HTTP-SRV-6 to 8 cover its writing
28  TextReply{status: U32, headers: List<&2, Http.Header>, body: String}
29  OctetsReply{status: U32, headers: List<&2, Http.Header>, bytes: Bytes.Bytes}
30  StreamReply{status: U32, headers: List<&2, Http.Header>,
31    chunks: Chan(Bytes.Bytes)}
32
33# the most octets one receive asks for
34def server.max() -> U32:
35  65536
36
37# the most receives one request takes
38def server.reads() -> Nat:
39  65536n
40
41# how long one accept waits before it is asked again, in ms
42def server.wait() -> U32:
43  3600000
44
45# how many idle accepts in a row a limited serve allows before it stops
46def server.idles() -> Nat:
47  1000000n
48
49# how long a failed accept waits before the next, so EMFILE does not spin
50def server.pause() -> U32:
51  100
52
53# listen on a host address and port, e.g. "127.0.0.1" or "0.0.0.0"
54def server.listen(host: String, port: U32) -> IO(Listener):
55  IO.try(Listener, TCP.listen(host, port))
56
57# accept one connection, waiting at most server.wait() ms
58def server.accept.un(r: Listener & Result<&1, &1, U32 & String, Socket>)
59  -> IO(Listener & Step.Accept<Socket>):
60  (lst, res) = r
61  IO.pure(Listener & Step.Accept<Socket>, (lst, Step.step.accepted(Socket, res)))
62
63def server.accept(listener: Listener) -> IO(Listener & Step.Accept<Socket>):
64  IO.bind(Listener & Result<&1, &1, U32 & String, Socket>,
65    Listener & Step.Accept<Socket>,
66    W.accept.deadline(listener, server.wait()), server.accept.un)
67
68# a lost connection logs its line and closes
69def server.lose(sock: Socket, line: String) -> IO(Unit):
70  IO.bind(Unit, Unit, IO.print_err(line), _u => Socket.close(sock))
71
72# --- reading a request ---
73
74# the request once it has all arrived (or the peer closed, or the receives
75# ran out): its pieces put together and parsed
76def server.read.done(sock: Socket, ps: List<&2, String>)
77  -> IO(Socket & Step.Got<Http.Request>):
78  IO.pure(Socket & Step.Got<Http.Request>,
79    (sock, Step.Kept{Http.ask(Spec.wire.join(ps))}))
80
81# what the request needs, kept once its head has ended
82def server.read.needs(m: Maybe<&2, Nat>, ps: List<&2, String>)
83  -> Maybe<&2, Nat>:
84  match m:
85    case None{}:
86      Step.step.need(Spec.wire.join(ps))
87    case Some{n}:
88      Some{n}
89
90# whether as much has arrived as the request needs
91def server.read.whole(m: Maybe<&2, Nat>, +have: Nat) -> Bool:
92  match m:
93    case None{}:
94      False{}
95    case Some{n}:
96      Bool.not(Nat.is_lt(have, n))
97
98# octets that arrived: stop when the request is whole, else read again
99def server.read.more(sock: Socket, +ps: List<&2, String>, +have: Nat,
100  +need: Maybe<&2, Nat>, whole: Bool,
101  rest: Socket -> List<&2, String> -> Nat -> Maybe<&2, Nat>
102    -> IO(Socket & Step.Got<Http.Request>))
103  -> IO(Socket & Step.Got<Http.Request>):
104  match whole:
105    case True{}:
106      server.read.done(sock, ps)
107    case False{}:
108      rest(sock)(ps)(have)(need)
109
110# octets added to the pieces: is the request now whole?
111def server.read.add(sock: Socket, +ps: List<&2, String>, +have: Nat,
112  need: Maybe<&2, Nat>,
113  rest: Socket -> List<&2, String> -> Nat -> Maybe<&2, Nat>
114    -> IO(Socket & Step.Got<Http.Request>))
115  -> IO(Socket & Step.Got<Http.Request>):
116  +need2 = server.read.needs(need, ps)
117  server.read.more(sock, ps, have, need2, server.read.whole(need2, have), rest)
118
119# octets that arrived, or none once the peer has closed
120def server.read.text(sock: Socket, +ps: List<&2, String>, +have: Nat,
121  need: Maybe<&2, Nat>, +text: String, closed: Bool,
122  rest: Socket -> List<&2, String> -> Nat -> Maybe<&2, Nat>
123    -> IO(Socket & Step.Got<Http.Request>))
124  -> IO(Socket & Step.Got<Http.Request>):
125  match closed:
126    case True{}:
127      server.read.done(sock, ps)
128    case False{}:
129      server.read.add(sock, text <> ps, Nat.add(have, String.length(text)),
130        need, rest)
131
132# a receive's answer: a failure loses the connection, no octets is the peer
133# closing, octets go onto the pieces
134def server.read.got(+ps: List<&2, String>, +have: Nat, need: Maybe<&2, Nat>,
135  r: Socket & Result<&1, &1, U32 & String, String>,
136  rest: Socket -> List<&2, String> -> Nat -> Maybe<&2, Nat>
137    -> IO(Socket & Step.Got<Http.Request>))
138  -> IO(Socket & Step.Got<Http.Request>):
139  (sock, res) = r
140  match res:
141    case Fail{(code, why)}:
142      IO.pure(Socket & Step.Got<Http.Request>,
143        (sock, Step.Lost{Step.step.line("read", why)}))
144    case Done{+text}:
145      server.read.text(sock, ps, have, need, text, String.is_empty(text), rest)
146
147# receive until the head has ended and its Content-Length has arrived, the
148# peer closes, or the receives run out
149def server.read.go(fuel: Nat, sock: Socket, +ps: List<&2, String>, +have: Nat,
150  +need: Maybe<&2, Nat>) -> IO(Socket & Step.Got<Http.Request>):
151  match fuel:
152    case 0n:
153      server.read.done(sock, ps)
154    case 1n+f:
155      IO.bind(Socket & Result<&1, &1, U32 & String, String>,
156        Socket & Step.Got<Http.Request>,
157        W.recv(sock, server.max(), 0),
158        r => server.read.got(ps, have, need, r,
159          s => p => h => m => server.read.go(f, s, p, h, m)))
160
161# receive then parse an HTTP request (RFC 9112 §3)
162def server.read(sock: Socket) -> IO(Socket & Step.Got<Http.Request>):
163  server.read.go(server.reads(), sock, [], 0n, None{})
164
165# --- writing a reply ---
166
167# after a send: the socket, or the line of a lost connection
168def server.sent.un(r: Socket & Result<&1, &1, U32 & String, Unit>)
169  -> IO(Socket & Step.Got<Unit>):
170  (sock, res) = r
171  IO.pure(Socket & Step.Got<Unit>, (sock, Step.step.got(Unit, "write", res)))
172
173# send octets
174def server.send(sock: Socket, b: Bytes.Bytes) -> IO(Socket & Step.Got<Unit>):
175  Bytes.Bytes{+len, buf} = b
176  IO.bind(Socket & Result<&1, &1, U32 & String, Unit>, Socket & Step.Got<Unit>,
177    W.send.words(sock, len, buf), server.sent.un)
178
179# send text as its UTF-8 octets
180def server.send.text(sock: Socket, text: String) -> IO(Socket & Step.Got<Unit>):
181  server.send(sock, Bytes.from_string(Spec.wire.payload(text)))
182
183# after the last send: close, logging first when it failed
184def server.close(r: Socket & Step.Got<Unit>) -> IO(Unit):
185  (sock, got) = r
186  match got:
187    case Step.Lost{line}:
188      server.lose(sock, line)
189    case Step.Kept{_u}:
190      Socket.close(sock)
191
192# after a send in the middle: go on, or lose the connection
193def server.then(r: Socket & Step.Got<Unit>, more: Socket -> IO(Unit))
194  -> IO(Unit):
195  (sock, got) = r
196  match got:
197    case Step.Lost{line}:
198      server.lose(sock, line)
199    case Step.Kept{_u}:
200      more(sock)
201
202# a text reply shaped for the request's method (HTTP-SRV-3), then close
203def server.answer(sock: Socket, method: String, reply: Http.Reply) -> IO(Unit):
204  IO.bind(Socket & Step.Got<Unit>, Unit,
205    server.send.text(sock, Http.respond.of(Http.reply.for(method, reply))),
206    server.close)
207
208# an octets reply once its length is known: its head, then its octets
209# unless it is bare (HTTP-SRV-6, 8)
210def server.octets.go(sock: Socket, +status: U32, headers: List<&2, Http.Header>,
211  r: Bytes.Bytes & U32, bare: Bool) -> IO(Unit):
212  (b, +n) = r
213  match bare:
214    case True{}:
215      IO.bind(Socket & Step.Got<Unit>, Unit,
216        server.send.text(sock, Http.respond.head(status, headers, 0n)),
217        server.close)
218    case False{}:
219      IO.bind(Socket & Step.Got<Unit>, Unit,
220        server.send(sock, Bytes.append(Bytes.from_string(
221          Http.respond.head(status, headers, U32.to_nat(n))), b)),
222        server.close)
223
224# the most chunks one streamed reply writes
225def server.chunks() -> Nat:
226  U32.to_nat(4294967295)
227
228# one chunk the channel gave: framed and sent, or skipped when empty
229def server.chunk.put(sock: Socket, c: Http.Chunk, b: Bytes.Bytes,
230  more: Socket -> IO(Unit)) -> IO(Unit):
231  match c:
232    case Http.NoChunk{}:
233      more(sock)
234    case Http.Chunk{line}:
235      IO.bind(Socket & Step.Got<Unit>, Unit,
236        server.send(sock, Bytes.append(Bytes.append(Bytes.from_string(line), b),
237          Bytes.from_string("\r\n"))),
238        r => server.then(r, more))
239
240# a chunk with its length known
241def server.chunk.sized(sock: Socket, r: Bytes.Bytes & U32,
242  more: Socket -> IO(Unit)) -> IO(Unit):
243  (b, +n) = r
244  server.chunk.put(sock, Http.chunk.frame(n), b, more)
245
246# what the channel gave: a chunk, or its close, which ends the body
247def server.chunk.got(sock: Socket, m: Maybe<&1, Bytes.Bytes>,
248  more: Socket -> IO(Unit)) -> IO(Unit):
249  match m:
250    case None{}:
251      IO.bind(Socket & Step.Got<Unit>, Unit,
252        server.send.text(sock, Http.chunk.end()), server.close)
253    case Some{b}:
254      server.chunk.sized(sock, Bytes.length(b), more)
255
256# every chunk the channel gives, until it is closed
257def server.stream.go(fuel: Nat, sock: Socket, +ch: Chan(Bytes.Bytes))
258  -> IO(Unit):
259  match fuel:
260    case 0n:
261      IO.bind(Socket & Step.Got<Unit>, Unit,
262        server.send.text(sock, Http.chunk.end()), server.close)
263    case 1n+f:
264      IO.bind(Maybe<&1, Bytes.Bytes>, Unit, Chan.recv(Bytes.Bytes, ch),
265        m => server.chunk.got(sock, m, s => server.stream.go(f, s, ch)))
266
267# a streamed reply after its head: its chunks, or nothing when it is bare
268def server.stream.body(sock: Socket, +ch: Chan(Bytes.Bytes), bare: Bool)
269  -> IO(Unit):
270  match bare:
271    case True{}:
272      IO.bind(Unit, Unit, Chan.close(Bytes.Bytes, ch), _u => Socket.close(sock))
273    case False{}:
274      server.stream.go(server.chunks(), sock, ch)
275
276# a handler's answer, shaped for the request's method (HTTP-SRV-3, 6 to 8)
277def server.reply(sock: Socket, +method: String, a: Answer) -> IO(Unit):
278  match a:
279    case TextReply{status, headers, body}:
280      server.answer(sock, method, Http.Reply{status, headers, body})
281    case OctetsReply{+status, headers, bytes}:
282      server.octets.go(sock, status, headers, Bytes.length(bytes),
283        Step.step.bare(method, status))
284    case StreamReply{+status, +headers, +chunks}:
285      IO.bind(Socket & Step.Got<Unit>, Unit,
286        server.send.text(sock, Http.respond.stream(status, headers)),
287        r => server.then(r,
288          s => server.stream.body(s, chunks, Step.step.bare(method, status))))
289
290# --- one connection ---
291
292# the handler's request: the request as text and its body's octets
293def server.req(req: Http.Request, +octets: String) -> Req:
294  match req:
295    case Http.Bad{why}:
296      Req{"", "", [], why, Bytes.from_string(octets)}
297    case Http.Request{method, target, headers, body}:
298      Req{method, target, headers, body, Bytes.from_string(octets)}
299
300# answer now, or call the handler: step.bend decides which (HTTP-SRV-1, 2, 9)
301def server.step(~C: Data, ~handle: C -> Req -> IO(Answer), +ctx: C,
302  sock: Socket, +method: String, +octets: String, step: Step.Step) -> IO(Unit):
303  match step:
304    case Step.Answer{reply}:
305      server.answer(sock, method, reply)
306    case Step.Call{req}:
307      IO.bind(Answer, Unit, handle(ctx)(server.req(req, octets)),
308        a => server.reply(sock, method, a))
309
310# after the read: a lost connection, or a request to step
311def server.conn.got(~C: Data, ~handle: C -> Req -> IO(Answer), +ctx: C,
312  r: Socket & Step.Got<Http.Request>) -> IO(Unit):
313  (sock, got) = r
314  match got:
315    case Step.Lost{line}:
316      server.lose(sock, line)
317    case Step.Kept{+raw}:
318      +req = Step.step.text(raw)
319      server.step(~C, handle, ctx, sock, Http.method.of(req),
320        Step.step.octets(raw), Step.step.of(req))
321
322# one connection: read, decide, maybe handle, write, close
323def server.conn(~C: Data, ~handle: C -> Req -> IO(Answer), +ctx: C,
324  sock: Socket) -> IO(Unit):
325  IO.bind(Socket & Step.Got<Http.Request>, Unit, server.read(sock),
326    r => server.conn.got(~C, handle, ctx, r))
327
328# --- the accept loop ---
329
330# give a slot back
331def server.slot.give(+slots: Chan(Unit), limited: Bool) -> IO(Unit):
332  match limited:
333    case True{}:
334      IO.bind(Maybe<&1, Unit>, Unit, Chan.recv(Unit, slots),
335        _m => IO.pure(Unit, Unit{}))
336    case False{}:
337      IO.pure(Unit, Unit{})
338
339# take a slot, waiting while every one is taken
340def server.slot.take(+slots: Chan(Unit), limited: Bool) -> IO(Unit):
341  match limited:
342    case True{}:
343      IO.bind(Result<&1, &1, Unit, Unit>, Unit, Chan.send(Unit, slots, Unit{}),
344        _r => IO.pure(Unit, Unit{}))
345    case False{}:
346      IO.pure(Unit, Unit{})
347
348# say a fork is done, when the serve waits for its forks
349def server.done(+done: Chan(Unit), tracked: Bool) -> IO(Unit):
350  match tracked:
351    case True{}:
352      IO.bind(Result<&1, &1, Unit, Unit>, Unit, Chan.send(Unit, done, Unit{}),
353        _r => IO.pure(Unit, Unit{}))
354    case False{}:
355      IO.pure(Unit, Unit{})
356
357# a connection's fork: serve it, give its slot back, and say it is done
358def server.fork.body(~C: Data, ~handle: C -> Req -> IO(Answer), +ctx: C,
359  sock: Socket, +slots: Chan(Unit), +limited: Bool, +done: Chan(Unit),
360  +tracked: Bool) -> IO(Unit):
361  IO.bind(Unit, Unit, server.conn(~C, handle, ctx, sock),
362    _u => IO.bind(Unit, Unit, server.slot.give(slots, limited),
363      _v => server.done(done, tracked)))
364
365# serve an accepted connection on its own fork, a slot taken first
366def server.fork(~C: Data, ~handle: C -> Req -> IO(Answer), +ctx: C,
367  sock: Socket, +slots: Chan(Unit), +limited: Bool, +done: Chan(Unit),
368  +tracked: Bool) -> IO(Unit):
369  IO.bind(Unit, Unit, server.slot.take(slots, limited),
370    _u => IO.spawn(Unit,
371      server.fork.body(~C, handle, ctx, sock, slots, limited, done, tracked)))
372
373# wait for n forks to say they are done
374def server.join(n: Nat, +done: Chan(Unit)) -> IO(Unit):
375  match n:
376    case 0n:
377      IO.pure(Unit, Unit{})
378    case 1n+p:
379      IO.bind(Maybe<&1, Unit>, Unit, Chan.recv(Unit, done),
380        _m => server.join(p, done))
381
382# what an accept brought, for the loop: `took` serves a connection and goes
383# on with one connection fewer, `idle` accepts again, `missed` logs, pauses
384# and goes on with one fewer (a failed accept counts toward the limit)
385def server.loop.got(~C: Data, ~handle: C -> Req -> IO(Answer), +ctx: C,
386  +slots: Chan(Unit), +limited: Bool, +done: Chan(Unit), +tracked: Bool,
387  r: Listener & Step.Accept<Socket>, took: Listener -> IO(Unit),
388  idle: Listener -> IO(Unit), missed: Listener -> IO(Unit)) -> IO(Unit):
389  (lst, got) = r
390  match got:
391    case Step.Took{sock}:
392      IO.bind(Unit, Unit,
393        server.fork(~C, handle, ctx, sock, slots, limited, done, tracked),
394        _u => took(lst))
395    case Step.Idle{}:
396      idle(lst)
397    case Step.Missed{line}:
398      IO.bind(Unit, Unit, IO.print_err(line), _u =>
399        IO.bind(Unit, Unit, IO.sleep(server.pause()), _v => missed(lst)))
400
401# the limited accept loop: `fuel` connections left, `idles` idle accepts in
402# a row left, `forks` forked so far
403def server.loop.go(~C: Data, ~handle: C -> Req -> IO(Answer), +ctx: C,
404  fuel: Nat, idles: Nat, +forks: Nat, +slots: Chan(Unit), +limited: Bool,
405  +done: Chan(Unit), listener: Listener) -> IO(Unit):
406  match fuel idles:
407    case 0n _:
408      IO.bind(Unit, Unit, Listener.close(listener), _u => server.join(forks, done))
409    case 1n+_f 0n:
410      IO.bind(Unit, Unit, Listener.close(listener), _u => server.join(forks, done))
411    case 1n+f0 1n+i:
412      +f = f0
413      IO.bind(Listener & Step.Accept<Socket>, Unit, server.accept(listener),
414        r => server.loop.got(~C, handle, ctx, slots, limited, done, True{}, r,
415          l => server.loop.go(~C, handle, ctx, f, server.idles(), 1n+forks, slots,
416            limited, done, l),
417          l => server.loop.go(~C, handle, ctx, 1n+f, i, forks, slots, limited,
418            done, l),
419          l => server.loop.go(~C, handle, ctx, f, server.idles(), forks, slots,
420            limited, done, l)))
421
422# the slots a concurrency limit allows (max 0: no limit)
423def server.slots(+max: U32) -> IO(Chan(Unit)):
424  Chan.new(Unit, Bool.pick(U32, U32.is_eq(max, 0), 1, max))
425
426# listen and serve up to `limit` connections, each on its own fork, at most
427# `max` at once (0: no limit), with a context value for the handler; returns
428# once every fork is done (SPEC.md HTTP-TRUST-2)
429def server.serve_io_with(~C: Data, ~handle: C -> Req -> IO(Answer), +ctx: C,
430  host: String, port: U32, limit: U32, +max: U32) -> IO(Unit):
431  do IO<Unit>:
432    lst : Listener <- server.listen(host, port)
433    +slots : Chan(Unit) <- server.slots(max)
434    +done : Chan(Unit) <- Chan.new(Unit, 1024)
435    server.loop.go(~C, handle, ctx, U32.to_nat(limit), server.idles(), 0n, slots,
436      Bool.not(U32.is_eq(max, 0)), done, lst)
437
438# a pure handler as an IO one
439def server.lift.with(~C: Data, ~handle: C -> Req -> Answer, +ctx: C, req: Req)
440  -> IO(Answer):
441  IO.pure(Answer, handle(ctx)(req))
442
443# listen and serve with a pure handler and a context value
444def server.serve_with(~C: Data, ~handle: C -> Req -> Answer, +ctx: C,
445  host: String, port: U32, limit: U32, max: U32) -> IO(Unit):
446  server.serve_io_with(~C, ~(c => r => server.lift.with(~C, handle, c, r)), ctx,
447    host, port, limit, max)
448
449# the accept loop with no limit: it runs until the process is stopped
450@unsafe def server.forever.go(~C: Data, ~handle: C -> Req -> IO(Answer),
451  +ctx: C, +slots: Chan(Unit), +limited: Bool, +done: Chan(Unit),
452  listener: Listener) -> IO(Unit):
453  IO.bind(Listener & Step.Accept<Socket>, Unit, server.accept(listener),
454    r => server.loop.got(~C, handle, ctx, slots, limited, done, False{}, r,
455      l => server.forever.go(~C, handle, ctx, slots, limited, done, l),
456      l => server.forever.go(~C, handle, ctx, slots, limited, done, l),
457      l => server.forever.go(~C, handle, ctx, slots, limited, done, l)))
458
459# listen and serve with an IO handler and a context value until the process
460# is stopped, at most `max` connections at once (0: no limit)
461@unsafe def server.serve_forever_with(~C: Data,
462  ~handle: C -> Req -> IO(Answer), +ctx: C, host: String, port: U32, +max: U32)
463  -> IO(Unit):
464  do IO<Unit>:
465    lst : Listener <- server.listen(host, port)
466    +slots : Chan(Unit) <- server.slots(max)
467    +done : Chan(Unit) <- Chan.new(Unit, 1)
468    server.forever.go(~C, handle, ctx, slots, Bool.not(U32.is_eq(max, 0)), done,
469      lst)
470
471# --- the text handlers (no context) ---
472
473# a `_with` request as the text request a plain handler takes
474def server.plain(r: Req) -> Http.Request:
475  Req{method, target, headers, body, bytes} = r
476  Http.Request{method, target, headers, body}
477
478# a text reply as an answer
479def server.text(reply: Http.Reply) -> IO(Answer):
480  match reply:
481    case Http.Torn{why}:
482      IO.pure(Answer, TextReply{500, [], why})
483    case Http.Reply{status, headers, body}:
484      IO.pure(Answer, TextReply{status, headers, body})
485
486# a plain IO handler as a `_with` one
487def server.wrap.io(~handle: Http.Request -> IO(Http.Reply), r: Req)
488  -> IO(Answer):
489  IO.bind(Http.Reply, Answer, handle(server.plain(r)), server.text)
490
491# listen and serve up to `limit` connections with an IO handler
492def server.serve_io(~handle: Http.Request -> IO(Http.Reply), host: String,
493  port: U32, limit: U32) -> IO(Unit):
494  server.serve_io_with(~Unit, ~(_u => r => server.wrap.io(handle, r)), Unit{},
495    host, port, limit, 0)
496
497# a pure handler as an IO one
498def server.lift(~handle: Http.Request -> Http.Reply, req: Http.Request)
499  -> IO(Http.Reply):
500  IO.pure(Http.Reply, handle(req))
501
502# listen and serve up to `limit` connections
503def server.serve(~handle: Http.Request -> Http.Reply, host: String, port: U32,
504  limit: U32) -> IO(Unit):
505  server.serve_io(~(req => server.lift(handle, req)), host, port, limit)
506
507# serve exactly one connection then stop
508def server.once(~handle: Http.Request -> Http.Reply, host: String, port: U32)
509  -> IO(Unit):
510  server.serve(handle, host, port, 1)
511
512# listen and serve with an IO handler until the process is stopped
513@unsafe def server.serve_forever(~handle: Http.Request -> IO(Http.Reply),
514  host: String, port: U32) -> IO(Unit):
515  server.serve_forever_with(~Unit, ~(_u => r => server.wrap.io(handle, r)), Unit{},
516    host, port, 0)
517
518# --- CORS (cors.bend), for pure handlers ---
519
520# turn a bad request into 400; otherwise call the handler
521def server.dispatch(~handle: Http.Request -> Http.Reply, req: Http.Request)
522  -> Http.Reply:
523  match req:
524    case Http.Bad{why}:
525      Http.Reply{400, [], why}
526    case Http.Request{method, target, headers, body}:
527      handle(Http.Request{method, target, headers, body})
528
529# apply HEAD body stripping once the method token is known
530def server.reply.built(~handle: Http.Request -> Http.Reply, method: String,
531  req: Http.Request) -> Http.Reply:
532  Http.reply.for(method, server.dispatch(handle, req))
533
534# HEAD responses are written with an empty body (RFC 9110 §9.3.2)
535def server.reply.text(~handle: Http.Request -> Http.Reply, req: Http.Request)
536  -> Http.Reply:
537  match req:
538    case Http.Bad{why}:
539      server.dispatch(handle, Http.Bad{why})
540    case Http.Request{+method, target, headers, body}:
541      server.reply.built(handle, method,
542        Http.Request{method, target, headers, body})
543
544# CORS preflight or simple headers, after HEAD bodies are dropped
545def server.cors_reply(
546  ~handle: Http.Request -> Http.Reply,
547  cfg: Cors.Cfg,
548  +req: Http.Request
549) -> Http.Reply:
550  Cors.cors.on(cfg, req, server.reply.text(handle, req))
551