July 26, 2019 by Lucian Mogosanu

This post is part of a series on Common Lisp WWWism, more specifically a continuation of ongoing work to understand the web server known as Hunchentoot and, as a result, produce a signed genesis to be used by members of the TMSR WoT.

In this episode we'll do another illustration of the Hunchentoot architecture; and we'll have fun documenting a running web server instance, thusly exploring for now a few of things that it can do1.

First, the architectural diagram, with bells, whistles and clickable stuff:

No, really, I'm not kidding: if you have a browser that implements SVG, clicking on the text should direct you to defgeneric pieces of code2. Anyway, the big squares are Hunchentoot components and, more specifically, the name of their CL classes, while the small squares found inside the big ones represent methods specializing on a given class. The green boxes are user actionable or defineable methods, so this is where you should start looking; while the arrows denote the "X calls Y" relation, with the exception of the dashed green arrow, that tells us that header-out is in fact a setf-able accessor used from somewhere within the context of acceptor-dispatch-request (e.g. from a request handler) to read and modify the header of a reply.

Now from this airplane view, Hunchentoot's organization looks quite digestible, which should give us a very good idea of how to start using it. So let's take a look at that, shall we?

Assuming we've loaded3 Hunchentoot into our CLtron of choice, we can now create an acceptor instance and start it:

> (defvar *myaccept* (make-instance 'hunchentoot:acceptor :port 8052)) > (hunchentoot:start *myaccept*)

... and now what? Say, for now, that we want to serve a static site -- I'm using The Tar Pit as my playground, but you can use whatever you fancy. Looking at acceptor-dispatch-request, we notice that it calls handle-static-file with the document-root as an argument. So let's set that, and additionally the error template directory, to our site:

(setf (hunchentoot:acceptor-document-root *myaccept*) "/home/spyked/thetarpit/site/" (hunchentoot:acceptor-error-template-directory *myaccept*) "/home/spyked/thetarpit/site/")

and now curl http://localhost:8052 should serve its contents.

But let's say we want to go one step further and serve some content (server-side) dynamically. The original Hunchentoot documentation actually provides a neat minimal example, which I'm going to steal, but not before explaining what we're going to do here.

Besides serving files off the disk, a web server can do other useful stuff, such as, in Apache's case, sending the file to a preprocessing engine (PHP or whatever), or as we're going to show, executing some other predefined action that depends on the request parameters (URL, cookies, HTTP method, variables and so on). For now, let's say that we want our server to respond to the URL "/yo" (where "/" is the site root) with the plain-text message "Hey!". Furthermore, let's say that we want to optionally parameterize requests to this URL by the variable "name", in which case the response will include the name: for example, if we do a GET request to "/yo?name=spyked", we want the server to respond with "Hey, spyked!".

We have a few possible ways of doing this. We could for example edit the current implementation of acceptor-dispatch-request, which is also the ugliest possible approach. On the other hand, Hunchentoot is built using Common Lisp's Object System mechanism (CLOS), which allows us to subclass the acceptor to a user-defined class and specialize the method above for our class. Let's try this out:

(defclass myacceptor (hunchentoot:acceptor) ()) (change-class *myaccept* 'myacceptor)

The change-class thing isn't something that we'd normally do, but if you've been following along, you'll notice that this didn't break our code, because well, Common Lisp is cool. Now for the dispatcher method:

(defmethod hunchentoot:acceptor-dispatch-request ((acceptor myacceptor) request) (cond ((string= (hunchentoot:script-name request) "/yo") (let ((name (cdr (assoc "name" (hunchentoot:get-parameters request) :test #'string=)))) (setf (hunchentoot:content-type*) "text/plain") (format nil "Hey~@[, ~A~]!" name))) (t (call-next-method))))

In human words: this is an implementation of acceptor-dispatch-request specialized on myacceptor, that, upon encountering the URL (script-name) "/yo", takes the value of the GET parameter known as "name" and returns a response string (possibly containing this "name") as plain text. Otherwise it transfers control to the "next most specific method"4, implicitly passing to it the existing arguments.

We could stop here, but we won't, as there's a short discussion to be had, mainly related to the extensibility of our approach, i.e. what happens when we add other custom URLs to this recipe? The naive result will look ugly and will be a pain to maintain and debug; while the more elaborate approach, involving putting every "/yo" into its own function, will initially fill our implementation with cond/case conditions, eventually leading to a more civilized dispatch mechanism, in the form of a lookup table from URLs to handler functions.

Well, it so happens that Hunchentoot already has an implementation for this type of thing, going under the name of easy-acceptor. easy-acceptor defines a dispatch table whose only dispatcher is (initially) the dispatch-easy-handlers function, which looks up handlers for URLs in a global handler list, easy-handler-alist. As things usually go with these domain-specific languages, most of the handler maintenance work is piled up in the define-easy-handler macro.

So, in order to illustrate this easy-stuff, first let's undo some of our previous work and redo the very basics:

(hunchentoot:stop *myaccept*) (setq *myaccept* (make-instance 'hunchentoot:easy-acceptor :port 8052 :document-root "/home/spyked/thetarpit/site/" :error-template-directory "/home/spyked/thetarpit/site/")) (hunchentoot:start *myaccept*)

Notice how now we're instancing easy-acceptor. Now we can define an equivalent "easy handler" for our previous "/yo" work:

(hunchentoot:define-easy-handler (say-yo :uri "/yo") (name) (setf (hunchentoot:content-type*) "text/plain") (format nil "Hey~@[, ~A~]!" name))

which about sums up our exercise. Initially I had wanted to show an example doing some fancy prefix/"smart" URL lookup à la MP-WP, but by now this post is so large5 that it can't be eaten in one sitting. Alas, I will have to leave all my fancy examples for another episode. Thus, until next time...

Update, July 27: comments on this post are listed in footnote 6.

Filed under:

RSS 2.0 feed. Comment. Send trackback.