Inching Forward

Finding Joy: Handlers

May 9, 2020

In part 1, we created a very simple web app:

(import halo)

(defn hello [request]
  {:status 200 :body "Hello, world!" :headers {"Content-Type" "text/plain"}})

(halo/server hello 8080)

Let’s dig into this.

First we import the halo module. The halo module contains the server function which starts the server up and listens for requests. We can call it by prefixing it with the module name: halo/server. The server function takes 2 arguments: a handler function, which we have defined as hello, and a port to run on.

A handler function is a function that takes a request table and returns a response table or struct:

A handler function

Our hello handler returns a struct containing the following http response elements:

The hello handler

When this simple app gets run, Halo will start up on port 8080 and respond to every request by calling our hello handler function.

If we run it:

janet hello.janet 
Server listening on [localhost:8080] ...

and use curl to make a request:

$ curl -i http://localhost:8080
HTTP/1.1 200 OK
Content-Type: text/plain
Content-Length: 13

Hello, world!

We can see our 200 in the status line, our Content-Type: text/plain header, and the “Hello, world!" body we set.

The basic foundation for the back end of any Halo/Joy web app is returning a table or struct with those 3 elements. Pretty straightforward, right?

There’s a problem though: right now our simple web app will return “Hello, world!” for any URL we request:

$ curl http://localhost:8080
Hello, world!

$ curl http://localhost:8080/foo
Hello, world!

$ curl http://localhost:8080/bar/baz
Hello, world!

If we want to respond differently to different requests, we somehow have to know what URL path is being used. Let’s examine the request parameter that our handler receives. Janet has a string/format function that we can use to convert the request parameter into something we can return in a :body. The %q format specifier can be used to turn any data structure into a string. Let’s change our app to return the request as a string:

(import halo)
  
(defn hello [request]
  {:status 200 :body (string/format "%q" request)})

(halo/server hello 8080)

The :headers element has been removed to keep things simple. It won’t break anything. Let’s run the server again:

janet hello.janet 
Server listening on [localhost:8080] ...

then in another tab try calling different URLs:

$ curl http://localhost:8080
@{:method "GET" :uri "/" :headers @{"User-Agent" "curl/7.54.0" "Accept" "*/*" "Host" "localhost:8080"}}

$ curl http://localhost:8080/foo
@{:method "GET" :uri "/foo" :headers @{"User-Agent" "curl/7.54.0" "Accept" "*/*" "Host" "localhost:8080"}}

$ curl http://localhost:8080/bar/baz
@{:method "GET" :uri "/bar/baz" :headers @{"User-Agent" "curl/7.54.0" "Accept" "*/*" "Host" "localhost:8080"}}

Here we can see that the request Halo is passing to our hello handler is a table (note the @ preceding the braces) containing 3 elements: :method, :uri, and :headers, which is also a table.

Request and Response

If we want to respond to different request paths, we can use the :uri value to do so. Let’s try:

(import halo)
  
(defn hello [request]
  (case (request :uri)
    "/"        {:status 200 :body "Root"}
    "/foo"     {:status 200 :body "Foo"}
    "/bar/baz" {:status 200 :body "Bar Baz"}
    {:status 404})))

(halo/server hello 8080)

A couple of things here:

If we call the server with no path, we should get “Root” in the body:

Request and Response

Let’s restart the server and try visiting the different urls:

$ curl http://localhost:8080
Root

$ curl http://localhost:8080/foo
Foo

$ curl http://localhost:8080/bar/baz
Bar Baz

$ curl -i http://localhost:8080/quux
HTTP/1.1 404 Not Found

Note on the last curl call, the i flag was used to print out headers to verify that the 404 status code was returned.

Nice–now we know how to respond to any request path. The way we’re handling it is kinda clunky though. In a future post we’ll define a better way to do this.


#janet #joy #web #lisp