Setting Up a Basic HTTP Server
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):
import chronos/apps/http/httpserver
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()
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()
when isMainModule:
waitFor main()
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 RequestFence as an argument. RequestFence is a Result type that can contain either a valid HttpRequestRef or an error. This allows Chronos to notify us if something went wrong during request parsing.
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 async pragma and raises: [CancelledError] (CancelledError) according to Chronos's checked exceptions.
Inside the handler, we first check if the request was received correctly. If not, we return a defaultResponse(), which is simply an empty response.
If the request is valid, we use the respond 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 (HttpWriteError). Note that we let 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:
- Define the address and port to listen on (
127.0.0.1:8080). - Create an instance of the server using
HttpServerRef.new. - Start the server with
server.start(). - Use
server.join()to wait until the server is stopped (which, in this case, will be never, until we manually terminate the program withCtrl-C). - In the
finallyblock, we ensure the server is stopped and its resources are released correctly.
when isMainModule:
waitFor main()
Finally, we use waitFor to start our async main routine.