server.bend source
server.bend on the hub · documented module
# ezhttp/server: listen, accept, parse one request, write one response.# Uses Base TCP (no TLS in v0). One exchange per connection with# Connection: close. Handler is a pure Request -> Reply function.import Baseimport ./http.bend as Httpimport ./cors.bend as Cors# default recv budget for one request messagedef server.max() -> U32: 65536# listen on a port (RFC 9112 connection establishment stays at the socket)def server.listen(port: U32) -> IO(Listener): IO.try(Listener, TCP.listen(port))# unpack accept: listener stays, socket from the Resultdef server.accept.of( lst: Listener, res: Result<&1, &1, U32 & String, Socket>) -> IO(Listener & Socket): do IO<Listener & Socket>: sock : Socket <- IO.pass(Socket, res) IO.pure(Listener & Socket, (lst, sock))def server.accept.un(pair: Listener & Result<&1, &1, U32 & String, Socket>) -> IO(Listener & Socket): (lst, res) = pair server.accept.of(lst, res)def server.accept(listener: Listener) -> IO(Listener & Socket): IO.bind( Listener & Result<&1, &1, U32 & String, Socket>, Listener & Socket, TCP.accept(listener), server.accept.un)# the short-body refusal, so a partial recv can continuedef server.read.why() -> String: "the request body stopped before its Content-Length"# whether this refusal is a body that has not arrived yetdef server.read.short(why: String) -> Bool: String.eq(why, server.read.why())# a parsed request still wants more bytesdef server.read.need(req: Http.Request) -> Bool: match req: case Http.Bad{why}: server.read.short(why) case Http.Request{method, target, headers, body}: False{}# how many extra recvs to chase a Content-Lengthdef server.read.fuel() -> Nat: 64n# parse what has arriveddef server.read.done(sock: Socket, got: String) -> IO(Socket & Http.Request): IO.pure(Socket & Http.Request, (sock, Http.ask(got)))# stop on a finished message, or ask for another recvdef server.read.next( sock: Socket, got: String, req: Http.Request, rest: Socket -> String -> IO(Socket & Http.Request), more: Bool) -> IO(Socket & Http.Request): match more: case False{}: IO.pure(Socket & Http.Request, (sock, req)) case True{}: rest(sock)(got)# append a chunk and decide whether it completes the requestdef server.read.grew( sock: Socket, +got: String, rest: Socket -> String -> IO(Socket & Http.Request)) -> IO(Socket & Http.Request): +req = Http.ask(got) server.read.next(sock, got, req, rest, server.read.need(req))# peer closed, or the chunk extends the bufferdef server.read.empty( sock: Socket, got: String, chunk: String, rest: Socket -> String -> IO(Socket & Http.Request), closed: Bool) -> IO(Socket & Http.Request): match closed: case True{}: server.read.done(sock, got) case False{}: server.read.grew(sock, got ++ chunk, rest)# one chunk of the request messagedef server.read.use( sock: Socket, got: String, +chunk: String, rest: Socket -> String -> IO(Socket & Http.Request)) -> IO(Socket & Http.Request): server.read.empty(sock, got, chunk, rest, String.eq(chunk, ""))# the bytes from one recvdef server.read.pass( sock: Socket, got: String, rest: Socket -> String -> IO(Socket & Http.Request), r: Result<&1, &1, U32 & String, String>) -> IO(Socket & Http.Request): IO.bind(String, Socket & Http.Request, IO.pass(String, r), chunk => server.read.use(sock, got, chunk, rest))# unpack recv's socket and resultdef server.read.un( got: String, rest: Socket -> String -> IO(Socket & Http.Request), pair: Socket & Result<&1, &1, U32 & String, String>) -> IO(Socket & Http.Request): (sock, r) = pair server.read.pass(sock, got, rest, r)# recv until Content-Length is met, the peer closes, or fuel runs out.# fuel is first so the recursive call is a decreasing self-call.def server.read.go(fuel: Nat, sock: Socket, got: String) -> IO(Socket & Http.Request): match fuel: case 0n: server.read.done(sock, got) case 1n+f: IO.bind( Socket & Result<&1, &1, U32 & String, String>, Socket & Http.Request, TCP.recv(sock, server.max()), pair => server.read.un(got, s => g => server.read.go(f, s, g), pair))# recv then parse an HTTP request (RFC 9112 §3)def server.read(sock: Socket) -> IO(Socket & Http.Request): server.read.go(server.read.fuel(), sock, "")# write a structured response and keep the socketdef server.write.res(sock: Socket, r: Result<&1, &1, U32 & String, Unit>) -> IO(Socket): IO.bind(Unit, Socket, IO.pass(Unit, r), _u => IO.pure(Socket, sock))def server.write.un(pair: Socket & Result<&1, &1, U32 & String, Unit>) -> IO(Socket): (s, r) = pair server.write.res(s, r)def server.write(sock: Socket, reply: Http.Reply) -> IO(Socket): IO.bind( Socket & Result<&1, &1, U32 & String, Unit>, Socket, TCP.send(sock, Http.respond.of(reply)), server.write.un)# turn a bad request into 400; otherwise call the handlerdef server.dispatch(~handle: Http.Request -> Http.Reply, req: Http.Request) -> Http.Reply: match req: case Http.Bad{why}: Http.Reply{400, [], why} case Http.Request{method, target, headers, body}: handle(Http.Request{method, target, headers, body})# apply HEAD body stripping once the method token is knowndef server.reply.built(~handle: Http.Request -> Http.Reply, method: String, req: Http.Request) -> Http.Reply: Http.reply.for(method, server.dispatch(handle, req))# HEAD responses are written with an empty body (RFC 9110 §9.3.2)def server.reply(~handle: Http.Request -> Http.Reply, req: Http.Request) -> Http.Reply: match req: case Http.Bad{why}: server.dispatch(handle, Http.Bad{why}) case Http.Request{+method, target, headers, body}: server.reply.built(handle, method, Http.Request{method, target, headers, body})# CORS preflight or simple headers, after HEAD bodies are droppeddef server.cors_reply( ~handle: Http.Request -> Http.Reply, cfg: Cors.Cfg, +req: Http.Request) -> Http.Reply: Cors.cors.on(cfg, req, server.reply(handle, req))# after read: write the handler's reply and closedef server.exchange.go( ~handle: Http.Request -> Http.Reply, sock: Socket, req: Http.Request) -> IO(Unit): IO.bind(Socket, Unit, server.write(sock, server.reply(handle, req)), Socket.close)def server.exchange.un( ~handle: Http.Request -> Http.Reply, pair: Socket & Http.Request) -> IO(Unit): (s, req) = pair server.exchange.go(handle, s, req)# one connection: read, handle, write, closedef server.exchange(~handle: Http.Request -> Http.Reply, sock: Socket) -> IO(Unit): IO.bind(Socket & Http.Request, Unit, server.read(sock), pair => server.exchange.un(handle, pair))# continue after accept: exchange, then call `again` with the listener.# `again` is a runtime closure so it can carry the fuel still left.def server.loop.cont( ~handle: Http.Request -> Http.Reply, again: Listener -> IO(Unit), pair: Listener & Socket) -> IO(Unit): (lst, sock) = pair IO.bind(Unit, Unit, server.exchange(handle, sock), _u => again(lst))# fuel-bounded accept loop (self-recursive)def server.loop.go( ~handle: Http.Request -> Http.Reply, fuel: Nat, listener: Listener) -> IO(Unit): match fuel: case 0n: IO.pure(Unit, Unit{}) case 1n+f: IO.bind(Listener & Socket, Unit, server.accept(listener), pair => server.loop.cont(handle, lst => server.loop.go(handle, f, lst), pair))# listen and serve up to `limit` connections (fuel from the limit)def server.loop( ~handle: Http.Request -> Http.Reply, listener: Listener, limit: U32) -> IO(Unit): server.loop.go(handle, U32.to_nat(limit), listener)# listen and serve up to `limit` connectionsdef server.serve( ~handle: Http.Request -> Http.Reply, port: U32, limit: U32) -> IO(Unit): IO.bind(Listener, Unit, server.listen(port), lst => server.loop(handle, lst, limit))# serve exactly one connection then stopdef server.once.cont( ~handle: Http.Request -> Http.Reply, pair: Listener & Socket) -> IO(Unit): (lst, sock) = pair IO.bind(Unit, Unit, server.exchange(handle, sock), _u => Listener.close(lst))def server.once(~handle: Http.Request -> Http.Reply, port: U32) -> IO(Unit): IO.bind(Listener, Unit, server.listen(port), lst => IO.bind(Listener & Socket, Unit, server.accept(lst), pair => server.once.cont(handle, pair)))