Logging Requests with Middleware
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:
import chronos/apps/http/httpserver
import std/[json, tables, times, monotimes]
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
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)
proc main() {.async: (raises: [TransportAddressError, CancelledError]).} =
var reports = newTable[string, string]()
let
middlewares = [HttpServerMiddlewareRef(handler: loggingMiddleware)]
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()
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 RequestFence, and the nextHandler (which is an 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
getMonoTimefromstd/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
inMillisecondsfromstd/times. - We return the
responsereceived from the handler chain.
You may wonder why HttpProcessCallback2 has a 2 in its name and why don't we use 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 HttpServerMiddlewareRef and pass it to the server constructor:
middlewares = [HttpServerMiddlewareRef(handler: loggingMiddleware)]
Then, include it in HttpServerRef.new:
proc main() {.async: (raises: [TransportAddressError, CancelledError]).} =
var reports = newTable[string, string]()
let
middlewares = [HttpServerMiddlewareRef(handler: loggingMiddleware)]
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.