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

Handling POST Requests and Processing JSON

Goal: Learn how to handle POST requests and process incoming JSON data.

Source code: chapter3/src/dashboard.nim

In a real-life application, you often need to receive data from clients, not just serve static content. Our dashboard needs to receive status reports from other services.

Let's update our server to handle POST requests containing JSON data and store these reports in memory:

import chronos/apps/http/httpserver
import std/[json, tables]

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
    address = initTAddress("127.0.0.1:8080")
    server = HttpServerRef.new(address, handler(reports)).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 this version, run it with nimble run and use a tool like curl to send a POST request:

$ curl -X POST -H "Content-Type: application/json" -d '{"name": "google.com", "status": "UP"}' http://127.0.0.1:8080/report

Then, visit 127.0.0.1:8080 in your browser to see the updated status.

Handling POST Requests

Info

The HTTP protocol divides each request and response into a header and a body. The header contains metadata like the request method and path, while the body contains the actual content — the JSON payload in our case. This is true for both requests and responses.

proc handler(reports: TableRef[string, string]): HttpProcessCallback2 =
  proc(
      reqfence: RequestFence
  ): Future[HttpResponseRef] {.async: (raises: [CancelledError]).} =

The first change you'll notice is that we wrapped our handler proc with another function that returns the actual handler (of type HttpProcessCallback2). This is done to enable passing an input param reports that we'll use to store the statuses.

In the handler, we added logic for the /report path:

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.")
  1. We check if the request method is MethodPost.
  2. We use request.getBody() to asynchronously read the entire request body.
  3. body is an array of bytes, so we need to convert it to a string before we can parse it. To do that, we use bytesToString function from chronos/apps/http/httpcommon.
  4. We use Nim's std/json library to parse the body as JSON. We wrap this in a try-except block to handle parsing errors. We want to catch all parsing errors at this point, so it's a rare case where catching generic CatchableError is fine.
  5. We extract the relevant fields and store them in our table. We use a separate try-except block to catch KeyError if the fields are missing.

Info

When dealing with JSON from clients, we must assume it can be malformed or missing fields. We handle these cases by catching parsing errors and KeyError exceptions, returning an appropriate HTTP 400 Bad Request status.

Generating Response

Finally, for the /status path, we now generate a dynamic string based on the data in our table:

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)

Storing Data in Memory

We use an in-memory TableRef to store our status reports.

var reports = newTable[string, string]()

let
  address = initTAddress("127.0.0.1:8080")
  server = HttpServerRef.new(address, handler(reports)).valueOr:
    echo "Unable to start HTTP server: " & error
    return

server.start()
echo "HTTP server running on http://127.0.0.1:8080"

We pass reports to the handler generating function to generate a handler that would store statuses to it.

Info

In a real app you would store your persistent data in a database of key-value storage. In this tutorial, we use a Table for simplicity's sake.