~/bend-docscommunity

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)))