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
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.")
- We check if the request method is
MethodPost. - We use
request.getBody()to asynchronously read the entire request body. bodyis an array of bytes, so we need to convert it to a string before we can parse it. To do that, we usebytesToStringfunction fromchronos/apps/http/httpcommon.- We use Nim's
std/jsonlibrary to parse the body as JSON. We wrap this in atry-exceptblock to handle parsing errors. We want to catch all parsing errors at this point, so it's a rare case where catching genericCatchableErroris fine. - We extract the relevant fields and store them in our table. We use a separate
try-exceptblock to catchKeyErrorif the fields are missing.
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.