How to generate a Location header

A common interaction pattern in web browsers and HTTP APIs is that you submit some data in your request, the server processes things, creates a new entity in the background, and then it tells you where to find that newly created entity. It tells you where to find it by including a Location header in the HTTP response; the value of that header is a URL.

How can you do that in Racket? It may depend on how, precisely, you’ve got things running. If you’re using URL-based dispatching with dispatch-rules, though, the solution is right under your nose. Here’s a tutorial where we generate random greetings in various languages, and return a Location header where we indicate how to get a non-random greeting.

Generating URLs with dispatch-rules

Download the code for this demonstration here (for location.rkt) and here (for respond.rkt).

dispatch-rules takes as input a description of a family of URLs—a kind of mini-DSL—and returns two values.

The first one is the one you’re probably thinking of: a servlet that works as a kind of cond, matching an incoming URL and sending the request to the first matching subservlet.

The second return value is a URL generator. It’s this value that does he job we’re looking to have done when we’re asking for Location URLs. The second return value can be thought of as a kind of inverse of the first value. The first value takes URLs as input and decides which servlet should be used. The second value takes a servlet and its arguments and generates the URL.

Here’s a simple example illustrating how URL generators can be used to make location headers. It’s a simple web application that generates greetings in several languages: English, German, Spanish, Portuguese, Japanese, and Arabic. When we visit /hello, a random language will be selected and we’ll receive a plain text response whose body is Hello in that language.

The response will also contain a Location header that shows you how to get this greeting in a non-random way. Thus, if, by chance, we get the Arabic greeting, the response will contain a Location header that says where one can always get the Arabic greeting. When we visit that URL, the greeting is not selected randomly. It’s always Arabic.

Implementation

We’ll start from the end—the dispatcher—and explain as we go along the various bits that make it up. Here’s where we’re headed:

(define-values (dispatcher url-generator)
  (dispatch-rules
   [("hello")
    #:method "get"
    hello]
   [("hello" (string-arg))
    #:method "get"
    hello+lang]
   [("hello")
    #:method (regexp ".*")
    method-not-allowed]
   [else
    not-found]))

The not-found and method-not-allowed servlets are not especially interesting; they are fallbacks to deal with weird incoming requests (specifically, unknown URLs and HTTP methods other than GET).

Let’s get started with the most basic data here. The list of languages is an immutable hash table:

(define greetings/hash
  (hash "en" "Hello!"
        "de" "Hallo!"
        "es" "¡Hola!"
        "pt" "Ola!"
        "jp" "こんにちは"
        "ar" "مرحب"))

We will later need to have all the languages in a list, and their number:

;; all available languages
(define languages (hash-keys greetings/hash))

;; the number of available language
(define num-languages (hash-count languages))

To generate a random greeting, we first select a random language and then use it as a key into the greetings/hash table:

;; -> string?
(define (random-language) (list-ref languages (random num-languages)))

If someone requests, say, /hello/ar, we should generate the Arabic greeting. There’s no randomness here, and no Location header. The servlet responsible for this, hello+lang, looks like this:

;; request? -> response?
(define (hello+lang req lang) (define greeting (hash-ref greetings/hash lang #f)) (cond ((string? greeting) (respond #:code 200 #:mime "text/plain;charset=UTF-8" #:body greeting)) (else (not-found))))

(Notice that we check whether the given language code makes sense. If someone requests /hello/12345, they’re going to get a Not Found response.)

What about random greetings? That’s the interesting bit. In this case, it’s not difficult to imagine how this will work: we pick a random language, and emit the greeting in that language. That’s fine, but how to we generate the Location header for the response? Here, at long last, we’ll use url-generator, the second return value coming from dispatch-rules.

(define (hello req)
  (define lang (random-language))
  (define greeting (hash-ref greetings/hash lang))
  (define loc (url-generator hello+lang lang))
  (respond #:code 200
           #:mime "text/plain;charset=UTF-8"
           #:headers (list (cons 'Location loc))
           #:body greeting))

Notice the call to url-generator. Its arguments are the servlet and the argument—here, the randomly generated language—for that servlet.

How does this work? Isn’t it weird that a servlet is being used as the argument of the URL generator?

Think of it this way: the big dispatch table with which dispatch-rules was initially called is consulted, where we look up which servlet hello+lang corresponds to; then, we know what the URL should look like, given a language (it’s one of the placeholder arguments). That’s how you make the Location URLs for our languages.