clojurecompojurehttp-kit

How do I invoke a resolved function in a Compojure view?


I have the following code as a test:

(defn test-fn [] (println "This is a test"))
(def my-test "test")
((resolve (symbol (str my-test "-fn"))))

Which runs as I would expect producing This is a test.

So I put it inside of a separate view like so:

(ns my-test.template-views
  (:require
   [hiccup.core :refer :all]
   [hiccup.page :refer :all]
   [my-test.home-views :refer :all]
   [my-test.page1-views :refer :all]))

(defn template-body
  [uri]
  (html5 {:lang "en"}
    [:body
     (let [the-page (if (> (count uri) 1)
                      (clojure.string/replace uri #"/" "")
                      "home")]
       ((resolve (symbol (str the-page "-body")))))]))

Which gets called from Compojure like this:

(ns the-test.reporting-dashboard
  (:gen-class)
  (:require
    [the-test.template-views :refer :all]
    [compojure.core :refer [defroutes GET POST context]]
    [compojure.route :as route]
    [org.httpkit.server :refer [run-server]]
    ))

(defn wrap-request
  [handler]
  (fn [request]
    (let [{remote-addr :remote-addr uri :uri scheme :scheme request-method :request-method} request]
      (println (str "REQUEST: " request)))
    (handler request)))

(defroutes app
  (wrap-request
    (GET "/" request
      {:status 200
       :headers {"Content-Type" "text/html"}
       :body (home-views/home-body (:uri request))}))
  (wrap-request
    (GET "/foo" request
      {:status 200
       :headers {"Content-Type" "text/html"}
       :body (template-body (:uri request))}))
  (route/resources "/")
  (route/not-found {:status 404
                    :headers {"Content-Type" "text/html"}
                    :body "<h1>Not Found</h1>"}))

When I call https//mydomain.tld/foo I get a java.lang.NullPointerException:

user=> Sat Apr 24 17:59:16 MDT 2021 [worker-2] ERROR - GET /foo
java.lang.NullPointerException
    at the-test.template_views$template_body.invokeStatic(template_views.clj:14)
    at the-test.template_views$template_body.invoke(template_views.clj:12)
    at the-test.my_test$fn__269541.invokeStatic(my_test.clj:49)
    at the-test.my_test$fn__269541.invoke(my_test.clj:46)
    at compojure.core$wrap_response$fn__269102.invoke(core.clj:158)
    at compojure.core$wrap_route_middleware$fn__269086.invoke(core.clj:128)
    at compojure.core$wrap_route_info$fn__269091.invoke(core.clj:137)
    at compojure.core$wrap_route_matches$fn__269095.invoke(core.clj:146)
    at the-test.my_test$wrap_request$fn__269531.invoke(my_test.clj:22)
    at compojure.core$routing$fn__269110.invoke(core.clj:185)
    at clojure.core$some.invokeStatic(core.clj:2705)
    at clojure.core$some.invoke(core.clj:2696)
    at compojure.core$routing.invokeStatic(core.clj:185)
    at compojure.core$routing.doInvoke(core.clj:182)
    at clojure.lang.RestFn.applyTo(RestFn.java:139)
    at clojure.core$apply.invokeStatic(core.clj:669)
    at clojure.core$apply.invoke(core.clj:662)
    at compojure.core$routes$fn__269114.invoke(core.clj:192)
    at org.httpkit.server.HttpHandler.run(RingHandler.java:117)
    at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:515)
    at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
    at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
    at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
    at java.base/java.lang.Thread.run(Thread.java:834)

What is going on that this function is no longer able to be called when in Compojure?


Solution

  • Not sure about the root cause, but I was able to find a similar, minimal example and a workaround.

    I created a simple project with lein new app demo, then replaced the code for core.clj with:

    (ns demo.core
      (:require [clojure.string :refer :all]))
    
    (defn -main
      [& args]
      (println ((resolve 'capitalize) "hello")))
    

    Running this with lein run crashes:

    $ lein run
    WARNING: reverse already refers to: #'clojure.core/reverse in namespace: demo.core, being replaced by: #'clojure.string/reverse
    WARNING: replace already refers to: #'clojure.core/replace in namespace: demo.core, being replaced by: #'clojure.string/replace
    Syntax error (NullPointerException) compiling at (/tmp/form-init1769060018979063159.clj:1:73).
    null
    
    Full report at:
    /tmp/clojure-17609112051049758700.edn
    

    The error report shows something about an error in Compiler.java so maybe it's some lazy initialization issue or just a bug. It seems to me the var cannot be resolved at the time you are attempting to call it as a function, hence the NullPointerException.

    A workaround would be to use ns-resolve instead:

    (ns demo.core
      (:require [clojure.string :refer :all]))
    
    (defn -main
      [& args]
      (println ((ns-resolve 'clojure.string 'capitalize) "hello")))
    

    The above works as expected:

    $ lein run
    WARNING: reverse already refers to: #'clojure.core/reverse in namespace: demo.core, being replaced by: #'clojure.string/reverse
    WARNING: replace already refers to: #'clojure.core/replace in namespace: demo.core, being replaced by: #'clojure.string/replace
    Hello
    

    See if you can replace your usage of resolve in the view with ns-resolve if you know the namespace where the symbol needs to be looked-up.