Goal: Learn how to extend your server's functionality with middleware.
Source code: chapter4/src/dashboard.nim
Middleware is a way to wrap your request handler with additional logic. This is useful for cross-cutting concerns like logging, authentication, modifying request and response headers, and for sharing a single HTTP server between multiple services (e.g. a metrics server and a REST API server).
Let's add a simple logging middleware that tracks how long each request takes to process:
#ANCHOR: import import chronos/apps/http/httpserver import std/[json, tables, times, monotimes] #ANCHOR_END: import #ANCHOR: middleware proc loggingMiddleware( middleware: HttpServerMiddlewareRef, reqfence: RequestFence, nextHandler: HttpProcessCallback2, ): Future[HttpResponseRef] {.async: (raises: [CancelledError]).} = let startTime = getMonoTime() let response = await nextHandler(reqfence) let duration = getMonoTime() - startTime if reqfence.isOk(): let request = reqfence.get() echo $request.meth & " " & request.uri.path & " processed in " & $duration.inMilliseconds & " ms" response #ANCHOR_END: middleware #ANCHOR: handler proc handler(reports: TableRef[string, string]): HttpProcessCallback2 = proc( reqfence: RequestFence ): Future[HttpResponseRef] {.async: (raises: [CancelledError]).} = let request = reqfence.valueOr: return defaultResponse() try: case request.uri.path of "/": await request.respond(Http200, "Welcome to the Status Dashboard!") of "/status": var output = "Current Service Status:\n" if reports.len == 0: output.add("- No reports available.") else: for name, status in reports: output.add("- " & name & ": " & status & "\n") await request.respond(Http200, output) of "/report": if request.meth != MethodPost: return await request.respond(Http405, "Method Not Allowed") let body = await request.getBody() data = try: parseJson(bytesToString(body)) except CatchableError: return await request.respond(Http400, "Invalid JSON.") name = try: data["name"].getStr() except KeyError: return await request.respond(Http400, "Missing 'name' field.") status = try: data["status"].getStr() except KeyError: return await request.respond(Http400, "Missing 'status' field.") reports[name] = status echo "Received report: " & name & " is " & status await request.respond(Http200, "Report received.") else: await request.respond(Http404, "Page not found.") except HttpError as exc: defaultResponse(exc) #ANCHOR_END: handler #ANCHOR: main proc main() {.async: (raises: [TransportAddressError, CancelledError]).} = var reports = newTable[string, string]() let #ANCHOR: setup_middleware middlewares = [HttpServerMiddlewareRef(handler: loggingMiddleware)] #ANCHOR_END: setup_middleware address = initTAddress("127.0.0.1:8080") server = HttpServerRef.new(address, handler(reports), middlewares = middlewares).valueOr: echo "Unable to start HTTP server: " & error return server.start() echo "HTTP server running on http://127.0.0.1:8080" try: await server.join() finally: await server.stop() await server.closeWait() #ANCHOR_END: main when isMainModule: waitFor main()
To test the middleware, run the project with nimble run and make some requests to your server (with curl and from your browser).
Defining a Middleware
A middleware handler is a function that takes the current middleware object, the httpserver: RequestFence, and the nextHandler (which is an httpserver: HttpProcessCallback2) in the chain:
proc loggingMiddleware( middleware: HttpServerMiddlewareRef, reqfence: RequestFence, nextHandler: HttpProcessCallback2, ): Future[HttpResponseRef] {.async: (raises: [CancelledError]).} = let startTime = getMonoTime() let response = await nextHandler(reqfence) let duration = getMonoTime() - startTime if reqfence.isOk(): let request = reqfence.get() echo $request.meth & " " & request.uri.path & " processed in " & $duration.inMilliseconds & " ms" response
- We record the current time before processing the request using getMonoTime from std/monotimes.
- We call await nextHandler(reqfence) to pass the request to the next middleware or the main handler.
- After the handler returns, we calculate the duration and print a log message. To get the processing duration in milliseconds, we use inMilliseconds from std/times.
- We return the response received from the handler chain.
You may wonder why HttpProcessCallback2 has a 2 in its name and why don't we use httpserver: HttpProcessCallback.
The difference is that HttpProcessCallback2 is a newer and stricter version while HttpProcessCallback is kept for backward compatibility.
So, long story short: use HttpProcessCallback2 unless you're sure you need HttpProcessCallback.
Registering Middleware
To use middleware, you need to create an array of httpserver: HttpServerMiddlewareRef and pass it to the server constructor:
middlewares = [HttpServerMiddlewareRef(handler: loggingMiddleware)]
Then, include it in new(typedesc[HttpServerRef], TransportAddress, HttpProcessCallback2, set[HttpServerFlags], set[ServerFlags], string, int, int, int, int, int, openArray[HttpServerMiddlewareRef]):
proc main() {.async: (raises: [TransportAddressError, CancelledError]).} = var reports = newTable[string, string]() let #ANCHOR: setup_middleware middlewares = [HttpServerMiddlewareRef(handler: loggingMiddleware)] #ANCHOR_END: setup_middleware address = initTAddress("127.0.0.1:8080") server = HttpServerRef.new(address, handler(reports), middlewares = middlewares).valueOr: echo "Unable to start HTTP server: " & error return server.start() echo "HTTP server running on http://127.0.0.1:8080" try: await server.join() finally: await server.stop() await server.closeWait()
Now, every time your server receives a request, you'll see a log message in your terminal with the method, path, and processing time.