http_server/server.bend source
http_server/server.bend on the hub · documented module
import Baseimport ../wire/wire.bend as Wimport ../http1/http1.bend as H1import ../http1/bytes.bend as Bimport ../http/request.bend as Reqimport ../http/response.bend as Resimport ../http/status.bend as S# grounds-http-server: serve(handler, config) answers HTTP/1.1 requests on a# port. Each connection runs on its own (IO.spawn), reads requests one after# another (keep-alive), and closes on Connection: close, on a refused# request, after max_requests, or when the client does.## Timeouts: a connection with no request under way closes after idle_ms# without a byte; once a request's first bytes come, its head and body# must all come within request_ms, else it gets 408 and the connection# closes. A client trickling a byte at a time cannot hold a connection.type Config is Data: Config{port: U32, max_head_bytes: U32, max_body_bytes: U32, max_requests: U32, idle_ms: U32, request_ms: U32}def default(port: U32) -> Config: Config{port, 16384, 1048576, 100, 5000, 10000}def RP() -> Type: Socket & Result<&1, &1, U32 & String, Maybe<&1, List<&2, U32>>>def RU() -> Type: Socket & Result<&1, &1, U32 & String, Unit>def copy(bs: B.Bytes, acc: B.Bytes) -> B.Bytes: match bs: case B.BNil{}: B.rev(acc) case B.BCon{b, t}: copy(t, B.BCon{b, acc})# a refused request's answer: its status, with the reason as the textdef refuse(st: S.Status) -> Res.Response: match st: case S.Status{+c}: Res.text(S.Status{c}, S.reason(S.Status{c}))# after a response is sent: close, or go on with the bytes after the requestlaw sent: for close: Bool for rest: B.Bytes for m: RU() for next: B.Bytes -> Nat -> Socket -> IO(Unit) IO(Unit)# a new request starts with no deadline: its first bytes set onedef sent(close, rest, m, next): match close: case True{}: (s, _) = m W.close(s) case False{}: (s, _) = m next(rest, 0n, s)def got.go(done: Bool, acc: B.Bytes, bs: List<&2, U32>, +dl: Nat, s: Socket, next: B.Bytes -> Nat -> Socket -> IO(Unit)) -> IO(Unit): match done: case True{}: W.close(s) case False{}: next(B.app(acc, B.of_list(bs)), dl, s)def empty(bs: List<&2, U32>) -> Bool: match bs: case Nil{}: True{} case _: False{}law late: for m: RU() IO(Unit)def late(m): (s, _) = m W.close(s)# the deadline passed mid-request: 408, then closedef timed_out(s: Socket) -> IO(Unit): do IO<Unit>: m : RU() <- W.wire_send(s, B.to_list(H1.write(refuse(S.request_timeout()), True{}))) late(m)# nothing came in time: an idle connection closes quietly, one mid-request# is told whydef quiet(idle: Bool, s: Socket) -> IO(Unit): match idle: case True{}: W.close(s) case False{}: timed_out(s)def polled.fold(idle: Bool, r: Result<&1, &1, U32 & String, Maybe<&1, List<&2, U32>>>, acc: B.Bytes, +dl: Nat, s: Socket, next: B.Bytes -> Nat -> Socket -> IO(Unit)) -> IO(Unit): match r: case Done{Some{+bs}}: got.go(empty(bs), acc, bs, dl, s, next) case Done{None{}}: quiet(idle, s) case Fail{_}: W.close(s)# more bytes came: go on with them, or close if the client haslaw polled: for idle: Bool for acc: B.Bytes for +dl: Nat for m: RP() for next: B.Bytes -> Nat -> Socket -> IO(Unit) IO(Unit)def polled(idle, acc, dl, m, next): (s, r) = m polled.fold(idle, r, acc, dl, s, next)# the deadline: set by the first read of a request, kept afterdef deadline(+dl: Nat, +now: Nat, +ms: U32) -> Nat: match dl: case 0n: Nat.add(now, U32.to_nat(ms)) case _: dldef wait.go(over: Bool, +dl: Nat, +now: Nat, acc: B.Bytes, s: Socket, next: B.Bytes -> Nat -> Socket -> IO(Unit)) -> IO(Unit): match over: case True{}: timed_out(s) case False{}: IO.bind(RP(), Unit, W.wire_recv_timeout(s, 65536, U32.from_nat(Nat.sub(dl, now))), m => polled(False{}, acc, dl, m, next))def wait.left(+dl: Nat, +now: Nat, acc: B.Bytes, s: Socket, next: B.Bytes -> Nat -> Socket -> IO(Unit)) -> IO(Unit): wait.go(Nat.is_le(dl, now), dl, now, acc, s, next)# read on: an idle connection waits idle_ms; a request under way waits# until its deadlinedef wait(idle: Bool, +ims: U32, +rms: U32, +dl: Nat, acc: B.Bytes, s: Socket, next: B.Bytes -> Nat -> Socket -> IO(Unit)) -> IO(Unit): match idle: case True{}: IO.bind(RP(), Unit, W.wire_recv_timeout(s, 65536, ims), m => polled(True{}, acc, 0n, m, next)) case False{}: do IO<Unit>: +now : Nat <- IO.now() wait.left(deadline(dl, now, rms), now, acc, s, next)# the last allowed request closes the connectiondef closing(+close: Bool, +left: Nat) -> Bool: match left: case 0n: True{} case _: close# one parse of what has come so far, and what to do with itdef step(~h: Req.Request -> IO(Res.Response), p: H1.Parsed, +acc: B.Bytes, s: Socket, +left: Nat, +ims: U32, +rms: U32, +dl: Nat, next: B.Bytes -> Nat -> Socket -> IO(Unit)) -> IO(Unit): match p: case H1.Parsed{req, +close, rest}: do IO<Unit>: res : Res.Response <- h(req) m : RU() <- W.wire_send(s, B.to_list(H1.write(res, closing(close, left)))) sent(closing(close, left), rest, m, next) case H1.More{}: wait(Bool.not(B.nonempty(acc)), ims, rms, dl, acc, s, next) case H1.Bad{e}: do IO<Unit>: m : RU() <- W.wire_send(s, B.to_list(H1.write(refuse(H1.status(e)), True{}))) sent(True{}, B.BNil{}, m, next)# a connection: acc is what has come and is not yet a request; fuel counts# the requests it may still servedef conn(~h: Req.Request -> IO(Res.Response), +fuel: Nat, +mh: U32, +mb: U32, +ims: U32, +rms: U32, +acc: B.Bytes, +dl: Nat, s: Socket) -> IO(Unit): match fuel: case 0n: W.close(s) case 1n+p: step(~h, H1.parse(copy(acc, B.BNil{}), mh, mb), acc, s, p, ims, rms, dl, b => d => sk => conn(~h, p, mh, mb, ims, rms, b, d, sk))def RA() -> Type: Listener & Result<&1, &1, U32 & String, Socket># an accepted connection runs on its own; the loop goes on acceptingdef accepted(~h: Req.Request -> IO(Res.Response), r: Result<&1, &1, U32 & String, Socket>, l: Listener, +mh: U32, +mb: U32, +ims: U32, +rms: U32, +mr: Nat, k: Listener -> IO(Unit)) -> IO(Unit): match r: case Done{s}: do IO<Unit>: x : Unit <- IO.spawn(Unit, conn(~h, mr, mh, mb, ims, rms, B.BNil{}, 0n, s)) k(l) case Fail{_}: k(l)# fuel bounds the connections served: a server's loop is bounded by the# outside world, which Bend's termination check sees as a countdef accept.loop(~h: Req.Request -> IO(Res.Response), +fuel: Nat, +mh: U32, +mb: U32, +ims: U32, +rms: U32, +mr: Nat, m: RA()) -> IO(Unit): match fuel: case 0n: (l, _) = m IO.pure(Unit, Unit{}) case 1n+p: (l, r) = m accepted(~h, r, l, mh, mb, ims, rms, mr, l2 => IO.bind(RA(), Unit, W.accept(l2), m2 => accept.loop(~h, p, mh, mb, ims, rms, mr, m2)))# answer requests on cfg's port with h, one connection at a time each, many# connections at oncedef serve(~h: Req.Request -> IO(Res.Response), cfg: Config) -> IO(Unit): match cfg: case Config{port, +mh, +mb, mr, +ims, +rms}: do IO<Unit>: l : Listener <- IO.try(Listener, W.listen(port)) m : RA() <- W.accept(l) accept.loop(~h, 4294967295n, mh, mb, ims, rms, U32.to_nat(mr), m)