You might consider this a cheating, but I really want to review all Lack
middlewares regardless most of them are from the same Lack
project. These middlewares are loadable as separate ASDF systems.
The problem of Lack middlewares is that they are not documented.
This middleware will output a backtrace and all request parameters to the stream or a file.
If you are using clack:clackup
function to start your app, it will apply
a backtrace
middleware to it, unless :use-default-middlewares nil
argument was given. Without configuration, all backtraces will be
written to *error-output*
stream.
Let’s see how does it work!
POFTHEDAY> (defparameter *app*
(lambda (env)
(declare (ignorable env))
(error "Oh my God!")))
*APP*
POFTHEDAY> (clack:clackup *app*
:port 8085)
Hunchentoot server is started.
Listening on 127.0.0.1:8085.
POFTHEDAY> (values (dex:get "http://localhost:8085/foo/bar"))
Backtrace for: #<SB-THREAD:THREAD "hunchentoot-worker-127.0.0.1:56469" RUNNING {1007707373}>
0: ((LAMBDA NIL :IN UIOP/IMAGE:PRINT-BACKTRACE))
1: ((FLET "THUNK" :IN UIOP/STREAM:CALL-WITH-SAFE-IO-SYNTAX))
2: (SB-IMPL::%WITH-STANDARD-IO-SYNTAX #<CLOSURE (FLET "THUNK" :IN UIOP/STREAM:CALL-WITH-SAFE-IO-SYNTAX) {D85A24B}>)
3: (UIOP/STREAM:CALL-WITH-SAFE-IO-SYNTAX #<CLOSURE (LAMBDA NIL :IN UIOP/IMAGE:PRINT-BACKTRACE) {100791B9EB}> :PACKAGE :CL)
4: (UIOP/IMAGE:PRINT-CONDITION-BACKTRACE #<SIMPLE-ERROR "Oh my God!" {100791B943}> :STREAM #<SYNONYM-STREAM :SYMBOL SLYNK::*CURRENT-ERROR-OUTPUT* {1001541093}> :COUNT NIL)
5: (LACK.MIDDLEWARE.BACKTRACE::PRINT-ERROR #<SIMPLE-ERROR "Oh my God!" {100791B943}> (:REQUEST-METHOD :GET :SCRIPT-NAME "" :PATH-INFO "/foo/bar" :SERVER-NAME "localhost" :SERVER-PORT 8085 :SERVER-PROTOCOL :HTTP/1.1 ...) #<SYNONYM-STREAM :SYMBOL SLYNK::*CURRENT-ERROR-OUTPUT* {1001541093}>)
6: ((FLET LACK.MIDDLEWARE.BACKTRACE::OUTPUT-BACKTRACE :IN "/Users/art/projects/lisp/lisp-project-of-the-day/.qlot/dists/ultralisp/software/fukamachi-lack-20200524065357/src/middleware/backtrace.lisp") #<SIMPLE-ERROR "Oh my God!" {100791B943}> (:REQUEST-METHOD :GET :SCRIPT-NAME "" :PATH-INFO "/foo/bar" :SERVER-NAME "localhost" :SERVER-PORT 8085 :SERVER-PROTOCOL :HTTP/1.1 ...))
...
31: (SB-THREAD::NEW-LISP-THREAD-TRAMPOLINE #<SB-THREAD:THREAD "hunchentoot-worker-127.0.0.1:56469" RUNNING {1007707373}> NIL #<CLOSURE (LABELS BORDEAUX-THREADS::%BINDING-DEFAULT-SPECIALS-WRAPPER :IN BORDEAUX-THREADS::BINDING-DEFAULT-SPECIALS) {100770731B}> NIL)
32: ("foreign function: call_into_lisp")
33: ("foreign function: new_thread_trampoline")
Above backtrace due to this condition:
Oh my God!
Request:
REQUEST-METHOD: :GET
SCRIPT-NAME: ""
PATH-INFO: "/foo/bar"
SERVER-NAME: "localhost"
SERVER-PORT: 8085
SERVER-PROTOCOL: :HTTP/1.1
REQUEST-URI: "/foo/bar"
URL-SCHEME: "http"
REMOTE-ADDR: "127.0.0.1"
REMOTE-PORT: 56469
QUERY-STRING: NIL
RAW-BODY: #<FLEXI-STREAMS:FLEXI-IO-STREAM {100791B313}>
CONTENT-LENGTH: 0
CONTENT-TYPE: NIL
CLACK.STREAMING: T
CLACK.IO: #<CLACK.HANDLER.HUNCHENTOOT::CLIENT {100791B363}>
HEADERS:
user-agent: "Dexador/0.9.14 (SBCL 2.0.2); Darwin; 19.5.0"
host: "localhost:8085"
accept: "*/*"
The problem here is that I did not receive a 500 error. An interactive
debugger popped up instead and HTTP request finished with a timeout. To
solve this problem, we need to pass a :debug nil
argument to clackup
:
(clack:clackup *app*
:port 8085
:debug nil)
Now we’ll try other configuration of this backtrace middleware.
To write output to the file, you need to specify the output option. It can be either a string or a pathname:
POFTHEDAY> (clack:clackup
(lack:builder
(:backtrace :output "/tmp/errors.log")
*app*)
:port 8089
:debug nil
;; If you don't turn off this,
;; backtrace also will be written to the
;; *error-output*.
:use-default-middlewares nil)
Also, you can pass as the output a variable pointing to the stream:
POFTHEDAY> (clack:clackup
(lack:builder
(:backtrace :output *trace-output*)
*app*)
:port 8090
:debug nil
:use-default-middlewares nil)
Another interesting option is :result-on-error
. It can be a function or a
list with the response data. This way we can return a customized error
response:
POFTHEDAY> (clack:clackup
(lack:builder
(:backtrace :output "/tmp/errors.log"
:result-on-error
'(500 (:content-type "text/plain")
("Stay patient. "
"We already fixing this error in the REPL")))
*app*)
:port 8092
:debug nil
:use-default-middlewares nil)
POFTHEDAY> (handler-case (dex:get "http://localhost:8092/foo/bar")
(error (condition)
condition))
#<DEXADOR.ERROR:HTTP-REQUEST-INTERNAL-SERVER-ERROR {1009B3BA03}>
POFTHEDAY> (dexador:response-status *)
500
POFTHEDAY> (dexador:response-body **)
"Stay patient. We already fixing this error in the REPL"
Specifying a function as an error handler allows you to render an error response using information from the unhandled condition:
POFTHEDAY> (defun make-error-response (condition)
(list 500 '(:content-type "text/plain")
(list (format nil
"Unhandled error: ~A"
condition))))
POFTHEDAY> (clack:clackup
(lack:builder
(:backtrace :output "/tmp/errors.log"
:result-on-error
#'make-error-response)
*app*)
:port 8093
:use-default-middlewares nil)
POFTHEDAY> (handler-case (dex:get "http://localhost:8093/foo/bar")
(error (condition)
(values (dex:response-status condition)
(dex:response-body condition))))
500
"Unhandled error: Oh my God!"
Notice, I didn’t specify a :debug nil
argument for clackup
. When you are
using :result-on-error
argument on backtrace middleware, it will return
a response before the lisp debugger will have a chance to pop up.
If you want to invoke debugger in some cases, you can call a
(invoke-debugger condition)
somewhere inside make-error-response
function.
Yesterday we’ll review some other Lack’s middleware.