Production vs. development environments for the Racket HTTP server
The site isn’t live…yet
We all know that when we’re working on a web site we might write some code that’s, well, not so pretty.
It might contain errors. Maybe it just does the wrong thing. We work on the site to our satisfaction, then we push it live.
When we’re developing, we may want our site to behave mostly the same as it would if it were live, but with some small (but crucial) differences. For instance:
- If you’re connecting to a database, probably you’re connecting to a different database than you could connect to were you in a live environment.
- If you’ve got an ecommerce site, you can use your site to buy things, even while developing it…but not using real credit cards, actual money.
- Debug output is useful when you’re developing…but not live. (Or: your debug output needs to go somewhere else.)
The problem isn’t necessarily either/or. One can imagine using a few different modes: production, testing/staging, and development. In development, we can let it all hang out. Testing is a halfway point between production and testing where, say, one can access a kinda-real-kinda-not test database. And so on.
To be specific, consider the following problem. When our site is live and we encounter an error internally, we want to delivery a simple
sorry an error occurred HTML page and log the error behind the scenes. When the site is not live (or, in other words, when the site is in development) and a program error occurs, we want to see a stack trace, presented in HTML.
How can you do all this with the Racket web server?
Approaches to the problem
Before digging in to Racket, note that there are a couple different approaches to the production vs. development problem. One can use:
- environment variables
- presence/absence of a file on the server
- file has/does not have certain content
Let’s implement the first one, a common approach.
The first step is to define a function that determines whether our site is in production mode, and we define a variable whose value is the value of this function. Here’s how you might accomplish that. (You’re welcome to download the source code.). Here is the very simple environment variable checking, site-mode.rkt:
(define production-env-variable "SITE_MODE")
(define production-env-value "production")
(define (production?) (let ([current (getenv production-env-variable)]) (and (string? current) (string=? current production-env-value))))
(define site-is-production? (production?))
The purpose of this code is to define a single variable,
site-is-production?, whose value is #t if and only if
- the variable SITE_MODE is present in the current environment, and
- the value of this environment variable is (exactly)
If either of these conditions fails, the site is not in production mode.
One thing to keep in mind here is that the value of the variable is set when Racket starts, and doesn’t change. If one wants to switch off production mode, one would have to kill the server, make a suitable change in the environment, and start the server from scratch. There’s no provision, in this approach, for changing the server mode as the server is running. (Which is not to say that that’s a bad idea.)
With this basis, let’s see how we can use the information about whether the site is in production mode. We will largely follow the example given in another tutorial, where the server is a toy example about rational numbers. Watch how
site-is-production? gets used.
The idea here is to make a server that, when in production mode, gracefully handles all errors by giving an HTTP 500 response with a little HTML apologizing for the error. Outside of production mode, we want to use the standard Racket error handler, which pretty prints (in HTML) the exception that was raised and logs the error, too.
(require web-server/dispatch web-server/servlet-env web-server/templates web-server/http/request-structs web-server/configuration/responders)
;; the next two files are provided in the accompanying
;; zip file -- did you download it?
(require (file "response.rkt"))
(require (file "site-mode.rkt"))
(define html-mime "text/html;charset=utf-8")
(define error-responder (make-parameter #f))
(define (get-homepage req n) (let ([r (/ 1 n)]) (response #:code 200 #:mime html-mime #:body (include-template "rational.html"))))
(define (fallback url exn) (cond (site-is-production? (response #:code 500 #:mime html-mime #:body (include-template "oops.html"))) (#t (let ([handler (error-responder)]) (handler url exn)))))
(define-values (dispatcher dispatch-url) (dispatch-rules [[(integer-arg)] #:method "get" get-homepage]))
(module+ main (parameterize ([error-responder servlet-error-responder]) (serve/servlet dispatcher #:port 6893 #:servlet-responder fallback #:launch-browser? #f #:servlet-regexp #rx"")))
The main idea is that, using giving a value for
serve/servlet, we are providing our own fallback error handler.
The secret sauce here (if you will) is the
parameterize bit (which goes along with the
make-parameter near the top).
We are making a kind of backup:
servlet-error-responder is the standard Racket error handler, the one that generates the pretty-printed exception and logs stack traces to the console. Our goal is to use that error handler, as long as we’re not in production. The idea, then, is to save that value (under the parameter
error-responder). If an error occurs, we check: are we in production mode? If so, we present a minimal HTML page as our response. If not, we can barf up the ugly stack trace. That’s what you see in the implementation of
fallback, which is our fallback error handler.
Of course, much more is possible. The main idea is to have some kind of mechanism for deciding whether we are in production mode or not. Once you’ve decided on a strategy for making that determination (here, we used environment variables), you’re good to go.
We used Racket’s parameters as a way to save a value before overriding it. Parameters could also be used to store the
production-ness of a site itself; that’s an option we didn’t explore here and may be necessary, depending on exactly how you’d like to implement your production-or-not strategy.