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:
#ANCHOR: import import chronos/apps/http/httpserver import std/[json, tables] #ANCHOR_END: import #ANCHOR: handler_closure proc handler(reports: TableRef[string, string]): HttpProcessCallback2 = proc( reqfence: RequestFence ): Future[HttpResponseRef] {.async: (raises: [CancelledError]).} = #ANCHOR_END: handler_closure let request = reqfence.valueOr: return defaultResponse() try: case request.uri.path of "/": await request.respond(Http200, "Welcome to the Status Dashboard!") #ANCHOR: status_get 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) #ANCHOR_END: status_get #ANCHOR: report_post 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.") #ANCHOR_END: report_post else: await request.respond(Http404, "Page not found.") except HttpError as exc: defaultResponse(exc) proc main() {.async: (raises: [TransportAddressError, CancelledError]).} = #ANCHOR: reports_table 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" #ANCHOR_END: reports_table 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
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 httpserver: 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 httpserver: getBody(HttpRequestRef) to asynchronously read the entire request body.
- 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 httpcommon: bytesToString(openArray[byte]) function from chronos/apps/http/httpcommon.
- 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.
- 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.
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.