HUNCHENTOOT - The Common Lisp web server formerly known as TBNL


 

Abstract

Hunchentoot is a web server written in Common Lisp and at the same time a toolkit for building dynamic websites. As a stand-alone web server, Hunchentoot is capable of HTTP/1.1 chunking (both directions), persistent connections (keep-alive), and SSL, but it can also sit behind the popular Apache using Marc Battyani's mod_lisp.

Hunchentoot provides facilities like automatic session handling (with and without cookies), logging (to Apache's log files or to a file in the file system), customizable error handling, and easy access to GET and POST parameters sent by the client. It does not include functionality to programmatically generate HTML output. For this task you can use any library you like, e.g. (shameless self-plug) CL-WHO or HTML-TEMPLATE.

Hunchentoot talks with its front-end or with the client over TCP/IP sockets and uses multiprocessing to handle several requests at the same time. Therefore, it cannot be implemented completely in portable Common Lisp. It currently works with LispWorks (which is the main development and testing platform), CMUCL (with MP support), SBCL (with Unicode and thread support), OpenMCL, and Allegro Common Lisp. Porting to other CL implementations shouldn't be too hard, see the files port-xxx.lisp and unix-xxx.lisp which comprise all the implementation-specific code.

Hunchentoot comes with a BSD-style license so you can basically do with it whatever you want.

Hunchentoot is used by Jalat, Heike Stephan, Makewavs, and ERGO.

Download shortcut: http://weitz.de/files/hunchentoot.tar.gz.


 

Contents

  1. Download and installation
    1. Hunchentoot behind a proxy
    2. Hunchentoot behind mod_lisp
  2. Support and mailing lists
  3. Examples and tutorials
  4. Function and variable reference
    1. Servers
    2. Handlers
    3. Requests
    4. Replies
    5. Cookies
    6. Sessions
    7. Logging and error handling
    8. Debugging Hunchentoot applications
    9. Miscellaneous
  5. History
  6. Symbol index
  7. Acknowledgements

 

Download and installation

Hunchentoot depends on a couple of other Lisp libraries which you'll need to install first: Make sure to use the newest versions of all of these libraries (which might themselves depend on other libraries)!

The preferred method to compile and load Hunchentoot is via ASDF.

Hunchentoot together with this documentation can be downloaded from http://weitz.de/files/hunchentoot.tar.gz. The current version is 0.6.1. There's also a port for Gentoo Linux thanks to Matthew Kennedy

Hunchentoot behind a proxy

If you're feeling unsecure about exposing Hunchentoot to the wild, wild Internet or if your Lisp web application is part of a larger website, you can hide it behind a proxy server. One approach that I have used several times is to employ Apache's mod_proxy module with a configuration that looks like this:
ProxyPass /lisp http://127.0.0.1:3000/lisp
ProxyPassReverse /lisp http://127.0.0.1:3000/lisp
This will tunnel all requests where the URI path begins with "/lisp" to a (Hunchentoot) server listening on port 3000 on the same machine.

Of course, there are several other (more lightweight) web proxies that you could use instead of Apache.

Hunchentoot behind mod_lisp

You can also couple Hunchentoot more tightly with Apache using mod_lisp. In this case, Apache will not send proxy requests to Hunchentoot, but communicate with it directly using a simple, line-based protocol. The downside of this approach is that it makes debugging harder. (Also, with mod_lisp, you can't accept request bodies that use chunked encoding. With the usual web browsers, this shouldn't be a problem, though.)

For this setup you need two things:

Then you will have to configure Apache and mod_lisp to make them aware of Hunchentoot. First, in your Apache configuration file (usually called httpd.conf) add these lines
LispServer 127.0.0.1 3000 "foo"

<Location /hunchentoot>
  SetHandler lisp-handler
</Location>
and afterwards restart Apache. This informs mod_lisp that there's a Lisp listening on port 3000 and named "foo" - you can of course use any other name or port or even put Hunchentoot on another physical machine. (In the latter case you'll have to replace 127.0.0.1 with the FQDN or IP address of this machine.)

The Location/SetHandler part means that every URL which starts with /hunchentoot will be handled by mod_lisp (and thus Hunchentoot) on this server. (Again, you can of course use other locations. See the Apache documentation for things like virtual hosts or directives like LocationMatch.)

To interface a Hunchentoot server with mod_lisp, you must start it with the :MOD-LISP-P keyword parameter of START-SERVER set to a true value.
 

Support and mailing lists

For questions, bug reports, feature requests, improvements, or patches please use the tbnl-devel mailing list. If you want to be notified about future releases subscribe to the tbnl-announce mailing list. These mailing lists were made available thanks to the services of common-lisp.net. You can search the devel mailing list here (thanks to Tiarn?n ? Corr?in).
 

Examples and tutorials

Hunchentoot comes with an example website which you can use to see if it works and which should also demonstrate a couple of the things you can do with Hunchentoot. Use it as a kind of "Hello World" code to get yourself started.

To run the example, enter the following code into your listener:

(asdf:oos 'asdf:load-op :hunchentoot-test)
(hunchentoot:start-server :port 4242)
You should now be able to point your browser at http://localhost:4242/hunchentoot/test and see something.

A tutorial for (an older version of) Hunchentoot can be found at http://www.jalat.com/blogs/lisp?id=3 thanks to Asbjørn Bjørnstad. And there's a TBNL tutorial from Frank Buss. Hunchentoot is not TBNL, but the two are similar enough to make the tutorial worthwhile.

For Win32, Bill Clementson explains how to set up Hunchentoot's predecessor TBNL with Apache/mod_lisp in his blog entry at http://bc.tech.coop/blog/041105.html. See also http://bc.tech.coop/blog/061013.html.
 

Function and variable reference

Servers

If you want Hunchentoot to actually do something, you have to start a server. You can also run several servers in one image, each one listening to a different port.


[Function]
start-server &key port address mod-lisp-p use-apache-log-p input-chunking-p read-timeout write-timeout setuid setgid ssl-certificate-file ssl-privatekey-file ssl-privatekey-password => server


Starts a Hunchentoot server instance and returns it. port ist the port the server will be listening on - the default is 80 (or 443 if SSL information is provided). If address is a string denoting an IP address, then the server only receives connections for that address. This must be one of the addresses associated with the machine and allowed values are host names such as "www.zappa.com" and address strings such as "72.3.247.29". If address is NIL, then the server will receive connections to all IP addresses on the machine. This is the default.

If mod-lisp-p is true (the default is NIL), the server will act as a back-end for mod_lisp, otherwise it will be a stand-alone web server. If use-apache-log-p is true (which is the default), log messages will be written to the Apache log file - this parameter has no effect if mod-lisp-p is NIL.

If input-chunking-p is true (which is the default), the server will accept request bodies without a Content-Length header if the client uses chunked transfer encoding. If you want to use this feature behind mod_lisp, you should make sure that your combination of Apache and mod_lisp can cope with that.

read-timeout is the read timeout (in seconds) for the socket stream used by the server - the default value is *DEFAULT-READ-TIMEOUT*. This parameter is ignored on OpenMCL and AllegroCL. write-timeout is the write timeout (in seconds) for the socket stream used by the server - the default value is *DEFAULT-WRITE-TIMEOUT*. This parameter is ignored on all implementations except for LispWorks 5.0 or higher. You can use NIL in both cases to denote that you don't want a timeout. If mod-lisp-p is true, the timeouts are always set to NIL.

On Unix you can use setuid and setgid to change the UID and GID of the process directly after the server has been started. (You might want to do this if you're using a privileged port like 80.) setuid and setgid can be integers (the actual IDs) or strings (for the user and group name respectively).

If you want your server to use SSL, you must provide the pathname designator(s) ssl-certificate-file for the certificate file and optionally ssl-privatekey-file for the private key file, both files must be in PEM format. If you only provide the value for ssl-certificate-file it is assumed that both the certificate and the private key are in one file. If your private key needs a password you can provide it through the ssl-privatekey-password keyword argument. If you don't use LispWorks, the private key must not be associated with a password, and the certificate and the private key must be in separate files.


[Function]
stop-server server => |


Stops a server started with START-SERVER. server must be an object as returned by START-SERVER.


[Special variable]
*server*


During the execution of dispatch functions and handlers this variable is bound to the server object (as returned by START-SERVER) which processes the request.


[Readers]
server-local-port server => port
server-address server => address


These methods can be used to query a Hunchentoot server object. The values correspond to the port and address parameters of START-SERVER.


[Special variable]
*default-read-timeout*


The default value for the read-timeout keyword argument to START-SERVER. The initial value is 20 (seconds).


[Special variable]
*default-write-timeout*


The default value for the write-timeout keyword argument to START-SERVER. The initial value is 20 (seconds).


[Special variable]
*cleanup-interval*


Should be NIL or a positive integer. The system calls *CLEANUP-FUNCTION* whenever *CLEANUP-INTERVAL* new worker threads have been created unless the value is NIL. The initial value is 100.


[Special variable]
*cleanup-function*


The function (with no arguments) which is called if *CLEANUP-INTERVAL* is not NIL. The initial value is a function which calls (HCL:MARK-AND-SWEEP 2) on LispWorks and does nothing on other Lisps.

On LispWorks this is necessary because each worker (which is created to handle an incoming http request and which dies afterwards unless the connection is persistent) is a Lisp process and LispWorks creates processes in generation 2.

Note that you can also set this value to NIL and tune LispWork's GC yourself, using for example COLLECT-GENERATION-2.

Handlers

Hunchentoot handles each incoming request dynamically depending on the contents of a global dispatch table. The details can be found below. (See the file test/test.lisp for examples.)


[Special variable]
*dispatch-table*


This is a list of function designators for dispatch functions each of which should be a function of one argument which accepts a REQUEST object and, depending on this object, should either return a handler to handle the request or NIL which means that the next dispatcher will be queried. A handler is a designator for a function with no arguments which usually returns a string or an array of octets to be sent to the client as the body of the http reply. (Note that if you use symbols as function designators, you can redefine your handler functions without the need to change the dispatch functions.) See the section about replies for more about what handlers can do.

The dispatchers in *DISPATCH-TABLE* are tried in turn until one of them returns a handler. If this doesn't happen, Hunchentoot will return a 404 status code (Not Found) to the client.

The default value of *DISPATCH-TABLE* is a list which just contains the symbol DEFAULT-DISPATCHER. See also *META-DISPATCHER*.


[Function]
default-dispatcher request => handler


This is a function which will always unconditionally return the value of *DEFAULT-HANDLER*. It is intended to be the last element of *DISPATCH-TABLE*


[Special variable]
*default-handler*


This variable holds the handler which is always returned by DEFAULT-DISPATCHER. The default value is a function which unconditonally shows a short Hunchentoot info page.


[Special variable]
*meta-dispatcher*


The value of this variable should be a function of one argument. It is called with the current Hunchentoot server instance and must return a dispatch table suitable for Hunchentoot. The initial value is a function which always unconditionally returns *DISPATCH-TABLE*.

This can obviously be used to assign different dispatch tables to different servers (and is useless if you only have one server).


[Function]
create-prefix-dispatcher prefix handler => dispatch-fn


A convenience function which will return a dispatcher that returns handler whenever the path part of the request URI starts with the string prefix.


[Function]
create-regex-dispatcher regex handler => dispatch-fn


A convenience function which will return a dispatcher that returns handler whenever the path part of the request URI matches the CL-PPCRE regular expression regex (which can be a string, an s-expression, or a scanner).


[Function]
handle-static-file path &optional content-type => nil


Sends the file denote by the pathname designator path with content type content-type to the client. Sets the necessary handlers. In particular the function employs HANDLE-IF-MODIFIED-SINCE.

If content-type is NIL the function tries to determine the correct content type from the file's suffix or falls back to "application/octet-stream" as a last resort.

Note that this function calls SEND-HEADERS internally, so after you've called it, the headers are sent and the return value of your handler is ignored.


[Function]
create-static-file-dispatcher-and-handler uri path &optional content-type => dispatch-fn


A convenience function which will return a dispatcher that dispatches to a handler which emits the file denoted by the pathname designator path with content type content-type if the SCRIPT-NAME of the request matches the string uri. Uses HANDLE-STATIC-FILE internally.

If content-type is NIL the function tries to determine the correct content type from the file's suffix or falls back to "application/octet-stream" as a last resort. *DEFAULT-CONTENT-TYPE*.


[Function]
create-folder-dispatcher-and-handler uri-prefix base-path &optional content-type => dispatch-fn


Creates and returns a dispatch function which will dispatch to a handler function which emits the file relative to base-path that is denoted by the URI of the request relative to uri-prefix. uri-prefix must be a string ending with a slash, base-path must be a pathname designator for an existing directory. Uses HANDLE-STATIC-FILE internally.

If content-type is not NIL, it will be used as a the content type for all files in the folder. Otherwise (which is the default) the content type of each file will be determined as usual.


[Generic function]
dispatch-request dispatch-table => result


This is a generic function so users can customize its behaviour. Look at the source code for details.


[Macro]
define-easy-handler description lambda-list [[declaration* | documentation]] form*


Defines a handler as if by DEFUN and optionally registers it with a URI so that it will be found by DISPATCH-EASY-HANDLERS.

description is either a symbol name or a list matching the destructuring lambda list

  (name &key uri default-parameter-type default-request-type).
lambda-list is a list the elements of which are either a symbol var or a list matching the destructuring lambda list
  (var &key real-name parameter-type init-form request-type).
The resulting handler will be a Lisp function with the name name and keyword parameters named by the var symbols. Each var will be bound to the value of the GET or POST parameter called real-name (a string) before the body of the function is executed. If real-name is not provided, it will be computed by downcasing the symbol name of name.

If uri (which is evaluated) is provided, then it must be a string or a function designator for a unary function. In this case, the handler will be returned by DISPATCH-EASY-HANDLERS, if uri is a string and the script name of the current request is uri, or if uri designates a function and applying this function to the current REQUEST object returns a true value.

Whether the GET or POST parameter (or both) will be taken into consideration, depends on request-type which can be :GET, :POST, :BOTH, or NIL. In the last case, the value of default-request-type (the default of which is :BOTH) will be used.

The value of var will usually be a string (unless it resulted from a file upload in which case it won't be converted at all), but if parameter-type (which is evaluated) is provided, the string will be converted to another Lisp type by the following rules:

If the corresponding GET or POST parameter wasn't provided by the client, var's value will be NIL. If parameter-type is 'STRING, var's value remains as is. If parameter-type is 'INTEGER and the parameter string consists solely of decimal digits, var's value will be the corresponding integer, otherwise NIL. If parameter-type is 'KEYWORD, var's value will be the keyword obtained by interning the upcased parameter string into the keyword package. If parameter-type is 'CHARACTER and the parameter string is of length one, var's value will be the single character of this string, otherwise NIL. If parameter-type is 'BOOLEAN, var's value will always be T (unless it is NIL by the first rule above, of course). If parameter-type is any other atom, it is supposed to be a function designator for a unary function which will be called to convert the string to something else.

Those were the rules for simple parameter types, but parameter-type can also be a list starting with one of the symbols LIST, ARRAY, or HASH-TABLE. The second value of the list must always be a simple parameter type as in the last paragraph - we'll call it the inner type below.

In the case of 'LIST, all GET/POST parameters called real-name will be collected, converted to the inner type as by the rules above, and assembled into a list which will be the value of var.

In the case of 'ARRAY, all GET/POST parameters which have a name like the result of

  (format nil "~A[~A]" real-name n)
where n is a non-negative integer, will be assembled into an array where the nth element will be set accordingly, after conversion to the inner type. The array, which will become the value of var, will be big enough to hold all matching parameters, but not bigger. Array elements not set as described above will be NIL. Note that VAR will always be bound to an array, which may be empty, so it will never be NIL, even if no appropriate GET/POST parameters are found.

The full form of a 'HASH-TABLE parameter type is

  (hash-table inner-type key-type test-function),
but key-type and test-function can be left out in which case they default to 'STRING and 'EQUAL, respectively. For this parameter type, all GET/POST parameters which have a name like the result of
  (format nil "~A{~A}" real-name key)
(where key is a string that doesn't contain curly brackets) will become the values (after conversion to inner-type) of a hash table with test function test-function where key (after conversion to key-type) will be the corresponding key. Note that var will always be bound to a hash table, which may be empty, so it will never be NIL, even if no appropriate GET/POST parameters are found.

To make matters even more complicated, the three compound parameter types also have an abbreviated form - just one of the symbols LIST, ARRAY, or HASH-TABLE. In this case, the inner type will default to 'STRING.

If parameter-type is not provided or NIL, default-parameter-type (the default of which is 'STRING) will be used instead.

If the result of the computations above would be that var would be bound to NIL, then init-form (if provided) will be evaluated instead, and var will be bound to the result of this evaluation.

Handlers built with this macro are constructed in such a way that the resulting Lisp function is useful even outside of Hunchentoot. Specifically, all the parameter computations above will only happen if *REQUEST* is bound, i.e. if we're within a Hunchentoot request. Otherwise, var will always be bound to the result of evaluating init-form unless a corresponding keyword argument is provided.

The example code that comes with Hunchentoot contains an example which demonstrates some of the features of DEFINE-EASY-HANDLER.


[Function]
dispatch-easy-handlers request => handler


This is a dispatcher which returns the appropriate handler defined with DEFINE-EASY-HANDLER, if there is one. The newest handlers are checked first. DEFINE-EASY-HANDLER makes sure that there's always only one handler per name and one per URI. URIs are compared by EQUAL, so anonymous functions won't be recognized as being identical.

Requests

When a request comes in, Hunchentoot creates a REQUEST object which is available to the handler via the special variable *REQUEST*. This object holds all the information available about the request and can be queried with the functions described in this chapter. Note that the internal structure of REQUEST objects should be considered opaque and may change in future releases of Hunchentoot.

In all of the functions below, the default value for request (which is either an optional or a keyword argument) is the value of *REQUEST*, i.e. handlers will usually not need to provide this argument when calling the function.

(Some of the function names in this section might seem a bit strange. This is because they were initially chosen to be similar to environment variables in CGI scripts.)


[Special variable]
*request*


Holds the current REQUEST object.


[Function]
host &optional request => string


Returns the value of the incoming Host http header. (This corresponds to the environment variable HTTP_HOST in CGI scripts.)


[Function]
request-method &optional request => keyword


Returns the request method as a keyword, i.e. something like :POST. (This corresponds to the environment variable REQUEST_METHOD in CGI scripts.)


[Function]
request-uri &optional request => string


Returns the URI for request. Note that this not the full URI but only the part behind the scheme and authority components, so that if the user has typed http://user:password@www.domain.com/xxx/frob.html?foo=bar into his browser, this function will return "/xxx/frob.html?foo=bar". (This corresponds to the environment variable REQUEST_URI in CGI scripts.)


[Function]
script-name &optional request => string


Returns the file name (or path) component of the URI for request, i.e. the part of the string returned by REQUEST-URI in front of the first question mark (if any). (This corresponds to the environment variable SCRIPT_NAME in CGI scripts.)


[Function]
query-string &optional request => string


Returns the query component of the URI for request, i.e. the part of the string returned by REQUEST-URI behind the first question mark (if any). (This corresponds to the environment variable QUERY_STRING in CGI scripts.) See also GET-PARAMETER and GET-PARAMETERS.


[Function]
get-parameter name &optional request => string


Returns the value of the GET parameter (as provided via the request URI) named by the string name as a string (or NIL if there ain't no GET parameter with this name). Note that only the first value will be returned if the client provided more than one GET parameter with the name name. See also GET-PARAMETERS.


[Function]
get-parameters &optional request => alist


Returns an alist of all GET parameters (as provided via the request URI). The car of each element of this list is the parameter's name while the cdr is its value (as a string). The elements of this list are in the same order as they were within the request URI. See also GET-PARAMETER.


[Function]
post-parameter name &optional request => string


Returns the value of the POST parameter (as provided in the request's body) named by the string name. Note that only the first value will be returned if the client provided more than one POST parameter with the name name. This value will usually be a string (or NIL if there ain't no POST parameter with this name). If, however, the browser sent a file through a multipart/form-data form, the value of this function is a three-element list
(path file-name content-type)
where path is a pathname denoting the place were the uploaded file was stored, file-name (a string) is the file name sent by the browser, and content-type (also a string) is the content type sent by the browser. The file denoted by path will be deleted after the request has been handled - you have to move or copy it somewhere else if you want to keep it. See also POST-PARAMETERS and *TMP-DIRECTORY*.


[Function]
post-parameters &optional request => alist


Returns an alist of all POST parameters (as provided via the request's body). The car of each element of this list is the parameter's name while the cdr is its value. The elements of this list are in the same order as they were within the request's body. See also POST-PARAMETER.


[Special variable]
*file-upload-hook*


If this is not NIL, it should be a designator for a unary function which will be called with a pathname for each file which is uploaded to Hunchentoot. The pathname denotes the temporary file to which the uploaded file is written. The hook is called directly before the file is created. At this point, *REQUEST* is already bound to the current REQUEST object, but obviously you can't access the post parameters yet.


[Function]
raw-post-data &key request external-format force-text force-binary want-binary => raw-body-or-stream


Returns the content sent by the client in the request body if there was any (unless the content type was multipart/form-data in which case NIL is returned). By default, the result is a string if the type of the Content-Type media type is "text", and a vector of octets otherwise. In the case of a string, the external format to be used to decode the content will be determined from the charset parameter sent by the client (or otherwise *HUNCHENTOOT-DEFAULT-EXTERNAL-FORMAT* will be used).

You can also provide an external format explicitly (through external-format) in which case the result will unconditionally be a string. Likewise, you can provide a true value for force-text which will force Hunchentoot to act as if the type of the media type had been "text" (with external-format taking precedence if provided). Or you can provide a true value for force-binary which means that you want a vector of octets at any rate. (If both force-text and force-binary are true, an error will be signaled.)

If, however, you provide a true value for want-stream, the other parameters are ignored and you'll get the content (flexi) stream to read from it yourself. It is then your responsibility to read the correct amount of data, because otherwise you won't be able to return a response to the client. If the content type of the request was multipart/form-data or application/x-www-form-urlencoded, the content has been read by Hunchentoot already and you can't read from the stream anymore.

You can call RAW-POST-DATA more than once per request, but you can't mix calls which have different values for want-stream.

Note that this function is slightly misnamed because a client can send content even if the request method is not POST.


[Function]
parameter name &optional request => string


Returns the value of the GET or POST parameter named by the string name as a string (or NIL if there ain't no parameter with this name). If both a GET and a POST parameter with the name name exist, the GET parameter will be returned. See also GET-PARAMETER and POST-PARAMETER.


[Function]
header-in name &optional request => string


Returns the incoming header named by the keyword name as a string (or NIL if there ain't no header with this name). Note that this queries the headers sent to Hunchentoot by the client or by mod_lisp. In the latter case this may not only include the incoming http headers but also some headers sent by mod_lisp.

For backwards compatibility, name can also be a string which is matched case-insensitively. See also HEADERS-IN.


[Function]
headers-in &optional request => alist


Returns an alist of all incoming headers. The car of each element of this list is the headers's name (a Lisp keyword) while the cdr is its value (as a string). There's no guarantee about the order of this list. See also HEADER-IN and the remark about incoming headers there.


[Function]
authorization &optional request => user, password


Returns as two values the user and password (if any) from the incoming Authorization http header. Returns NIL if there is no such header.


[Function]
remote-addr &optional request => string


Returns the IP address (as a string) of the client which sent the request. (This corresponds to the environment variable REMOTE_ADDR in CGI scripts.) See also REAL-REMOTE-ADDR.


[Function]
remote-port &optional request => number


Returns the IP port (as a number) of the client which sent the request.


[Function]
real-remote-addr &optional request => string{, list}


Returns the value of the incoming X-Forwarded-For http header as the second value in the form of a list of IP addresses and the first element of this list as the first value if this header exists. Otherwise returns the value of REMOTE-ADDR as the only value.


[Function]
server-addr &optional request => string


Returns the IP address (as a string) where the request came in. (This corresponds to the environment variable SERVER_ADDR in CGI scripts.)


[Function]
server-port &optional request => number


Returns the IP port (as a number) where the request came in.


[Function]
server-protocol &optional request => keyword


Returns the version of the http protocol which is used by the client as a Lisp keyword - this is usually either :HTTP/1.0 or :HTTP/1.1. (This corresponds to the environment variable SERVER_PROTOCOL in CGI scripts.)


[Function]
mod-lisp-id &optional request => string


Returns the 'Server ID' sent by mod_lisp. This corresponds to the third parameter in the "LispServer" directive in Apache's configuration file and can be interesting if you deploy several different Apaches or Hunchentoot instances at once. Returns NIL in stand-alone servers.


[Function]
ssl-session-id &optional request => string


Returns Apache's SSL session ID if it exists. Note that SSL sessions aren't related to Hunchentoot sessions. (This corresponds to the environment variable SSL_SESSION_ID in CGI scripts.) Returns NIL in stand-alone servers.


[Function]
user-agent &optional request => string


Returns the value of the incoming User-Agent http header. (This corresponds to the environment variable HTTP_USER_AGENT in CGI scripts.)


[Function]
referer &optional request => string


Returns the value of the incoming Referer (sic!) http header. (This corresponds to the environment variable HTTP_REFERER in CGI scripts.)


[Function]
cookie-in name &optional request => string


Returns the value of the incoming cookie named by the string name (or NIL if there ain't no cookie with this name). See also COOKIES-IN.


[Function]
cookies-in &optional request => alist


Returns an alist of all incoming cookies. The car of each element of this list is the cookie's name while the cdr is the cookie's value. See also COOKIE-IN.


[Accessor]
aux-request-value symbol &optional request => value, present-p
(setf (aux-request-value symbol &optional request) new-value)


This accessor can be used to associate arbitrary data with the the symbol symbol in the REQUEST object request. present-p is true if such data was found, otherwise NIL.


[Function]
delete-aux-request-value symbol &optional request => |


Completely removes any data associated with the symbol symbol from the REQUEST object request. Note that this is different from using AUX-REQUEST-VALUE to set the data to NIL.


[Function]
recompute-request-parameters &key request external-format => |


Recomputes the GET and POST parameters for the REQUEST object request. This only makes sense if you've changed the external format and with POST parameters it will only work if the request body was sent with the application/x-www-form-urlencoded content type.

The default value for external-format is *HUNCHENTOOT-DEFAULT-EXTERNAL-FORMAT*. See test/test.lisp for an example.

Replies

It is the responsibility of a handler function to prepare the reply for the client. This is done by For each request there's one REPLY object which is accessible to the handler via the special variable *REPLY*. This object holds all the information available about the reply and can be accessed with the functions described in this chapter. Note that the internal structure of REPLY objects should be considered opaque and may change in future releases of Hunchentoot.

In all of the functions below, the default value for the optional argument reply is the value of *REPLY*, i.e. handlers will usually not need to provide this argument when calling the function.

While Hunchentoot's preferred way of sending data to the client is the one described above (i.e. the handler returns the whole payload as a string or an array of octets) you can, if you really need to (for example for large content bodies), get a stream you can write to directly. This is achieved by first setting up *REPLY* and then calling SEND-HEADERS. Note that in this case the usual error handling is disabled. See the file test/test.lisp for an example.


[Special variable]
*reply*


Holds the current REPLY object.


[Accessor]
header-out name &optional reply => string
(setf (header-out name &optional reply) new-value)


HEADER-OUT returns the outgoing http header named by the keyword name if there is one, otherwise NIL. SETF of HEADER-OUT changes the current value of the header named name. If no header named name exists it is created. For backwards compatibility, name can also be a string in which case the association between a header and its name is case-insensitive.

Note that the headers Set-Cookie, Content-Length, and Content-Type cannot be queried by HEADER-OUT and must not be set by SETF of HEADER-OUT.

See also HEADERS-OUT, CONTENT-TYPE, CONTENT-LENGTH, COOKIES-OUT, and COOKIE-OUT.


[Function]
headers-out &optional request => alist


Returns an alist of all outgoing http parameters (except for Set-Cookie, Content-Length, and Content-Type). The car of each element of this list is the headers's name while the cdr is its value. This alist should not be manipulated directly, use SETF of HEADER-OUT instead.


[Function]
cookie-out name &optional reply => cookie


Returns the outgoing cookie named by the string name (or NIL if there ain't no cookie with this name). See also COOKIES-OUT and the section about cookies.


[Function]
cookies-out &optional reply => alist


Returns an alist of all outgoing cookies. The car of each element of this list is the cookie's name while the cdr is the cookie itself. See also COOKIE-OUT and the section about cookies.


[Accessor]
return-code &optional reply => number
(setf (return-code &optional reply) new-value)


RETURN-CODE returns the http return code of the reply, SETF of RETURN-CODE changes it. The return code of each REPLY object is initially set to +HTTP-OK+.


[Accessor]
content-type &optional reply => string
(setf (content-type &optional reply) new-value)


CONTENT-TYPE returns the outgoing Content-Type http header. SETF of CONTENT-TYPE changes the current value of this header. The content type of each REPLY object is initially set to the value of *DEFAULT-CONTENT-TYPE*.


[Accessor]
content-length &optional reply => length
(setf (content-length &optional reply) new-value)


CONTENT-LENGTH returns the outgoing Content-Length http header. SETF of CONTENT-LENGTH changes the current value of this header. The content length of each REPLY object is initially set to NIL. If you leave it like that, Hunchentoot will automatically try to compute the correct value using LENGTH. If you set the value yourself, you must make sure that it's the correct length of the body in octets (not in characters). In this case, Hunchentoot will use the value as is which can lead to erroneous behaviour if it is wrong - so, use at your own risk.

Note that setting this value explicitely doesn't mix well with URL rewriting.


[Function]
send-headers => stream


Sends the initial status line and all headers as determined by the REPLY object *REPLY*. Returns a flexi stream to which the body of the reply can be written. Once this function has been called, further changes to *REPLY* don't have any effect. Also, automatic handling of errors (i.e. sending the corresponding status code to the browser, etc.) is turned off for this request. Likewise, functions like REDIRECT or throwing to HANDLER-DONE won't have the desired effect once the headers are sent.

If your handlers return the full body as a string or as an array of octets, you should not call this function. If a handler calls SEND-HEADERS, its return value is ignored.

See also REPLY-EXTERNAL-FORMAT.


[Accessor]
reply-external-format &optional reply => external-format
(setf (reply-external-format &optional reply) new-value)


Gets and sets the external format of the REPLY object reply. This external format is used when character content is written to the client after the headers have been sent. In particular, it is the external format of the stream returned by SEND-HEADERS (but of course you can change it because it's a flexi stream).

The initial value for each request is the value of *HUNCHENTOOT-DEFAULT-EXTERNAL-FORMAT*.


[Constants]
+http-continue+
+http-switching-protocols+
+http-ok+
+http-created+
+http-accepted+
+http-non-authoritative-information+
+http-no-content+
+http-reset-content+
+http-partial-content+
+http-multiple-choices+
+http-moved-permanently+
+http-moved-temporarily+
+http-see-other+
+http-not-modified+
+http-use-proxy+
+http-temporary-redirect+
+http-bad-request+
+http-authorization-required+
+http-payment-required+
+http-forbidden+
+http-not-found+
+http-method-not-allowed+
+http-not-acceptable+
+http-proxy-authentication-required+
+http-request-time-out+
+http-conflict+
+http-gone+
+http-length-required+
+http-precondition-failed+
+http-request-entity-too-large+
+http-request-uri-too-large+
+http-unsupported-media-type+
+http-requested-range-not-satisfiable+
+http-expectation-failed+
+http-internal-server-error+
+http-not-implemented+
+http-bad-gateway+
+http-service-unavailable+
+http-gateway-time-out+
+http-version-not-supported+


The values of these constants are 100, 101, 200, 201, 202, 203, 204, 205, 206, 300, 301, 302, 303, 304, 305, 307, 400, 401, 402, 403, 404, 405, 406, 407, 408, 409, 410, 411, 412, 413, 414, 415, 416, 417, 500, 501, 502, 503, 504, and 505. See RETURN-CODE.


[Special variable]
*default-content-type*


The value of this variable is used to initialize the content type of each REPLY object. Its initial value is "text/html; charset=iso-8859-1". See CONTENT-TYPE.

Cookies

Outgoing cookies are stored in the request's REPLY object (see COOKIE-OUT and COOKIES-OUT). They are CLOS objects defined like this:
(defclass cookie ()
  ((name :initarg :name
         :reader cookie-name
         :type string
         :documentation "The name of the cookie - a string.")
   (value :initarg :value
          :accessor cookie-value
          :initform ""
          :documentation "The value of the cookie. Will be URL-encoded when sent to the browser.")
   (expires :initarg :expires
            :initform nil
            :accessor cookie-expires
            :documentation "The time (a universal time) when the cookie expires (or NIL).")
   (path :initarg :path
         :initform nil
         :accessor cookie-path
         :documentation "The path this cookie is valid for (or NIL).")
   (domain :initarg :domain
           :initform nil
           :accessor cookie-domain
           :documentation "The domain this cookie is valid for (or NIL).")
   (secure :initarg :secure
           :initform nil
           :accessor cookie-secure
           :documentation "A generalized boolean denoting whether this cookie is a secure cookie.")))
The reader COOKIE-NAME and the accessors COOKIE-VALUE, COOKIE-EXPIRES, COOKIE-PATH, COOKIE-DOMAIN, and COOKIE-SECURE are all exported from the HUNCHENTOOT package.


[Function]
set-cookie name &key value expires path domain secure reply => cookie


Creates a COOKIE object from the parameters provided to this function and adds it to the outgoing cookies of the REPLY object reply. If a cookie with the same name (case-sensitive) already exists, it is replaced. The default for reply is *REPLY*. The default for value is the empty string.


[Function]
set-cookie* cookie &optional reply => cookie


Adds the COOKIE object cookie to the outgoing cookies of the REPLY object reply. If a cookie with the same name (case-sensitive) already exists, it is replaced. The default for reply is *REPLY*.

Sessions

TNBL supports sessions: Once a Hunchentoot page has called START-SESSION, Hunchentoot uses either cookies or (if the client doesn't send the cookies back) rewrites URLs to keep track of this client, i.e. to provide a kind of 'state' for the stateless http protocol. The session associated with the client is an opaque CLOS object which can be used to store arbitrary data between requests.

Hunchentoot makes some reasonable effort to prevent eavesdroppers from hijacking sessions (see below), but this should not be considered really secure. Don't store sensitive data in sessions and rely solely on the session mechanism as a safeguard against malicious users who want to get at this data!

For each request there's one SESSION object which is accessible to the handler via the special variable *SESSION*. This object holds all the information available about the session and can be accessed with the functions described in this chapter. Note that the internal structure of SESSION objects should be considered opaque and may change in future releases of Hunchentoot.

Sessions are automatically verified for validity and age when the REQUEST object is instantiated, i.e. if *SESSION* is not NIL then this session is valid (as far as Hunchentoot is concerned) and not too old. Old sessions are automatically removed.


[Special variable]
*session*


Holds the current SESSION object (if any) or NIL.


[Function]
start-session => session


Returns *SESSION* if it isn't NIL, otherwise creates a new SESSION object and returns it.


[Accessor]
session-value symbol &optional session => value, present-p
(setf (session-value symbol &optional session) new-value)


This accessor can be used to associate arbitrary data with the the symbol symbol in the SESSION object session. present-p is true if such data was found, otherwise NIL. The default value for session is *SESSION*.

If SETF of SESSION-VALUE is called with session being NIL then a session is automatically instantiated with START-SESSION.


[Function]
delete-session-value symbol &optional session => |


Completely removes any data associated with the symbol symbol from the SESSION object session. Note that this is different from using SESSION-VALUE to set the data to NIL. The default value for session is *SESSION*.


[Function]
reset-sessions => |


This function unconditionally invalidates and destroys all sessions immediately.


[Function]
session-string session => string


Returns a unique string that's associated with the SESSION object session. This string is sent to the browser as a cookie value or as a GET parameter,


[Function]
session-counter session => count


Returns the number of times (requests) the SESSION object session has been used.


[Accessor]
session-max-time session => seconds
(setf (session-max-time session) seconds)


This gets or sets the maximum time (in seconds) the SESSION object session should be valid before it's invalidated: If a request associated with this session comes in and the last request for the same session was more than seconds seconds ago than the session is deleted and a new one is started for this client. The default value is determined by *SESSION-MAX-TIME*.


[Function]
session-remote-addr session => address


Returns the 'real' remote address (see REAL-REMOTE-ADDR) of the client for which the SESSION object session was initiated.


[Function]
session-user-agent session => address


Returns the 'User-Agent' http header (see USER-AGENT) of the client for which the SESSION object session was initiated.


[Special variable]
*use-remote-addr-for-sessions*


If this value is true (the default is NIL) then the 'real' remote address (see REAL-REMOTE-ADDR) of the client will be encoded into the session identifier, i.e. if this value changes on the client side, the session will automatically be invalidated.

Note that this is not secure, because it's obviously not very hard to fake an X_FORWARDED_FOR header. On the other hand, relying on the remote address (see REMOTE-ADDR) of the client isn't an ideal solution either, because some of your users may connect through http proxies and the proxy they use may change during the session. But then again, some proxies don't send X_FORWARDED_FOR headers anyway. Sigh...


[Special variable]
*use-user-agent-for-sessions*


If this value is true (which is the default) then the 'User-Agent' http header (see USER-AGENT) of the client will be encoded into the session identifier, i.e. if this value changes on the client side the session will automatically be invalidated.

While this is intended to make the life of malicious users harder, it might affect legitimate users as well: I've seen this http header change with certain browsers when the Java plug-in was used.


[Special variable]
*rewrite-for-session-urls*


If this value is true (which is the default) then content bodies sent by Hunchentoot will be rewritten (using URL-REWRITE) such that GET parameters for session handling are appended to all relevant URLs. This only happens, though, if the body's content type (see CONTENT-TYPE) starts with one of the strings in *CONTENT-TYPES-FOR-URL-REWRITE* and unless the client has already sent a cookie named *SESSION-COOKIE-NAME*.

Note that the function which rewrites the body doesn't understand Javascript, so you have to take care of URLs in Javascript code yourself.


[Special variable]
*content-types-for-url-rewrite*


This is a list of strings (the initial value is ("text/html" "application/xhtml+xml")) the content-type of an outgoing body is compared with if *REWRITE-FOR-SESSION-URLS* is true. If the content-type starts with one of these strings, then url-rewriting will happen, otherwise it won't.


[Special variable]
*session-cookie-name*


This is the name that is used for the session-related cookie or GET parameter sent to the client. Its default value is "hunchentoot-session". Note that changing this name while Hunchentoot is running will invalidate existing sessions.


[Special variable]
*session-removal-hook*


The value of this variable should be a function of one argument, a SESSION object. This function is called directly before the session is destroyed, either by RESET-SESSIONS or when it's invalidated because it's too old.


[Special variable]
*session-max-time*


The default time (in seconds) after which a session times out - see SESSION-MAX-TIME. This value is initially set to 1800.


[Macro]
do-sessions (var &optional result-form) statement* => result


Executes the statements with var bound to each existing SESSION object consecutively. An implicit block named NIL surrounds the body of this macro. Returns the values returned by result-form unless RETURN is executed. The scope of the binding of var does not include result-form.


[Special variable]
*session-gc-frequency*


A session garbage collection (see SESSION-GC) will happen every *SESSION-GC-FREQUENCY* requests (counting only requests which use sessions) if the value of this variable is not NIL. It's default value is 50.


[Function]
session-gc => |


Deletes sessions which are too old - see SESSION-TOO-OLD-P. Usually, you don't call this function directly - see *SESSION-GC-FREQUENCY*.


[Function]
session-too-old-p session => generalized-boolean


Returns a true value if the SESSION object session is too old and would be deleted during the next session GC. You don't have to check this manually for sessions in *SESSION*, but it might be useful if you want to loop through all sessions.

Logging and error handling

Hunchentoot provides facilities for writing to Apache's error log file (when using the mod_lisp front-end) or for logging to an arbitrary file in the file system. Note that, due to the nature of mod_lisp, Apache log mesages don't appear immediately but only after all data has been sent from Hunchentoot to Apache/mod_lisp.

Furthermore, all errors happening within a handler which are not caught by the handler itself are handled by Hunchentoot - see details below.


[Accessor]
log-file => pathname
(setf (log-file) pathspec)


The function LOG-FILE returns a pathname designating the log file which is currently used (unless log messages are forwarded to Apache). This destination for log messages can be changed with (SETF LOG-FILE). The initial location of the log file is implementation-dependent.


[Generic function]
log-message log-level format &rest args => |


Schedules a message for the Apache log file or writes it directly to the current log file depending on the value of the use-apache-log-p argument to START-SERVER. log-level should be one of the keywords :EMERG, :ALERT, :CRIT, :ERROR, :WARNING, :NOTICE, :INFO, or :DEBUG which correspond to the various Apache log levels. log-level can also be NIL in which case mod_lisp's default log level is used. If Apache isn't used, the log level is just written to the log file unless it's NIL. format and args are used as with FORMAT.

LOG-MESSAGE is a generic function, so you can specialize it or bypass it completely with an around method.


[Function]
log-message* format &rest args => |


Like LOG-MESSAGE but with log-level set to *DEFAULT-LOG-LEVEL*.


[Special variable]
*default-log-level*


The log level used by LOG-MESSAGE*. The initial value is NIL.


[Special variable]
*log-lisp-errors-p*


Whether unhandled errors in handlers should be logged. See also *LISP-ERRORS-LOG-LEVEL*. The default value is T.


[Special variable]
*lisp-errors-log-level*


The log level used to log Lisp errors. See also *LOG-LISP-ERRORS-P*. The default value is :ERROR.


[Special variable]
*log-lisp-warnings-p*


Whether unhandled warnings in handlers should be logged. See also *LISP-WARNINGS-LOG-LEVEL*. The default value is T.


[Special variable]
*lisp-warnings-log-level*


The log level used to log Lisp warnings. See also *LOG-LISP-WARNINGS-P*. The default value is :WARNING.


[Special variable]
*log-lisp-backtraces-p*


Whether backtraces should also be logged in addition to error messages and warnings. This value will only have effect if *LOG-LISP-ERRORS-P* or *LOG-LISP-WARNINGS-P* is true. The default value is NIL.


[Special variable]
*log-prefix*


All messages written to the Apache error log by Hunchentoot are prepended by a string which is the value of this variable enclosed in square brackets. If the value is NIL, however, no such prefix will be written. If the value is T (which is the default), the prefix will be "[Hunchentoot]".


[Special variable]
*show-lisp-errors-p*


Whether unhandled Lisp errors should be shown to the user. If this value is NIL (which is the default), only the message An error has occurred will be shown.


[Special variable]
*show-lisp-backtraces-p*


Whether backtraces should also be shown to the user. This value will only have effect if *SHOW-LISP-ERRORS-P* is true. The default value is NIL.


[Special variable]
*show-access-log-messages*


If this variable is true and if the value of the use-apache-log-p argument to START-SERVER was NIL, then for each request a line somewhat similar to what can be found in Apache's access log will be written to the log file. The default value of this variable is T.


[Special variable]
*http-error-handler*


This variable holds NIL (the default) or a function designator for a function of one argument. The function gets called if the responsible handler has set a return code other than +HTTP-OK+ or +HTTP-NOT-MODIFIED+ and *HANDLE-HTTP-ERRORS-P* is true. It receives the return code as its argument and can return the contents of an error page or NIL if it refuses to handle the error, i.e. if Hunchentoot's default error page should be shown. (Note that the function can access the request and reply data.)


[Special variable]
*handle-http-errors-p*


This variable holds a generalized boolean that determines whether return codes other than +HTTP-OK+ or +HTTP-NOT-MODIFIED+ are treated specially. When its value is true (the default), either a default body for the return code or the result of calling *HTTP-ERROR-HANDLER* is used. When the value is NIL, no special action is taken and you are expected to supply your own response body to describe the error.


[Function]
get-backtrace condition => backtrace


This is the function that is used internally by Hunchentoot to show or log backtraces. It accepts a condition object condition and returns a string with the corresponding backtrace.

Debugging Hunchentoot applications

The best option to debug a Hunchentoot application is probably to use the debugger.

One important thing you should try if you're behind mod_lisp is to use an external log file (as opposed to Apache's log) because it can reveal error messages that might otherwise get lost if something's broken in the communication between Hunchentoot and mod_lisp.

Good luck... :)


[Special variable]
*catch-errors-p*


If the value of this variable is NIL (the default is T), then errors which happen while a request is handled aren't caught as usual, but instead your Lisp's debugger is invoked. This variable should obviously always be set to a true value in a production environment.

Miscellaneous

Various functions and variables which didn't fit into one of the other categories.


[Symbol]
handler-done


This is a catch tag which names a catch which is active during the lifetime of a handler. The handler can at any time throw the outgoing content body (or NIL) to this catch to immediately abort handling the request. See the source code of REDIRECT for an example.


[Function]
no-cache => |


This function will set appropriate outgoing headers to completely prevent caching on virtually all browsers.


[Function]
handle-if-modified-since time => |


This function is designed to be used inside a handler. If the client has sent an 'If-Modified-Since' header (see RFC 2616, section 14.25) and the time specified matches the universal time time then the header +HTTP-NOT-MODIFIED+ with no content is immediately returned to the client.

Note that for this function to be useful you should usually send 'Last-Modified' headers back to the client. See the code of CREATE-STATIC-FILE-DISPATCHER-AND-HANDLER for an example.


[Function]
rfc-1123-date &optional time => string


This function accepts a universal time time (default is the current time) and returns a string which encodes this time according to RFC 1123. This can be used to send a 'Last-Modified' header - see HANDLE-IF-MODIFIED-SINCE.


[Function]
redirect target &key host protocol add-session-id permanently => |


Sends back appropriate headers to redirect the client to target (a string). If target is a full URL starting with a scheme, host and protocol are ignored. Otherwise, target should denote the path part of a URL, protocol must be one of the keywords :HTTP or :HTTPS, and the URL to redirect to will be constructed from host, protocol, and target.

If permanently is true (the default is NIL), a 301 status code will be sent, otherwise a 302 status code. If host is not provided, the current host (see HOST) will be used. If protocol is the keyword :HTTPS, the client will be redirected to a https URL, if it's :HTTP it'll be sent to a http URL. If both host and protocol aren't provided, then the value of protocol will match the current request.


[Function]
require-authorization &optional realm => |


Sends back appropriate headers to require basic HTTP authentication (see RFC 2617) for the realm realm. The default value for realm is "Hunchentoot".


[Function]
escape-for-html string => escaped-string


Escapes all occurrences of the characters #\<, #\>, #\', #", and #\& within string for HTML output.


[Function]
url-encode string &optional external-format => url-encoded-string


URL-encodes a string using the external format external-format. The default for external-format is the value of *HUNCHENTOOT-DEFAULT-EXTERNAL-FORMAT*.


[Function]
url-decode string &optional external-format => url-encoded-string


URL-decodes a string using the external format external-format, i.e. this is the inverse of URL-ENCODE. It is assumed that you'll rarely need this function, if ever. But just in case - here it is. The default for external-format is the value of *HUNCHENTOOT-DEFAULT-EXTERNAL-FORMAT*.


[Function]
http-token-p object => generalized-boolean


This function tests whether object is a non-empty string which is a token according to RFC 2068 (i.e. whether it may be used for, say, cookie names).


[Special variable]
*tmp-directory*


This should be a pathname denoting a directory where temporary files can be stored. It is used for file uploads.


[Special variable]
*hunchentoot-default-external-format*


The (flexi stream) external format used when computing the REQUEST object. The default value is the result of evaluating
(flex:make-external-format :latin1 :eol-style :lf)

 

History

Hunchentoot's predecessor TBNL (which is short for "To Be Named Later") grew over the years as a toolkit that I used for various commercial and private projects. In August 2003, Daniel Barlow started a review of web APIs on the lispweb mailing list and I described the API of my hitherto-unreleased bunch of code (and christened it "TBNL").

It turned out that Jeff Caldwell had worked on something similar so he emailed me and proposed to join our efforts. As I had no immediate plans to release my code (which was poorly organized, undocumented, and mostly CMUCL-specific), I gave it to Jeff and he worked towards a release. He added docstrings, refactored, added some stuff, and based it on KMRCL to make it portable across several Lisp implementations.

Unfortunately, Jeff is at least as busy as I am so he didn't find the time to finish a full release. But in spring 2004 I needed a documented version of the code for a client of mine who thought it would be good if the toolkit were publicly available under an open source license. So I took Jeff's code, refactored again (to sync with the changes I had done in the meantime), and added documentation. This resulted in TBNL 0.1.0 (which initially required mod_lisp as its front-end). Jeff's code (which includes a lot more stuff that I didn't use) is still available from his own website tbnl.org.

In March 2005, Bob Hutchinson sent patches which enabled TBNL to use other front-ends than mod_lisp. This made me aware that TBNL was already almost a full web server, so eventually I wrote Hunchentoot which was a full web server, implemented as a wrapper around TBNL. Hunchentoot 0.1.0 was released at the end of 2005 and was originally LispWorks-only.

Hunchentoot 0.4.0, released in October 2006, was the first release which also worked with other Common Lisp implementations. It is a major rewrite and also incorporates most of TBNL and replaces it completely.
 

Symbol index

Here are all exported symbols of the HUNCHENTOOT package in alphabetical order linked to their corresponding entries:
 

Acknowledgements

Thanks to Jeff Caldwell - TBNL would not have been released without his efforts. Thanks to Marc Battyani for mod_lisp and to Chris Hanson for mod_lisp2. Thanks to Stefan Scholl and Travis Cross for various additions and fixes to TBNL, to Michael Weber for initial file upload code, and to Janis Dzerins for his RFC 2388 code. Thanks to Bob Hutchison for his code for multiple front-ends (which made me realize that TBNL was already pretty close to a "real" web server) and the initial UTF-8 example. Thanks to John Foderaro's AllegroServe for inspiration. Thanks to Uwe von Loh for the Hunchentoot logo.

Hunchentoot originally used code from ACL-COMPAT, specifically the chunking code from Jochen Schmidt. (This has been replaced by Chunga.) When I ported Hunchentoot to other Lisps than LispWorks, I stole code from ACL-COMPAT, KMRCL, and trivial-sockets for implementation-dependent stuff like sockets and MP.

Parts of this documentation were prepared with DOCUMENTATION-TEMPLATE, no animals were harmed.

$Header: /usr/local/cvsrep/hunchentoot/doc/index.html,v 1.53 2007/01/24 00:34:37 edi Exp $

BACK TO MY HOMEPAGE