Clack File Uploads

(defun upload-demo (env)
  (case (getf env :request-method)
    (:get
     `(
       200
       nil
       (,(cl-who:with-html-output-to-string (s)
	   (:html
	    (:body
	     (:form :method "post"	           ; Any combination other than post & multipart/form-data
		    :enctype "multipart/form-data" ; causes the browser to just send the filename
	      (:input :type "file"
		      :name "foo")
	      (:input :type "submit"))))))))
    (:post
     (let ((req (lack.request:make-request env))) ; The lack request builder will get the
       (print env)				  ; body parameters for us
       (destructuring-bind (stream fname content-type)
	   (cdr (assoc "foo" (lack.request:request-body-parameters req) :test #'equal))
	 (when (and
		(typep stream 'file-stream)
		(probe-file stream))
	   (delete-file stream))	; see https://github.com/fukamachi/smart-buffer/issues/1
	 `(
	   200
	   nil
	   (,(format nil "~S ~S ~S ~S~%" env stream fname content-type))))))))

Getting started with clack

Clack is a very simple framework for unifying the different lisp web application servers. However there isn't a lot of documentation for it. This page hopes to address this.

Annotated examples of usage

Formatting

All input and output are presented in monospaced preformatted blocks. The color indicates what sort they are:

"Lisp code has a gray background; it can be typed at the REPL"
NIL
: shell code has a green background, it can be typed at a shell prompt
(format t "Output from running commands has a yellow background; do not type this")
Output from running commands has a yellow background; do not type this

Load dependencies used in examples:

   (ql:quickload '(clack alexandria optima))
   (use-package :optima)
To load "clack":
  Load 1 ASDF system:
    clack
; Loading "clack"
...
To load "alexandria":
  Load 1 ASDF system:
    alexandria
; Loading "alexandria"

To load "optima":
  Load 1 ASDF system:
    optima
; Loading "optima"

Start a simple server

The only required argument for clackup is the application; the simplest form of the application is a function of one argument.

The function should retun a list of the form (http-response-code http-headers-alist &optional body)

body can be a vector of (unsigned-byte 8), a pathname, or a list of strings.

(defparameter *clack-server* (clack:clackup (lambda (env)
                                         '(200 nil ("Hello, World!")))))
Hunchentoot server is started.
Listening on localhost:5000.

Let's test it with curl:

   curl -s http://localhost:5000
Hello, World!

Stop the server

     (clack:stop *clack-server*)

Redefining the handler

It's a bit of a pain to have to restart the server all the time, let's make a redefinable handler:

     (defun handler (env) '(200 nil ("Hello World, redefinable!")))

And start the server; we call the function by name to allow redefinition

    (defparameter *clack-server*
      (clack:clackup (lambda (env) (funcall 'handler env))))
Hunchentoot server is started.
Listening on localhost:5000.

Check that it works…

   curl -s http://localhost:5000
Hello World, redefinable!

Now let's redefine it and take a look at what is in the environment:

     (defun handler (env)
       `(200 nil (,(prin1-to-string env))))

View results…

   curl -s http://localhost:5000
(:REQUEST-METHOD :GET :SCRIPT-NAME "" :PATH-INFO "/" :SERVER-NAME "localhost"
 :SERVER-PORT 5000 :SERVER-PROTOCOL :HTTP/1.1 :REQUEST-URI "/" :URL-SCHEME
 "http" :REMOTE-ADDR "127.0.0.1" :REMOTE-PORT 53824 :QUERY-STRING NIL :RAW-BODY
 #<FLEXI-STREAMS:FLEXI-IO-STREAM {1021B536E3}> :CONTENT-LENGTH NIL
 :CONTENT-TYPE NIL :CLACK.STREAMING T :CLACK.IO
 #<CLACK.HANDLER.HUNCHENTOOT::CLIENT {1021B537F3}> :HEADERS
 #<HASH-TABLE :TEST EQUAL :COUNT 3 {1021B53C13}>)

This is the core part of clack; the environment plist.

Documentation for it is available in the lack README

The fact that it is a plist means capturing values of interest can be done with destructuring-bind:

  (defun handler (env)
    (destructuring-bind (&key request-method path-info request-uri
                              query-string headers &allow-other-keys)
        env
      `(200
        nil
        (,(format nil "Method: ~S Path: ~S URI: ~A Query: ~S~%Headers: ~S"
                  request-method path-info request-uri query-string
                  (alexandria:hash-table-alist headers))))))
curl -s http://localhost:5000
Method: :GET Path: "/" URI: / Query: NIL
Headers: (("accept" . "*/*") ("user-agent" . "curl/7.53.0")
          ("host" . "localhost:5000"))

Optima can be useful too:

  (defun handler (env)
    (optima:match env
      ((guard (property :path-info path)
              (alexandria:starts-with-subseq "/foo/" path))
       `(200 nil (,(format nil "The path '~A' is in /foo/~%" path))))
      ((guard (property :path-info path)
              (alexandria:starts-with-subseq "/bar/" path))
       `(200 nil (,(format nil "The path '~A' is in /bar/~%" path))))
      ((property :path-info path)
       `(404 nil (,(format nil "Path ~A not found~%" path))))))
   curl -s http://localhost:5000/foo/quux
   curl -s http://localhost:5000/bar/quux
   curl -s http://localhost:5000/baz/quux
The path '/foo/quux' is in /foo/
The path '/bar/quux' is in /bar/
Path /baz/quux not found

Public API

CLACK:CLACKUP

Syntax:

clackup app &key server port debug silent use-thread use-default-middlewares &allow-other-keys

=> handler

Arguments and Values:

  • app–A designator for a function of one argument;or a subclass of lack.component:lack-component;or a pathname; or a string.
  • server–A symbol. The default is :hunchentoot
  • port–An integer. The default is 5000
  • debug–A boolean. The default is t
  • silent–A boolean. The default is nil
  • use-thread–A boolean. The default is t on systems that support threading and nil otherwise.
  • use-default-middlewares–A boolean. The default is t
  • handler–A clack.handler::handler.

Description:

clackup starts a server using the backend designated by server on port port.

app is used to build the handler chain for the server as follows:

  • If app is a function then it will be used directly, and called on each request with the requst environment as its only parameter
  • If app is a subclass of lack.component:lack-component then (lack.component:call app environment) will be called on every request
  • If app is a pathname then it will be treated as a lisp file to be evaluated. The result of the last form of the file will be used as above
  • If app is a string, then it will be coerced to a pathname and used as above.
  • If use-default-middlewares is true then app will be wrapped by the default middlewares

server designates the backend to use; if the backend is not found, then clackup will attempt to load it via quicklisp or asdf.

port specifies which port to listen on.

debug specifies that debug mode is on. The results of this is backend specific, but typically will handle all errors in the body of app by returning a 500 response to the user if false

silent Suppresses printing of status messages.

use-thread If true, the backend is launched in a separate thread.