lein-ring icon indicating copy to clipboard operation
lein-ring copied to clipboard

Async handlers work only with Jetty

Open dpiliouras opened this issue 4 years ago • 8 comments

There are two issues preventing async handlers from working in any Servlet container, other than Jetty (e.g. Tomcat). Thankfully, one of them is super easy to fix, but the other I'm having trouble with and I would appreciate your help.

  1. The web.xml emitted should contain a async-supported tag - especially when the [:ring :async?] option is true. This is very easily fixable by adding [:async-supported (:async? ring-options false)]], right before closing the :servlet tag in the make-web-xml function (here). If I manage to fix point 2 (see below), I can include this in the PR, otherwise it's such a trivial change that I don't really see the point of a PR just for that.

  2. The compile-listener function calls generate-handler in order to resolve the actual user handler(here). As you can see if the [:ring :servlet-path-info?] option is true (or missing), the user-handler is wrapped in another function, which however takes a single argument, and therefore won't work with the three arguments passed to it (per the async ring.util.servlet/make-service-method). If the [:ring :servlet-path-info?] option is explicitly set to false, then it works as expected (assuming of course that the actual handler allows for 3 args). Now, you might be tempted to think that this is also easily fixable, but trust me, it isn't...I tried adding a 3-arg arity to that function returned by generate-handler, and everything compiles/builds/deploys no problem, but then when I try to use it on a real project, I get compile-syntax errors like this:

:clojure.main/message
 "Syntax error (ClassNotFoundException) compiling at (com/foo/gw/listener.clj:1:519).\nleiningen.ring.war\n",
 :clojure.main/triage
 {:clojure.error/phase :compile-syntax-check,
  :clojure.error/line 1,
  :clojure.error/column 519,
  :clojure.error/source "listener.clj",
  :clojure.error/path "com/foo/gw/listener.clj",
  :clojure.error/class java.lang.ClassNotFoundException,
  :clojure.error/cause "leiningen.ring.war"},

Here is the code in case you have doubts that the syntax is correct:

(defn- with-context-path-info [req context-path]
  (assoc req
    :context context-path
    :path-info (-> (:uri req) (subs (count context-path)) not-empty (or "/"))))

(defn generate-handler [project handler-sym]
  (if (get-in project [:ring :servlet-path-info?] true)
    `(let [handler# ~(generate-resolve handler-sym)]
       (fn
         ([request# respond# raise#]
          (let [context# (.getContextPath
                           ^javax.servlet.http.HttpServletRequest
                           (:servlet-request request#))]
            (-> request#
                (with-context-path-info context#)
                (handler# respond# raise#))))
         ([request#]
          (let [context# (.getContextPath
                           ^javax.servlet.http.HttpServletRequest
                           (:servlet-request request#))]
            (-> request#
                (with-context-path-info context#)
                (handler#))))))
    (generate-resolve handler-sym)))

I also tried generating two distinct functions based on the :async? option, but I got the exact same compile errors:

(if (get-in project [:ring :async?])  
  (fn [req respond raise] ...) 
  (fn [req] ...))

If you can provide any form of insight as to why such a simple change (adding an arity) would cause compile-syntax errors, that would be fantastic. Many thanks in advance...

dpiliouras avatar Jul 30 '21 14:07 dpiliouras

I'm not sure if this is the only error, but the problem I can see is that you're using with-context-path-info inside the code you generate, but that function is private, not public.

weavejester avatar Aug 02 '21 01:08 weavejester

Hmm fair point, but I would expect a IllegalStateException - var: #'lein.ring.war/with-context-path-info is not public, if that was really the issue here...I'll try it and let you know.

dpiliouras avatar Aug 02 '21 08:08 dpiliouras

Hi there,

So, I've abandoned the idea of providing 2 arities, and I've gone with var-args instead which seems to work:

(defn generate-handler [project handler-sym]
  (if (get-in project [:ring :servlet-path-info?] true)
    `(let [handler# ~(generate-resolve handler-sym)]
       (fn [req# & args#]
         (let [context# (.getContextPath
                          ^javax.servlet.http.HttpServletRequest
                          (:servlet-request req#))]
           (apply handler#
                  (assoc req#
                    :context context#
                    :path-info (-> (:uri req#) (subs (count context#)) not-empty (or "/")))
                  args#))))
    (generate-resolve handler-sym)))

I have to admit that I'm not 100% happy with it, but at least it works. Do you want a PR for it, or would you prefer to have a stab at it yourself - see if you can get the 2-arity working?

dpiliouras avatar Aug 13 '21 10:08 dpiliouras

I'd rather figure out why it doesn't work, but I'm not going to have any time to investigate for a while.

weavejester avatar Aug 13 '21 10:08 weavejester

Like you, I would love to know why it can't compile the 2-arity fn, but at this point I've run out of ideas and things I can try. My custom fork is at version 0.12.23 (with the above code which at least works), so that gives you an idea of how many attempts I've made. The error is not helpful at all :( ...

$ cat /var/folders/zw/c3k5myj97z740h4sqdmnvwsr0000gq/T/clojure-8932508282312467787.edn

{:clojure.main/message
 "Syntax error (ClassNotFoundException) compiling at (com/elcom/gw/listener.clj:1:808).\nleiningen.ring.war\n",
 :clojure.main/triage
 {:clojure.error/phase :compile-syntax-check,
  :clojure.error/line 1,
  :clojure.error/column 808,
  :clojure.error/source "listener.clj",
  :clojure.error/path "com/elcom/gw/listener.clj",
  :clojure.error/class java.lang.ClassNotFoundException,
  :clojure.error/cause "leiningen.ring.war"},
 :clojure.main/trace
 {:via
  [{:type clojure.lang.Compiler$CompilerException,
    :message
    "Syntax error compiling at (com/elcom/gw/listener.clj:1:808).",
    :data
    {:clojure.error/phase :compile-syntax-check,
     :clojure.error/line 1,
     :clojure.error/column 808,
     :clojure.error/source "com/elcom/gw/listener.clj"},
    :at [clojure.lang.Compiler analyzeSeq "Compiler.java" 7119]}
   {:type java.lang.ClassNotFoundException,
    :message "leiningen.ring.war",
    :at
    [java.net.URLClassLoader findClass "URLClassLoader.java" 435]}],
  :trace
  [[java.net.URLClassLoader findClass "URLClassLoader.java" 435]
   [clojure.lang.DynamicClassLoader
    findClass
    "DynamicClassLoader.java"
    69]
   [java.lang.ClassLoader loadClass "ClassLoader.java" 589]
   [clojure.lang.DynamicClassLoader
    loadClass
    "DynamicClassLoader.java"
    77]
   [java.lang.ClassLoader loadClass "ClassLoader.java" 522]
   [java.lang.Class forName0 "Class.java" -2]
   [java.lang.Class forName "Class.java" 468]
   [clojure.lang.RT classForName "RT.java" 2212]
   [clojure.lang.RT classForNameNonLoading "RT.java" 2225]
   [clojure.lang.Compiler$HostExpr maybeClass "Compiler.java" 1041]
   [clojure.lang.Compiler macroexpand1 "Compiler.java" 7049]
   [clojure.lang.Compiler analyzeSeq "Compiler.java" 7097]
   [clojure.lang.Compiler analyze "Compiler.java" 6793]
   [clojure.lang.Compiler analyze "Compiler.java" 6749]
   [clojure.lang.Compiler$InvokeExpr parse "Compiler.java" 3892]
   [clojure.lang.Compiler analyzeSeq "Compiler.java" 7113]
   [clojure.lang.Compiler analyze "Compiler.java" 6793]
   [clojure.lang.Compiler analyzeSeq "Compiler.java" 7099]
   [clojure.lang.Compiler analyze "Compiler.java" 6793]
   [clojure.lang.Compiler analyze "Compiler.java" 6749]
   [clojure.lang.Compiler$BodyExpr$Parser parse "Compiler.java" 6124]
   [clojure.lang.Compiler$LetExpr$Parser parse "Compiler.java" 6440]
   [clojure.lang.Compiler analyzeSeq "Compiler.java" 7111]
   [clojure.lang.Compiler analyze "Compiler.java" 6793]
   [clojure.lang.Compiler analyzeSeq "Compiler.java" 7099]
   [clojure.lang.Compiler analyze "Compiler.java" 6793]
   [clojure.lang.Compiler analyze "Compiler.java" 6749]
   [clojure.lang.Compiler$BodyExpr$Parser parse "Compiler.java" 6124]
   [clojure.lang.Compiler$FnMethod parse "Compiler.java" 5471]
   [clojure.lang.Compiler$FnExpr parse "Compiler.java" 4033]
   [clojure.lang.Compiler analyzeSeq "Compiler.java" 7109]
   [clojure.lang.Compiler analyze "Compiler.java" 6793]
   [clojure.lang.Compiler analyzeSeq "Compiler.java" 7099]
   [clojure.lang.Compiler analyze "Compiler.java" 6793]
   [clojure.lang.Compiler analyze "Compiler.java" 6749]
   [clojure.lang.Compiler$BodyExpr$Parser parse "Compiler.java" 6124]
   [clojure.lang.Compiler$LetExpr$Parser parse "Compiler.java" 6440]
   [clojure.lang.Compiler analyzeSeq "Compiler.java" 7111]
   [clojure.lang.Compiler analyze "Compiler.java" 6793]
   [clojure.lang.Compiler analyzeSeq "Compiler.java" 7099]
   [clojure.lang.Compiler analyze "Compiler.java" 6793]
   [clojure.lang.Compiler access$300 "Compiler.java" 38]
   [clojure.lang.Compiler$LetExpr$Parser parse "Compiler.java" 6388]
   [clojure.lang.Compiler analyzeSeq "Compiler.java" 7111]
   [clojure.lang.Compiler analyze "Compiler.java" 6793]
   [clojure.lang.Compiler analyzeSeq "Compiler.java" 7099]
   [clojure.lang.Compiler analyze "Compiler.java" 6793]
   [clojure.lang.Compiler analyze "Compiler.java" 6749]
   [clojure.lang.Compiler$BodyExpr$Parser parse "Compiler.java" 6124]
   [clojure.lang.Compiler$FnMethod parse "Compiler.java" 5471]
   [clojure.lang.Compiler$FnExpr parse "Compiler.java" 4033]
   [clojure.lang.Compiler analyzeSeq "Compiler.java" 7109]
   [clojure.lang.Compiler analyze "Compiler.java" 6793]
   [clojure.lang.Compiler analyzeSeq "Compiler.java" 7099]
   [clojure.lang.Compiler analyze "Compiler.java" 6793]
   [clojure.lang.Compiler access$300 "Compiler.java" 38]
   [clojure.lang.Compiler$DefExpr$Parser parse "Compiler.java" 596]
   [clojure.lang.Compiler analyzeSeq "Compiler.java" 7111]
   [clojure.lang.Compiler analyze "Compiler.java" 6793]
   [clojure.lang.Compiler analyze "Compiler.java" 6749]
   [clojure.lang.Compiler compile1 "Compiler.java" 7730]
   [clojure.lang.Compiler compile1 "Compiler.java" 7725]
   [clojure.lang.Compiler compile1 "Compiler.java" 7725]
   [clojure.lang.Compiler compile "Compiler.java" 7802]
   [clojure.lang.RT compile "RT.java" 411]
   [clojure.lang.RT load "RT.java" 457]
   [clojure.lang.RT load "RT.java" 424]
   [clojure.core$load$fn__6856 invoke "core.clj" 6115]
   [clojure.core$load invokeStatic "core.clj" 6114]
   [clojure.core$load doInvoke "core.clj" 6098]
   [clojure.lang.RestFn invoke "RestFn.java" 408]
   [clojure.core$load_one invokeStatic "core.clj" 5897]
   [clojure.core$compile$fn__6861 invoke "core.clj" 6125]
   [clojure.core$compile invokeStatic "core.clj" 6125]
   [user$eval5 invokeStatic "form-init10924602446184730501.clj" 1]
   [user$eval5 invoke "form-init10924602446184730501.clj" 1]
   [clojure.lang.Compiler eval "Compiler.java" 7181]
   [clojure.lang.Compiler eval "Compiler.java" 7170]
   [clojure.lang.Compiler eval "Compiler.java" 7171]
   [clojure.lang.Compiler load "Compiler.java" 7640]
   [clojure.lang.Compiler loadFile "Compiler.java" 7578]
   [clojure.main$load_script invokeStatic "main.clj" 475]
   [clojure.main$init_opt invokeStatic "main.clj" 477]
   [clojure.main$init_opt invoke "main.clj" 477]
   [clojure.main$initialize invokeStatic "main.clj" 508]
   [clojure.main$null_opt invokeStatic "main.clj" 542]
   [clojure.main$null_opt invoke "main.clj" 539]
   [clojure.main$main invokeStatic "main.clj" 664]
   [clojure.main$main doInvoke "main.clj" 616]
   [clojure.lang.RestFn applyTo "RestFn.java" 137]
   [clojure.lang.Var applyTo "Var.java" 705]
   [clojure.main main "main.java" 40]],
  :cause "leiningen.ring.war",
  :phase :compile-syntax-check}}

dpiliouras avatar Aug 13 '21 11:08 dpiliouras

Is it possible to provide a repository that replicates the error?

weavejester avatar Aug 13 '21 11:08 weavejester

I'll see what I can do...Would a zipped project do, in case I'm not able to publish something online?

dpiliouras avatar Aug 13 '21 12:08 dpiliouras

Yes, if you like you can send it directly to my email, [email protected]. Otherwise, if the problem can be replicated with a minimal project, then that would be fine, too.

weavejester avatar Aug 13 '21 12:08 weavejester