Functionally rewriting HTTP requests and responses with struct-copy
struct-copy. This function takes, say, an HTTP response as input and produces a copy of it, with some details changed.
Example: HEAD requestsTo 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.
ImplementationHere's how you can implement that idea in Racket. We need a few ingredients to pull this off:
- a function that takes a request and changes its method from HEAD to GET
- a function that takes a response and throws away its body
corefunction that works with requests in the normal way (that is, does no further rewriting shenanigans)
wrapperfunction that takes a request, perhaps transforms it, and passes the transformed request to the core function.
HEAD to GETThis function does the job:
The idea here is to take a request as input and change its method to GET. We use
;; request? -> request? (define (head->get req) (struct-copy request req [method #"GET"]))
struct-copyto achieve this. This makes sense because requests in Racket are structures.
Throw away the bodyThis function does the job:
;; response? -> response? (define (strip-body resp) (struct-copy response resp [output write-nothing]))
write-nothingis the function
;; output-port? -> exact-nonnegative-integer? (define (write-nothing port) (write-bytes #"" port))
write-nothingtakes a port as input and, well, writes the empty (byte) string to it. The
gotchahere 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
Core functionAt 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 functionThe 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.
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
;; request? -> response? (define (start req) (if (bytes=? #"HEAD" (request-method req)) (strip-body (dispatcher (head->get req))) (dispatcher req)))
- fake a GET request,
- pass it along to dispatcher, and
- throw away whatever response body comes back from dispatcher.
Putting it all togetherYou can download a self-contained demonstration of all this here. Running
head.rktin the zip file will launch a server running on port 6893. Run the server in DrRacket or in the console by running
$ racket head.rktAs 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))))
respondfunction is coming from the
respond.rktfile included in the demo zip file.)
TakeawayThe 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
methodfield of an HTTP request and the
outputfield of an HTTP response).
struct-copyhelps us to program in a pleasantly functional way while making great web sites with Racket.