import Base import ../http/method.bend as M import ../http/status.bend as S import ../http/request.bend as Req import ../http/headers.bend as H import ../http/response.bend as Res # middleware as compile-time wrappers: each takes the next handler as a # template, so a stack is one expression the compiler inlines: # # Server.serve(~Mw.logger(~Mw.request_id(~Mw.recover(~app))), cfg) # # A wrapper that needs a number takes it as a template too: a def with no # arguments, e.g. def small() -> U32: 1024, as Mw.body_limit(~small, ~app) def RS() -> Type: Result<&1, &1, String, Res.Response> # Recover # ------- def recover.go(r: RS()) -> IO(Res.Response): match r: case Done{res}: IO.pure(Res.Response, res) case Fail{why}: do IO: x : Unit <- IO.print_err("error: " ++ why) return Res.text(S.internal_error(), "Internal Server Error") # a handler that may fail: Fail{why} logs why to stderr and answers 500, # so a handler's error never reaches the client def recover(~next: Req.Request -> IO(RS()), r: Req.Request) -> IO(Res.Response): IO.bind(RS(), Res.Response, next(r), recover.go) # Body limit # ---------- # a body and its length, counted in one read type Sized is Data: Sized{n: U32, body: List<&2, U32>} def count(bs: List<&2, U32>, +n: U32, acc: List<&2, U32>) -> Sized: match bs: case Nil{}: Sized{n, List.reverse(&2, U32, acc)} case Con{b, t}: count(t, (n + 1 : U32), Con{b, acc}) def body_limit.go(~next: Req.Request -> IO(Res.Response), ok: Bool, m: M.Method, t: String, hs: List<&2, H.Header>, b: List<&2, U32>) -> IO(Res.Response): match ok: case True{}: next(Req.Request{m, t, hs, b}) case False{}: IO.pure(Res.Response, Res.text(S.content_too_large(), "Content Too Large")) def body_limit.sized(~max: U32, ~next: Req.Request -> IO(Res.Response), s: Sized, m: M.Method, t: String, hs: List<&2, H.Header>) -> IO(Res.Response): match s: case Sized{n, b}: body_limit.go(~next, U32.is_le(n, max), m, t, hs, b) # a body over max bytes answers 413 before next sees it; the server's # max_body_bytes bounds every body, this one a handler's def body_limit(~max: U32, ~next: Req.Request -> IO(Res.Response), r: Req.Request) -> IO(Res.Response): match r: case Req.Request{m, t, hs, b}: body_limit.sized(~max, ~next, count(b, 0, []), m, t, hs) # Request id # ---------- def hex.digit(+d: U32) -> Char: Chr{Bool.pick(U32, U32.is_lt(d, 10), (d + 48 : U32), (d + 87 : U32))} def hex.go(n: Nat, +x: U32, acc: String) -> String: match n: case 0n: acc case 1n+p: hex.go(p, (x >> 4n : U32), SCon{hex.digit((x .&. 15 : U32)), acc}) def hex(+x: U32) -> String: hex.go(8n, x, "") def random(r: Result<&1, &1, U32 & String, U32>) -> U32: match r: case Done{x}: x case Fail{_}: 0 # every response gets x-request-id: 16 random hex digits def request_id(~next: Req.Request -> IO(Res.Response), r: Req.Request) -> IO(Res.Response): do IO: a : Result<&1, &1, U32 & String, U32> <- IO.random_u32() b : Result<&1, &1, U32 & String, U32> <- IO.random_u32() res : Res.Response <- next(r) return Res.header(res, "x-request-id", hex(random(a)) ++ hex(random(b))) # Logger # ------ def copy(s: String, acc: String) -> String: match s: case SNil{}: String.reverse(acc) case SCon{Chr{x}, t}: copy(t, SCon{Chr{(x + 0 : U32)}, acc}) # the method's name, copied: M.show hands back MOther's own String, which # would take the method the request still needs def method_name(m: M.Method) -> String: match m: case M.MOther{x}: copy(x, "") case M.MGet{}: "GET" case M.MHead{}: "HEAD" case M.MPost{}: "POST" case M.MPut{}: "PUT" case M.MDelete{}: "DELETE" case M.MConnect{}: "CONNECT" case M.MOptions{}: "OPTIONS" case M.MTrace{}: "TRACE" case M.MPatch{}: "PATCH" def logged(line: String, +start: Nat, res: Res.Response) -> IO(Res.Response): match res: case Res.Response{S.Status{+c}, hs, b}: do IO: +now : Nat <- IO.now() x : Unit <- IO.print(line ++ " " ++ U32.show(c) ++ " " ++ Nat.show(Nat.sub(now, start)) ++ "ms") return Res.Response{S.Status{c}, hs, b} def logger.go(~next: Req.Request -> IO(Res.Response), line: String, r: Req.Request) -> IO(Res.Response): do IO: +start : Nat <- IO.now() res : Res.Response <- next(r) logged(line, start, res) # one line per request on stdout: "GET /todos/2 200 0ms" def logger(~next: Req.Request -> IO(Res.Response), r: Req.Request) -> IO(Res.Response): match r: case Req.Request{+m, +t, hs, b}: logger.go(~next, method_name(m) ++ " " ++ copy(t, ""), Req.Request{m, t, hs, b})