Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update stacktrace to include trimmed? and use color? in web #387

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
122 changes: 78 additions & 44 deletions ring-devel/src/ring/middleware/stacktrace.clj
Original file line number Diff line number Diff line change
Expand Up @@ -5,117 +5,151 @@
This middleware is for debugging purposes, and should be limited to
development environments."
(:require [clojure.java.io :as io]
[clojure.string :as str]
[hiccup.core :refer [html h]]
[hiccup.page :refer [html5]]
[clj-stacktrace.core :refer :all]
[clj-stacktrace.repl :refer :all]
[clj-stacktrace.core :refer [parse-exception]]
sirmspencer marked this conversation as resolved.
Show resolved Hide resolved
[clj-stacktrace.repl :refer [clojure-method-str
sirmspencer marked this conversation as resolved.
Show resolved Hide resolved
elem-color
java-method-str
pst
pst-on
source-str]]
[ring.util.response :refer [content-type response status]]))

(defn swap-trace-elems
sirmspencer marked this conversation as resolved.
Show resolved Hide resolved
"Recursively replace :trace-elems with :trimmed-elems"
[exception]
(let [trimmed (or (:trimmed-elems exception) '())
cause (:cause exception)]
(if cause
(assoc exception
:cause (swap-trace-elems cause)
:trace-elems trimmed)
(assoc exception :trace-elems trimmed))))

(defn trim-error-elems [ex trimmed?]
sirmspencer marked this conversation as resolved.
Show resolved Hide resolved
(if trimmed?
(swap-trace-elems (parse-exception ex))
(parse-exception ex)))

(defn wrap-stacktrace-log
"Wrap a handler such that exceptions are logged to *err* and then rethrown.
Accepts the following options:

:color? - if true, apply ANSI colors to stacktrace (default false)"
:color? - if true, apply ANSI colors to stacktrace (default false)
:trimmed? - if true, use the trimmed-elems instead of trace-elems (default false)"
sirmspencer marked this conversation as resolved.
Show resolved Hide resolved
([handler]
(wrap-stacktrace-log handler {}))
([handler options]
(let [color? (:color? options)]
(let [{:keys [color? trimmed?]} options]
(fn
([request]
(try
(handler request)
(catch Throwable ex
(pst-on *err* color? ex)
(pst-on *err* color? (trim-error-elems ex trimmed?))
(throw ex))))
([request respond raise]
(try
(handler request respond (fn [ex] (pst-on *err* color? ex) (raise ex)))
(handler request respond (fn [ex] (pst-on *err* color? (trim-error-elems ex trimmed?)) (raise ex)))
sirmspencer marked this conversation as resolved.
Show resolved Hide resolved
(catch Throwable ex
(pst-on *err* color? ex)
(pst-on *err* color? (trim-error-elems ex trimmed?))
(throw ex))))))))

(defn- style-resource [path]
(html [:style {:type "text/css"} (slurp (io/resource path))]))

(defn- elem-partial [elem]
(defn color-style
sirmspencer marked this conversation as resolved.
Show resolved Hide resolved
"Returns a style tag with the color appropriate for the given trace elem.
Cyan is replaced with black for readability on the light background."
[elem]
{:style
{:color (str/replace (name (elem-color elem)) "cyan" "black")}})

(defn- elem-partial [elem color?]
(if (:clojure elem)
[:tr.clojure
[:tr.clojure (when color? (color-style elem))
[:td.source (h (source-str elem))]
[:td.method (h (clojure-method-str elem))]]
[:tr.java
[:tr.java (when color? (color-style elem))
[:td.source (h (source-str elem))]
[:td.method (h (java-method-str elem))]]))

(defn- html-exception [ex]
(let [[ex & causes] (iterate :cause (parse-exception ex))]
(defn- html-exception [ex color? trimmed?]
(let [[ex & causes] (iterate :cause (trim-error-elems ex trimmed?))]
(html5
[:head
[:title "Ring: Stacktrace"]
(style-resource "ring/css/stacktrace.css")]
[:body
[:div#exception
[:h1 (h (.getName ^Class (:class ex)))]
[:div.message (h (:message ex))]
[:h1 (h (.getName ^Class (:class ex)))]
sirmspencer marked this conversation as resolved.
Show resolved Hide resolved
[:div.message (h (:message ex))]
(when (pos? (count (:trace-elems ex)))
[:div.trace
[:table
sirmspencer marked this conversation as resolved.
Show resolved Hide resolved
[:tbody (map elem-partial (:trace-elems ex))]]]
[:table
[:tbody (map #(elem-partial % color?) (:trace-elems ex))]]])
(for [cause causes :while cause]
[:div#causes
[:h2 "Caused by " [:span.class (h (.getName ^Class (:class cause)))]]
sirmspencer marked this conversation as resolved.
Show resolved Hide resolved
[:div.message (h (:message cause))]
[:div.trace
[:table
[:tbody (map elem-partial (:trace-elems cause))]]]])]])))
[:h2 "Caused by " [:span.class (h (.getName ^Class (:class cause)))]]
[:div.message (h (:message cause))]
[:div.trace
[:table
[:tbody (map #(elem-partial % color?) (:trace-elems cause))]]]])]])))

(defn- text-ex-response [e]
(-> (response (with-out-str (pst e)))
(status 500)
(content-type "text/plain")))

(defn- html-ex-response [ex]
(-> (response (html-exception ex))
(defn- html-ex-response [ex color? trimmed?]
(-> (response (html-exception ex color? trimmed?))
(status 500)
(content-type "text/html")))

(defn- ex-response
"Returns a response showing debugging information about the exception.

Renders HTML if that's in the accept header (indicating that the URL was
opened in a browser), but defaults to plain text."
[req ex]
[req ex color? trimmed?]
(let [accept (get-in req [:headers "accept"])]
(if (and accept (re-find #"^text/html" accept))
(html-ex-response ex)
(html-ex-response ex color? trimmed?)
(text-ex-response ex))))

(defn wrap-stacktrace-web
"Wrap a handler such that exceptions are caught and a response containing
a HTML representation of the exception and stacktrace is returned."
[handler]
(fn
([request]
(try
(handler request)
(catch Throwable ex
(ex-response request ex))))
([request respond raise]
(try
(handler request respond (fn [ex] (respond (ex-response request ex))))
(catch Throwable ex
(respond (ex-response request ex)))))))
a HTML representation of the exception and stacktrace is returned.
Accepts the following option:
:color? - if true, apply ANSI colors to terminal stacktrace (default false)
:trimmed? - if true, use the trimmed-elems instead of trace-elems (default false)"
sirmspencer marked this conversation as resolved.
Show resolved Hide resolved
([handler]
(wrap-stacktrace-web handler {}))
([handler options]
(let [{:keys [color? trimmed?]} options]
(fn
([request]
(try
(handler request)
(catch Throwable ex
(ex-response request ex color? trimmed?))))
([request respond raise]
(try
(handler request respond (fn [ex] (respond (ex-response request ex color? trimmed?))))
sirmspencer marked this conversation as resolved.
Show resolved Hide resolved
(catch Throwable ex
(respond (ex-response request ex color? trimmed?)))))))))

(defn wrap-stacktrace
"Wrap a handler such that exceptions are caught, a corresponding stacktrace is
logged to *err*, and a HTML representation of the stacktrace is returned as a
response.

Accepts the following option:

:color? - if true, apply ANSI colors to terminal stacktrace (default false)"
:color? - if true, apply ANSI colors to terminal stacktrace (default false)
:trimmed? - if true, use the trimmed-elems instead of trace-elems (default false)"
{:arglists '([handler] [handler options])}
([handler]
(wrap-stacktrace handler {}))
([handler options]
(-> handler
(wrap-stacktrace-log options)
(wrap-stacktrace-web))))
(wrap-stacktrace-web options))))
60 changes: 32 additions & 28 deletions ring-devel/test/ring/middleware/test/stacktrace.clj
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
(def assert-app (wrap-stacktrace (fn [_] (assert (= 1 2)))))


(def html-req {:headers {"accept" "text/html"}})
(def js-req {:headers {"accept" "application/javascript"}})
(def html-req {:headers {"accept" "text/html"}})
(def js-req {:headers {"accept" "application/javascript"}})
(def plain-req {})

(deftest wrap-stacktrace-smoke
Expand All @@ -25,35 +25,39 @@
(is (or (.startsWith body "java.lang.Exception")
(.startsWith body "java.lang.AssertionError")))))
(testing "requests without Accept header"
(let [{:keys [status headers body]} (app js-req)]
(let [{:keys [status headers body]} (app plain-req)]
(is (= 500 status))
(is (= {"Content-Type" "text/plain"} headers))
(is (or (.startsWith body "java.lang.Exception")
(.startsWith body "java.lang.AssertionError"))))))))

(def default-params {})
(def non-default-params {:color? true :trimmed? true})

(deftest wrap-stacktrace-cps-test
(testing "no exception"
(let [handler (wrap-stacktrace (fn [_ respond _] (respond :ok)))
response (promise)
exception (promise)]
(handler {} response exception)
(is (= :ok @response))
(is (not (realized? exception)))))

(testing "thrown exception"
(let [handler (wrap-stacktrace (fn [_ _ _] (throw (Exception. "fail"))))
response (promise)
exception (promise)]
(binding [*err* (java.io.StringWriter.)]
(handler {} response exception))
(is (= 500 (:status @response)))
(is (not (realized? exception)))))

(testing "raised exception"
(let [handler (wrap-stacktrace (fn [_ _ raise] (raise (Exception. "fail"))))
response (promise)
exception (promise)]
(binding [*err* (java.io.StringWriter.)]
(handler {} response exception))
(is (= 500 (:status @response)))
(is (not (realized? exception))))))
(doseq [params [default-params non-default-params]]
(testing "no exception"
sirmspencer marked this conversation as resolved.
Show resolved Hide resolved
(let [handler (wrap-stacktrace (fn [_ respond _] (respond :ok)) params)
response (promise)
exception (promise)]
(handler {} response exception)
(is (= :ok @response))
(is (not (realized? exception)))))

(testing "thrown exception"
(let [handler (wrap-stacktrace (fn [_ _ _] (throw (Exception. "fail"))) params)
sirmspencer marked this conversation as resolved.
Show resolved Hide resolved
response (promise)
exception (promise)]
(binding [*err* (java.io.StringWriter.)]
(handler {} response exception))
(is (= 500 (:status @response)))
(is (not (realized? exception)))))

(testing "raised exception"
(let [handler (wrap-stacktrace (fn [_ _ raise] (raise (Exception. "fail"))) params)
sirmspencer marked this conversation as resolved.
Show resolved Hide resolved
response (promise)
exception (promise)]
(binding [*err* (java.io.StringWriter.)]
(handler {} response exception))
(is (= 500 (:status @response)))
(is (not (realized? exception)))))))