Functionally rewriting HTTP requests and responses with struct-copy

A common task for many web sites is to rewrite HTTP requests and responses. In request rewriting, one receives an HTTP request, tweaks it in some way, and passes the manipulated request on to another party. Response rewriting is similar: one receives an HTTP response, manipulates it somehow, and then passes that along to another party who is looking for a response.

With Racket, since requests and responses are structures (request, response), a straightforward way to accomplish rewriting is to use struct-copy. This function takes, say, an HTTP response as input and produces a copy of it, with some details changed.

Example: HEAD requests

To illustrate, here’s how one can implement HEAD requests using the Racket web server.

The aim of an HTTP HEAD request is more or less identical to GET, with one proviso: no body should be returned. HEAD requests can be issued to check whether an resource has changed (one would look at ETags in the header, or perhaps Last-Modified), or how big it is (by looking at the Content-Type) flag. HEAD may also serve as a kind of probe: is the resource even there at all?

A natural way of implementing HEAD is to take the request as input, rewrite its HTTP method from HEAD to GET, pass along that request, and then throw away the response body.

Implementation

Here’s how you can implement that idea in Racket. We need a few ingredients to pull this off:

Let’s take care of these tasks one at a time.

HEAD to GET

This function does the job:

;; request? -> request?
(define (head->get req) (struct-copy request req [method #"GET"]))

The idea here is to take a request as input and change its method to GET. We use struct-copy to achieve this. This makes sense because requests in Racket are structures.

Throw away the body

This function does the job:

;; response? -> response?
(define (strip-body resp) (struct-copy response resp [output write-nothing]))

where write-nothing is the function

;; output-port? -> exact-nonnegative-integer?
(define (write-nothing port) (write-bytes #"" port))

write-nothing takes a port as input and, well, writes the empty (byte) string to it.

The gotcha here is that, for Racket responses, the body is a function. It’s not simply, say, a (byte) string. That’s why write-nothing—a function—is the value stored in the output field.

Core function

At this point, the code responder function can be whatever you want. The mantra to keep in mind is: request as argument, response as value.

(If you’re looking for a bit of starter code, a concrete example can be found below.)

Wrapper function

The wrapper function (for lack of a better word) is responsible for taking the original request as input, possibly changing some details, and passing the possibly modified request along to the core responder.

;; request? -> response?
(define (start req) (if (bytes=? #"HEAD" (request-method req)) (strip-body (dispatcher (head->get req))) (dispatcher req)))

Notice that dispatcher gets used in either case. In case the request method is not HEAD, we simply invoke the dispatcher directly. If we do get a HEAD request, we

  1. fake a GET request,
  2. pass it along to dispatcher, and
  3. throw away whatever response body comes back from dispatcher.

Putting it all together

You can download a self-contained demonstration of all this here. Running head.rkt in the zip file will launch a server running on port 6893. Run the server in DrRacket or in the console by running

$ racket head.rkt

As we discussed, it is up to you to determine how to handle complete responses. You’re welcome to modify the example that I’ve included in the demo, which is a very simple function that essentially just prints what you requested, and how:

;; request? -> response?
(define (dispatcher req) (respond #:mime "text/plain" #:body (format "You requested \"~a\" using the ~a method." (url->string (request-uri req)) (request-method req))))

(The respond function is coming from the respond.rkt file included in the demo zip file.)

Takeaway

The driver of all this is struct-copy. It is a simple but powerful tool for generating new structures (in our case, HTTP requests and responses) whose fields are copied from a given structure, with specified fields assigned specified valued (in our case, the method field of an HTTP request and the output field of an HTTP response). struct-copy helps us to program in a pleasantly functional way while making great web sites with Racket.