Let’s start with yet another basic, "Hello World" application.
(ns org.example.hello
(:require [io.pedestal.http :as http]
[io.pedestal.http.route :as route])
(defn hello-handler [_request] (1)
{:status 200
:body "Hello, World!"})
(def routes
#{["/hello" :get hello-handler :route-name ::hello]}) (2)
(defn start []
(-> {::http/port 9999
::http/routes routes
::http/type :jetty
::http/join? false} (3)
http/create-server
http/start))
(defonce server (atom nil)) (4)
1 | The request map isn’t used here, so we follow the convention of adding a leading underscore; this may also prevent
warnings in your IDE. |
2 | Every route must have a unique :route-name and since we are providing a function, hello-handler , it must
be explicitly specified. |
3 | Normally, the call to io.pedestal.http/start doesn’t return (until something else shuts down the server); adding
::http/join? false is necessary for REPL-oriented development. |
4 | To survive reloading of this namespace, the running server is stored in an Atom. |
With that in place, evaluate (reset! server (start))
; this will create the server and start it, storing the running system map
back into the server
atom.
You can now use your favorite HTTP client (such as
HTTPIe) to access the /hello
route:
> http :9999/hello
HTTP/1.1 200 OK
Content-Security-Policy: object-src 'none'; script-src 'unsafe-inline' 'unsafe-eval' 'strict-dynamic' https: http:;
Content-Type: text/plain
Date: Fri, 18 Nov 2022 23:41:49 GMT
Strict-Transport-Security: max-age=31536000; includeSubdomains
Transfer-Encoding: chunked
X-Content-Type-Options: nosniff
X-Download-Options: noopen
X-Frame-Options: DENY
X-Permitted-Cross-Domain-Policies: none
X-XSS-Protection: 1; mode=block
Hello, World!
So far, so good. You can also see that Pedestal’s default set of interceptors has added a number of security-related
headers to the response.
Now we’re ready to make a change. Let’s change the response body to be Hello, Clojure World!
.
We can change the hello-handler
function, reload the namespace, and:
> http -pb :9999/hello (1)
Hello, World!
1 | The -pb option prints just the response body, omitting the response headers. |
That’s strange. It’s as if the code change wasn’t picked up. We can verify that the code
change is, in fact, live by evaluating (hello-handler nil)
at the REPL:
(hello-handler nil)
=> {:status 200, :body "Hello, Clojure World!"}
What’s happened here is that the value of the org.example.hello/hello-handler
Var has changed; it now holds a new
Clojure function, one that says "Hello, Clojure World!". However, the routes
and start
functions captured the value of hello-handler
at the time
they executed.
We could say ugh, fine and just adapt to a cycle where you reload your REPL code and then restart the server object, but
that’s slow and error-prone.
We want a fast feedback cycle where that is not necessary, and that goal is entirely achievable.
The first step is to stop the service by evaluating (swap! server http/stop)
.
Next, change the routes
function:
(def routes
#{["/hello" :get `hello-handler]})
Notice that backtick character before hello-handler
. That’s Clojure’s syntax quote; like a single quote,
it keeps hello-handler
from being evaluated, but unlike single quote, the symbol is namespace qualified. It’s the
equivalent of 'org.example.hello/hello-handler
.
To understand what’s going on here, we must consider a key stage in Pedestal: route expansion.
Route expansion is a step during service startup where the provided routes are converted
from one of a number of specifications (such as the table format used here) into an executable format.
Part of expansion is a rule that says that a handler that’s a symbol is replaced with the evaluation of that symbol.
That’s when org.example.hello/hello-handler
is converted from a symbol to function.
Notice that we also no longer have to specify a :route-name; with a handler symbol, a default route name is generated
that’s a keyword version of the symbol.
Now, change the function to return "Hello, Programming World!"
, reload the namespace, and get the /hello
URL again:
> http -pb :9999/hello
Hello, Clojure World!
What gives? Where’s our reloading?
Because of the back-ticked symbol, the evaluation of hello-handler
occurred later than in the original code,
not inside the routes
definition, but somewhere inside the call to
api:create-server[]
… but that evaluation and expansion still only happens once, and so there’s still a capture.
We need the route to always reflect the live version of the hello-handler
function, and that means we have
to avoid capture, not just of the hello-handler
function, but of the entire route specification.