Server-side cookie processing in Racket

Cookies are little bits of data that a web server and a client exchange with one another. Roughly speaking, the server throws the cookies, the browser catches them, and throws them back.

In a bit more detail:

From the server’s point of view, cookies in an incoming request (if there are any) are simple key-value pairs. The server may use this information as part of processing the request. In turn, the server may include cookies, in its response. The cookies that the server includes in its response can be understood as instructions to the user agent: 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 that This 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, by theme I 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:

(Some of these options are, um, not so easy on the eyes.)

One of these themes, black-on-white, will be the default. That means that an incoming request that does not specify, in its cookies, which theme to use will be trated as though black-on-white were specified.

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
}

Since the content behind the theme.css resource is dynamic (depends on the value of the theme cookie), a function will do nicely. We’ll use the excellent css-expr package:

(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])))

(There’s no check here that the values inserted here for 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))

We use byte strings (and not simply strings) because byte strings are the values that are really stored in the key and value pairs in cookies. Indeed, that’s what 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))

We are using 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

To deal with the Location header, we need to extract the Referer header. (Yes, it should be 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)))

Now we’re in a position to extract the referrer:

;; 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))))

(We are using the 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]))

And here’s how we launch the whole thing:

(module+ main
  (serve/servlet
   go
   #:port our-port
   #:command-line? #f
   #:launch-browser? #f
   #:servlet-regexp #rx""
   #:servlet-responder oops))

The 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

Visit http://localhost:6994/ to get started.

Takeaway

Cookies are a powerful technique for passing state between the server and the client. Here, we dealt with a fairly simple cookie scenario: the server, in this example, was able deal with requests without having to rely on other systems (e.g., Redis, memcached, some other cache/database).