| (in-package :cl-user) |
| (defpackage clack-errors |
| (:nicknames :lack.middleware.clack.errors) |
| (:use :cl) |
| (:import-from :trivial-backtrace |
| :print-backtrace) |
| (:export :*clack-error-middleware* |
| :*lack-middleware-clack-errors*)) |
| (in-package :clack-errors) |
|
|
| (defparameter *dev-css-path* |
| (merge-pathnames |
| #p"static/style-dev.css" |
| (asdf:component-pathname (asdf:find-system :clack-errors)))) |
|
|
| (defparameter *prod-css-path* |
| (merge-pathnames |
| #p"static/style-prod.css" |
| (asdf:component-pathname (asdf:find-system :clack-errors)))) |
|
|
| (defparameter *highlight-css* |
| (merge-pathnames |
| #p"static/highlight-lisp/themes/github.css" |
| (asdf:component-pathname (asdf:find-system :clack-errors)))) |
|
|
| (defparameter *highlight-js* |
| (merge-pathnames |
| #p"static/highlight-lisp/highlight-lisp.js" |
| (asdf:component-pathname (asdf:find-system :clack-errors)))) |
|
|
| (defparameter +dev-template+ |
| (djula:compile-template* |
| (asdf:system-relative-pathname :clack-errors #p"templates/dev-page.html"))) |
|
|
| (defparameter +prod-template+ |
| (djula:compile-template* |
| (asdf:system-relative-pathname :clack-errors #p"templates/prod-page.html"))) |
|
|
| (defun condition-name (condition) |
| (symbol-name (class-name (class-of condition)))) |
|
|
| (defun condition-slots (condition) |
| (mapcar #'closer-mop:slot-definition-name |
| (closer-mop:class-slots (class-of condition)))) |
|
|
| (defun slot-values (obj) |
| (loop for slot in (condition-slots obj) |
| collecting |
| (list :slot (symbol-name slot) |
| :value (slot-value obj slot)))) |
|
|
| (defparameter +backtrace-regex+ "\\n\\w*\\d+:" |
| "A regular expression to split backtraces") |
|
|
| (defun split-backtrace (str) |
| (ppcre:split +backtrace-regex+ str)) |
|
|
| (defun parse-backtrace (bt) |
| (destructuring-bind (header &rest frames) (split-backtrace bt) |
| (let ((error-msg (subseq header |
| (position #\: header :from-end t))) |
| (date-time (subseq header |
| (1+ (position #\: header)) |
| (position #\A header)))) |
| (list error-msg date-time frames)))) |
|
|
| (defun render (bt condition env) |
| (let* ((backtrace (parse-backtrace bt))) |
| (djula:render-template* +dev-template+ |
| nil |
| :name (condition-name condition) |
| :slots (slot-values condition) |
| :datetime (nth 1 backtrace) |
| :backtrace (subseq (caddr backtrace) 6) |
| :url (getf env :path-info) |
| :method (getf env :request-method) |
| :query (getf env :query-string) |
| :css (concatenate 'string |
| (uiop:read-file-string *dev-css-path*) |
| (uiop:read-file-string *highlight-css*)) |
| :js (uiop:read-file-string *highlight-js*) |
| :env (loop for (key value) on env by #'cddr collecting |
| (list :key key |
| :value value))))) |
|
|
| (defun render-prod (condition env) |
| (djula:render-template* +prod-template+ |
| nil |
| :name (condition-name condition) |
| :url (getf env :path-info) |
| :css (uiop:read-file-string *prod-css-path*))) |
|
|
| (defparameter *clack-error-middleware* |
| (lambda (app &key (debug t) (prod-render #'render-prod)) |
| (lambda (env) |
| (block nil |
| (handler-bind ((error |
| (lambda (condition) |
| (let ((backtrace (with-output-to-string (stream) |
| (write-string (print-backtrace condition :output nil) |
| stream)))) |
| (return (list 500 |
| '(:content-type "text/html") |
| (list |
| (if debug |
| (render backtrace condition env) |
| (funcall prod-render condition env))))))))) |
| (funcall app env)))))) |
|
|
| (setf (symbol-value '*lack-middleware-clack-errors*) *clack-error-middleware*) |
|
|