Installing a fallback error handler for your Racket web app
True story: Your Racket code isn't perfectHere's what you don't want visitors to your awesome new Racket web app to see:
How can that be?!, you're asking yourself. This isn't a post about trying to avoid writing code that produces such mistakes, such as type checking or program verification. It's understood that when writing a program we are trying our best not to crash things. But as you know, sh*tuff happens. You'll almost certainly encounter unusual situations or edge cases that, despite your most conscientious efforts, you didn't foresee. And you want to be prepared.
Expecting the unexpectedError handling, in the web server context, is the process by which you deal with errors that come up while handling a request. Dealing with errors in the various handlers that make up your web app is a good first step. Division by zero when handling data that comes from a supposedly trusted third-party is different from division by zero when a user gave you some bogus input (quite possibly unintentionally). In the first case, you need to have some kind of intelligent fallback; in the second case, you can probably just notify your user of busted input that needs fixing. In general, I think there's no such thing as the
rightthing to do when, say, a division by zero happens. It depends on whom you're dealing with, and where, and when. But how to deal with those unexpected errors that slipped through the cracks, despite all your best efforts? It's smart to set up a fallback error handler for a Racket web app, a last-ditch way of dealing with uncaught exceptions. Here's how you can do it.
Telling Racket to use a fallback error handlerHere's how you can set up an error handler in your Racket app. Use the
servlet-responderkeyword argument to
serve/servlet, like so:
The value you provide there should be a function of two arguments: a URL (that is, a
(serve/servlet my-dispatcher #:servlet-responder internal-server-error ;; other settings here )
url?) and an exception (that is, an
exn?). The first argument is the URL that was visited, and whose evaluation landed you in the current mess. The second argument is the exception that was raised (or thrown) by Racket during evaluation.
Example(This code relies on
response.rkt, a little convenience utility for making HTTP responses in Racket. You can download the source code for this tutorial here.) Let's make a simple site that deliberately opens the door to an easily identifiable error: division by zero. Keep in mind that we're focusing for the moment on the problem of making a fallback error handler. We're not considering error handling for specific request handlers. In this case, we could easily check the inputs we'll receive and respond appropriately. The fallback error handler is dealing with rather less information than is available in any particular request handler. That means that the information we'll have available to us, when writing our fallback error handler, is probably going to relatively coarse-grained. (Of course, there's no problem having a fallback error handler and handling more specific errors in more specific ways.) Here's a fallback error handler that (1) logs the URL whose processing generated the exception, and (2) responds with an error page.
oops.html looks like this:
(define (oops url ex) (log-error "[~a] ~a ~~~~> ~a" (datetime->iso8601 (now/utc)) (url->string url) (exn-message ex)) (response #:code 500 #:mime html-mime #:body (include-template "oops.html")))
Now we need to install
<!doctype html> <html lang="en"> <head> <title>Ouch!</title> </head> <body> <p>It seems that we've run into a bit of an issue here. Please check back shortly!</p> </body> </html>
oopsas the fallback error handler. I assume that a dispatcher named
dispatcherhas been defined. It looks like this:
Finally, here's how we get things started:
(serve/servlet dispatcher ;; other keyword arguments here #:servlet-responder oops)
(define-values (dispatcher dispatch-url) (dispatch-rules [[(integer-arg)] #:method "get" get-homepage])) (serve/servlet dispatcher #:port 6893 #:command-line? #t #:servlet-responder oops #:launch-browser? #f #:servlet-regexp #rx"")
get-homepagelooks like this:
and rational.html is this:
(define html-mime "text/html;charset=utf-8") (define (get-homepage req n) (let ([r (/ 1 n)]) (response #:code 200 #:mime html-mime #:body (include-template "rational.html"))))
<!doctype html> <html lang="en"> <head> <title>Rational numbers are awesome</title> </head> <body> <p>Today I learned that the reciprocal of @|n| is @|r|.</p> </body> </html>
Launching the siteTo run this site, either do racket handler.rkt in the unzipped code directory, or open handler.rkt in DrRacket and click the Run button. When the site is launched (it is launched, right?), visit URLs such as /3 and /23. Or even /0. After visiting that last one, look at how Racket responds. You should see some error output from Racket, and the content of oops.html. (If you're running handler.rkt in DrRacket, you may not see the error logging.)
What happens when the fallback error handler fails?It's important that your fallback error handler be as carefully written as possible. If something goes wrong during its evaluation, you will see a stack trace. That makes sense: by specifying a fallback error handler, you're telling the Racket server that any uncaught error should be passed to it. If that function, in turn, throws an error, well, Racket's back is pressed to the wall and it won't respond terribly well. Suppose we had written
(Do you see the typo?) Now when we visit /0 (assuming you've done the change to
;; takes a URL and the exception (define (oops url ex) (log-error "[~a] ~a ~~~~> ~a" (datetime->iso8601 (now/utc)) (url->string url) (exn-message ex)) (response #:code 500 #:mine html-mime #:body (include-template "oops.html")))
oops) we get something quite unpleasant on the eyes:
Lesson: make sure you double check your fallback error handler.
<html><head><title>Servlet Error</title><link rel="stylesheet" href="/error.css"/></head><body><div class="section"><div class="title">Exception</div><p>The application raised an exception with the message:<pre>application: procedure does not expect an argument with given keyword procedure: response given keyword: #:mine arguments...: #:body "<!doctype html>\n<html lang=\"en\">\n <head>\n <title>Ouch!</title>\n </head>\n <body>\n <p>It seems that we've run into a bit of an issue here. Please check back shortly!</p>\n </body>\n</html>" #:code 500 #:mine "text/html;charset=utf-8"</pre></p><p>Stack trace:<pre><unknown procedure> at: line 1352, column 14, in file /Applications/Racket v6.9/collects/racket/private/kw.rkt <unknown procedure> at: line 342, column 33, in file /Applications/Racket v6.9/collects/racket/contract/private/arrow-higher-order.rkt select-handler/no-breaks at: line 163, column 2, in file /Applications/Racket v6.9/collects/racket/private/more-scheme.rkt <unknown procedure> at: line 58, column 2, in file /Applications/Racket v6.9/share/pkgs/web-server-lib/web-server/dispatchers/dispatch-servlets.rkt <unknown procedure> at: line 342, column 33, in file /Applications/Racket v6.9/collects/racket/contract/private/arrow-higher-order.rkt <unknown procedure> at: line 342, column 33, in file /Applications/Racket v6.9/collects/racket/contract/private/arrow-higher-order.rkt <unknown procedure> at: line 342, column 33, in file /Applications/Racket v6.9/collects/racket/contract/private/arrow-higher-order.rkt <unknown procedure> at: line 131, column 8, in file /Applications/Racket v6.9/share/pkgs/web-server-lib/web-server/private/dispatch-server-with-connect-unit.rkt </pre></p></div></body></html>
What errors aren't covered?A fallback error handler does not handler all possible errors. If you visit /abc, for instance, you do not see the results from oops. Rather, you get an HTTP 404 with this response body:
<html> <head><title>Not Found</title></head> <body bgcolor='white'> <p>The file you were looking for was not found on this server.</p> <p>Powered by <a href="http://racket-lang.org/">Racket</a></p> </body> </html>
oopsget triggered? In this case, you're trying to access a resource that doesn't exist. This is, certainly, a kind of error. But it's not the same kind of error that we're talking about here, in this guide.
Resource not founderrors are different from exceptions being raised (thrown) during the execution of code for known resources. There is a way of handling those errors in Racket, too. But enough for now; we'll learn about that kind of error in a different tutorial.