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). 10 import Base 11 import bend-kit-wire@0.4.6.1/wire.bend as W 12 import bend-kit-bytes@0.3.2.0/bytes.bend as Bytes 13 import ./http.bend as Http 14 import ./cors.bend as Cors 15 import ./step.bend as Step 16 import ./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) 20 type 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) 27 type 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 34 def server.max() -> U32: 35 65536 36 37 # the most receives one request takes 38 def server.reads() -> Nat: 39 65536n 40 41 # how long one accept waits before it is asked again, in ms 42 def server.wait() -> U32: 43 3600000 44 45 # how many idle accepts in a row a limited serve allows before it stops 46 def server.idles() -> Nat: 47 1000000n 48 49 # how long a failed accept waits before the next, so EMFILE does not spin 50 def 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" 54 def 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 58 def server.accept.un(r: Listener & Result<&1, &1, U32 & String, Socket>) 59 -> IO(Listener & Step.Accept): 60 (lst, res) = r 61 IO.pure(Listener & Step.Accept, (lst, Step.step.accepted(Socket, res))) 62 63 def server.accept(listener: Listener) -> IO(Listener & Step.Accept): 64 IO.bind(Listener & Result<&1, &1, U32 & String, Socket>, 65 Listener & Step.Accept, 66 W.accept.deadline(listener, server.wait()), server.accept.un) 67 68 # a lost connection logs its line and closes 69 def 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 76 def server.read.done(sock: Socket, ps: List<&2, String>) 77 -> IO(Socket & Step.Got): 78 IO.pure(Socket & Step.Got, 79 (sock, Step.Kept{Http.ask(Spec.wire.join(ps))})) 80 81 # what the request needs, kept once its head has ended 82 def 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 91 def 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 99 def 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)) 103 -> IO(Socket & Step.Got): 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? 111 def 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)) 115 -> IO(Socket & Step.Got): 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 120 def 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)) 124 -> IO(Socket & Step.Got): 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 134 def 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)) 138 -> IO(Socket & Step.Got): 139 (sock, res) = r 140 match res: 141 case Fail{(code, why)}: 142 IO.pure(Socket & Step.Got, 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 149 def server.read.go(fuel: Nat, sock: Socket, +ps: List<&2, String>, +have: Nat, 150 +need: Maybe<&2, Nat>) -> IO(Socket & Step.Got): 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, 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) 162 def server.read(sock: Socket) -> IO(Socket & Step.Got): 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 168 def server.sent.un(r: Socket & Result<&1, &1, U32 & String, Unit>) 169 -> IO(Socket & Step.Got): 170 (sock, res) = r 171 IO.pure(Socket & Step.Got, (sock, Step.step.got(Unit, "write", res))) 172 173 # send octets 174 def server.send(sock: Socket, b: Bytes.Bytes) -> IO(Socket & Step.Got): 175 Bytes.Bytes{+len, buf} = b 176 IO.bind(Socket & Result<&1, &1, U32 & String, Unit>, Socket & Step.Got, 177 W.send.words(sock, len, buf), server.sent.un) 178 179 # send text as its UTF-8 octets 180 def server.send.text(sock: Socket, text: String) -> IO(Socket & Step.Got): 181 server.send(sock, Bytes.from_string(Spec.wire.payload(text))) 182 183 # after the last send: close, logging first when it failed 184 def server.close(r: Socket & Step.Got) -> 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 193 def server.then(r: Socket & Step.Got, 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 203 def server.answer(sock: Socket, method: String, reply: Http.Reply) -> IO(Unit): 204 IO.bind(Socket & Step.Got, 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) 210 def 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, 216 server.send.text(sock, Http.respond.head(status, headers, 0n)), 217 server.close) 218 case False{}: 219 IO.bind(Socket & Step.Got, 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 225 def server.chunks() -> Nat: 226 U32.to_nat(4294967295) 227 228 # one chunk the channel gave: framed and sent, or skipped when empty 229 def 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, 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 241 def 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 247 def 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, 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 257 def 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, 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 268 def 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) 277 def 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, 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 293 def 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) 301 def 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 311 def server.conn.got(~C: Data, ~handle: C -> Req -> IO(Answer), +ctx: C, 312 r: Socket & Step.Got) -> 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 323 def server.conn(~C: Data, ~handle: C -> Req -> IO(Answer), +ctx: C, 324 sock: Socket) -> IO(Unit): 325 IO.bind(Socket & Step.Got, Unit, server.read(sock), 326 r => server.conn.got(~C, handle, ctx, r)) 327 328 # --- the accept loop --- 329 330 # give a slot back 331 def 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 340 def 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 349 def 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 358 def 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 366 def 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 374 def 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) 385 def 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, 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 403 def 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, 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) 423 def 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) 429 def 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: 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 439 def 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 444 def 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, 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: 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 474 def 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 479 def 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 487 def 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 492 def 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 498 def 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 503 def 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 508 def 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 521 def 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 530 def 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) 535 def 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 545 def 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