Setting Up a Basic HTTP Server

Source   Edit  

Goal: Learn how to create and start a simple HTTP server with Chronos.

Source code: chapter1/src/dashboard.nim

First, let's initialize a new binary project with Nimble. Switch to your preferred project directory in your terminal and run:

$ nimble init dashboard

When prompted, choose binary for the package type.

Now, open the generated dashboard.nimble file and add chronos to the dependencies:

# Dependencies

requires "nim >= 2.0.0"
requires "chronos"

Finally, open src/dashboard.nim and replace the code in it with this (we'll go through each line in a moment):

#ANCHOR: import
import chronos/apps/http/httpserver
#ANCHOR_END: import

#ANCHOR: handler
proc handler(
    reqfence: RequestFence
): Future[HttpResponseRef] {.async: (raises: [CancelledError]).} =
  if reqfence.isErr():
    return defaultResponse()

  let request = reqfence.get()

  try:
    await request.respond(Http200, "Hello, Chronos!")
  except HttpWriteError:
    defaultResponse()

#ANCHOR_END: handler

#ANCHOR: main
proc main() {.async: (raises: [TransportAddressError, CancelledError]).} =
  let
    address = initTAddress("127.0.0.1:8080")
    server = HttpServerRef.new(address, handler).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()

#ANCHOR_END: main

#ANCHOR: run
when isMainModule:
  waitFor main()
#ANCHOR_END: run

To execute the project, run this command from the dashboard directory:

$ nimble run

You should see the following message in your terminal:

HTTP server running on http://127.0.0.1:8080

Now, open your web browser and go to 127.0.0.1:8080. You should see "Hello, Chronos!".

Line-by-Line Explanation

import chronos/apps/http/httpserver

httpserver module implements the HTTP server capabilities, i.e. listening for incoming connections and responding to HTTP requests.

proc handler(
    reqfence: RequestFence
): Future[HttpResponseRef] {.async: (raises: [CancelledError]).} =
  if reqfence.isErr():
    return defaultResponse()

  let request = reqfence.get()

  try:
    await request.respond(Http200, "Hello, Chronos!")
  except HttpWriteError:
    defaultResponse()

We define a handler function that will be called for every incoming request.

Note that this function takes a httpserver: RequestFence as an argument. httpserver: RequestFence is a Result type that can contain either a valid httpserver: HttpRequestRef or an error. This allows Chronos to notify us if something went wrong during request parsing.

Note: Result comes from results library. It's somewhat similar to Nim's built-in Options type but more powerful. Chronos uses it all around the place whenever a function can return a result or an error.

The function is annotated with the asyncmacro: async(untyped) pragma and raises: [CancelledError] (futures: CancelledError) according to Chronos's Errors and exceptions: Checked exceptions.

Inside the handler, we first check if the request was received correctly. If not, we return a httpserver: defaultResponse(), which is simply an empty response.

If the request is valid, we use the httpserver: respond(HttpRequestRef, HttpCode, ByteChar) method to send a simple string back to the client with an HTTP 200 OK status.

We wrap the respond call in a try-except block to handle potential network errors (httpcommon: HttpWriteError). Note that we let futures: CancelledError propagate to the caller instead of catching it.

proc main() {.async: (raises: [TransportAddressError, CancelledError]).} =
  let
    address = initTAddress("127.0.0.1:8080")
    server = HttpServerRef.new(address, handler).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()

In the main function, we:

  1. Define the address and port to listen on (127.0.0.1:8080).
  2. Create an instance of the server using new(typedesc[HttpServerRef], TransportAddress, HttpProcessCallback2, set[HttpServerFlags], set[ServerFlags], string, int, int, int, int, int, openArray[HttpServerMiddlewareRef]).
  3. Start the server with httpserver: start(HttpServerRef).
  4. Use httpserver: join(HttpServerRef) to wait until the server is stopped (which, in this case, will be never, until we manually terminate the program with Ctrl-C).
  5. In the finally block, we ensure the server is stopped and its resources are released correctly.
Note: `valueOr` is a helper template from the `results` package that returns the value of a Result or executes a given code block if it is an error.
when isMainModule:
  waitFor main()

Finally, we use asyncfutures: waitFor(Future[void]) to start our async main routine.