Server-side cookie processing in Racket
please add this name-value pair to your internal DB. Cookies are a simple but powerful way of maintaining state using HTTP. Which is just fancy way of saying that cookies are a way of making sure that a browser (AKA user agent) and the server agree about the values that certain variables should have. Cookies aren’t the only way of maintaining state (or, if you prefer, passing variables and values around). One can include little bits of information in the URL, for example. Here’s a simple tutorial to illustrate how to get started using cookies in your Racket-based web applications.
Wrinkles with cookies
Before digging in to how to work with cookies in your Racket web apps, a word or two is in order about whether you will see these cookies at all. If you’ve looked at the preferences/settings for your browser, chances are good that you’ve discovered a setting where cookies can be disabled. If cookies are disabled, from the server point of view, this means that you won’t receive any cookies at all. It’s game over. You may be able to detect that and take countermeasures, or at least fail gracefully. But the countermeasures may be too nettlesome to implement. If you’ve ever seen notices on web sites saying something to the effect thatThis site requires that cookies be enabled, now you know why. You may also have seen a way of cleaning out cookies. This amounts to trashing the browser’s cookies database. After doing that, then, the next time that somone connects to your server it will appear that this is their first time visiting. Any state that you’ve previously relied upon will be missing, and your server may need to fall back to an initial state. For instance, users may need to sign in again.
Demonstration: Theme switcher
Let’s build a simple web site that allows its users to choose the color theme that they want to use. To keep things simple, bythemeI mean a combination of a background color and a text color. Of course, themes can be a lot more involved than hat, but let’s keep things simple. There will be a predefined list of background-text combinations on offer; users choose which one they want to use from a drop-down menu. Here are the options:
- black on white
- white on black
- red on white
- black on greed
- yellow on red
Implementation
Our little demonstration has a number of moving parts. Let’s imagine a server that offers the following resources:- root where a drop-down menu of possible themes is presented
- an HTML presentation of, well, very little. Contains the theme switcher in the guise of a form consising of a single drop-down menu and submit button.
- hi
- another page to demonstrate that the theme really does switch
- style.css
- same as the CSS behind this genius website.
- theme.css
- CSS that reflects the current theme.
- change-theme
- a resource to that changes the color theme. One submits a POST reqest to this resource specifying the intended theme. Redirects to the resource from which this reousrce was called.
The themes
In the file style.css we have the following bare-bones CSS:body {
margin:40px auto;
max-width:650px;
line-height:1.6;
font-size:18px;
color:#444;
padding:0 10px
}
(define (make-theme background-color text-color)
(define bg/symbol (string->symbol background-color))
(define text/symbol (string->symbol text-color))
(css-expr->css
(css-expr
[body
#:background-color ,bg/symbol
#:color text/symbol])))
background-color
and color
are legal CSS values.)
Later we will see how we use this function when it comes time to generate an HTTP response for theme.css.
Extracting the theme from a request
One of our core tasks, in this tutorial, is to identify, given a request, which theme it spcifies in its cookie, if any. We’ll use#f
to indicate that no theme can be found. For this web site, we will adopt the convention that we will always look for the theme under the key theme
:
(define theme-key
"theme")
(define theme-key/bytes
(string->bytes/utf-8 theme-key))
make-cookie
expects.
Now we can extract the cookie from an incoming request (or detect that there was no cookie):
; request? -> #f | bytes?
(define (extract-theme req)
(define data (request-bindings/raw req))
(define b (bindings-assq theme-key/bytes data))
(if (binding:form? b)
(binding:form-value b)
#f))
request-bindings/raw
and bindings-assq
to dive into the header of the request and extract the theme cookie.
Changing the theme
Changing the theme is the most complex piece of the whole puzzle here. Not so much because the code is hard, but because the interplay between the server and the client involves a bit of juggling. The idea is that the change-theme resource (which we haven’t yet defined) is responsible for taking input specifying what the desired theme is, and then generating a suitable response. In the response, cookies are set, so that in all future requests from the browser, the intended themm will be submitted. The challenge here is a bit subtle, so put on your thinking glasses for a bit. The change-state resource is playing a bit of ping pong. It receives a POST request. It then sends the browser right back where it came from, by returning a redirect response with a Location header. But it is not a no-op, as it may sound like. In the response, a Set-Cookie header will be present. Upon processing the response with the Set-Cookie header, this is where the state of the browser is changed. Moreover, in this approach, nothing changes on the server.Extracting the referrer
When change-theme receives a POST request, its job is to generate a redirect response. The two key pieces of information it needs to deliver in the response are- a Set-Cookie header, indicating what theme is to be used, and
- a mono{Location} header, indicating where the browser should go to
referrer.) That should be a string. But we actually receive bytes as input, so let’s make sure we can safely deal with byte strings that aren’t really UTF strings:
;; bytes? -> (or #f string?)
(define (bytes->string bstr)
(define (fail err) #f)
(with-handlers ([exn:fail:contract? fail])
(bytes->string/utf-8 bstr)))
;; request? string? -> string?
(define (referrer req fallback)
(unless (string? fallback)
(error "Fallback should be a string:" fallback))
(unless (request? req)
(error "First argument should be a request? value:" req))
(define r (headers-assq* #"Referer" (request-headers/raw req)))
(cond ((bytes? r)
(let ([str (bytes->string r)])
(if (string? str)
str
fallback)))
(else
fallback)))
Generating the response
All the pieces are now in place to generate the complete response: we can extract the theme (or detect that no theme was supplied, or that an illegal value was given), and we know where to redirect.;; request? -> response?ŧ
(define (change-theme req)
(define ref (referrer req "/"))
(define t (extract-theme req))
(define theme
(cond ((and (bytes? t)
(known-theme? t))
t)
((bytes? t)
(log-error "Unknown theme \"~a\". Using the fallback." t)
default-theme/bytes)
(else
(log-error "Theme not found; using the fallback.")
default-theme/bytes)))
(respond #:code 303
#:headers (list (cons 'Location ref))
#:cookies (list (make-cookie theme-key/bytes theme))))
respond
function to conveniently generate HTTP responses. It’s included in the demo code for this tutorial.)
The dispatcher
Here, at last, is the dispatcher for this little demonostration:(define-values (go url-generator)
(dispatch-rules
[("") #:method "get" view-homepage]
[("style.css") #:method "get" get-style]
[("theme.css") #:method "get" get-theme]
[("change-theme") #:method "post" change-theme]
[else not-found]))
(module+ main
(serve/servlet
go
#:port our-port
#:command-line? #f
#:launch-browser? #f
#:servlet-regexp #rx""
#:servlet-responder oops))
not-found
function is defined in respond.rkt, which defines respond
and a couple of other convenience functions. The fallback error handler function oops
are defined as so:
(define (oops url err)
;; nothing fancy, just say that something went wrong
;; after logging the error
(log-error (format "~a" err))
(respond #:code 500
#:body "Oops, something went wrong."))
How to run
Download the code here. Run cookie.rkt in DrRacket or in the command line, like so:$ racket cookie.rkt