Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

  1. We record the current time before processing the request using getMonoTime from std/monotimes.
  2. We call await nextHandler(reqfence) to pass the request to the next middleware or the main handler.
  3. 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.
  4. We return the response received from the handler chain.

Info

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.