HTTP2 in Common Lisp
Table of Contents
- 1 Overview
- 2 Tutorials
- 3 API documentation
- 4 Implementation details (not part of API)
[in package HTTP2]
1 Overview
This is an HTTP/2 implementation in Common Lisp. It provides both high-level interface as well as components to build an optimized tools for specific use cases.
For quick start, quickload HTTP2 and see Tutorials that show how to use a
simple client to fetch a resource or how to start the server and serve some
content.
For more documentation consult API documentation.
2 Tutorials
2.1 Client tutorials
[in package HTTP2/CLIENT]
The client tutorials show how to fetch a web resource using built-in Drakma-style interface, and how to make more advanced client that does several requests in parallel using HTTP/2 streams.
2.1.1 Using built-in HTTP/2 client
You can use RETRIEVE-URL to fetch a a web resource.
(http2/client:retrieve-url "https://example.com")
==> "<!doctype html>
... <html>
... <head>
... <title>Example Domain</title>
...
... <meta charset="utf-8" />
... <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
... <meta name="viewport" conten...[sly-elided string of length 1256]"
==> 200 (8 bits, #xC8, #o310, #b11001000)
==> (("content-length" . "1256") ("x-cache" . "HIT") ("vary" . "Accept-Encoding")
... ("server" . "ECS (bsb/27E0)")
... ("last-modified" . "Thu, 17 Oct 2019 07:18:26 GMT")
... ("expires" . "Thu, 28 Sep 2023 19:38:44 GMT")
... ("etag" . "\"3147526947+ident\"") ("date" . "Thu, 21 Sep 2023 19:38:44 GMT")
... ("content-type" . "text/html; charset=UTF-8")
... ("cache-control" . "max-age=604800") ("age" . "151654"))
==> "/"
==> #<VANILLA-CLIENT-CONNECTION >
==> NIL
==> "HTTP2 does not provide reason phrases"
It was designed to be similar to DRAKMA:HTTP-REQUEST, but is not completely
same. See below what the function does, some notable differences are:
The last parameter (response reason) is no longer relevant in HTTP/2. It is a dummy string that might be removed in further releases.
The path is taken as-is. You cannot provide
GETparameters to fill in.The redirects are not handled transparently. What is returned is returned.
It is your responsibility to make sure that the HTTP/2 headers are correct (lowercase, order).
You cannot get stream on output to work with. You get the connection, and you can use it.
Basically, the differences fall into two areas, that it does not (yet) provide all the features of Drakma, and HTTP/2 is different.
2.1.2 Build your own client
Let us see what it takes to build simplified RETRIEVE-URL function from
components. It will use CL+SSL to build a Lisp stream over TLS stream over
network stream.
HTTP/2 requests are done over TLS connection created with an ALPN indication that it is to
be used for HTTP/2. The helper function here is CONNECT-TO-TLS-SERVER, and then
WITH-OPEN-STREAM can be used:
(defun my-retrieve-url (url)
(let ((parsed-url (puri:parse-uri url)))
(with-open-stream (network-stream
(connect-to-tls-server (puri:uri-host parsed-url)
:sni (puri:uri-host parsed-url)
:port (or (puri:uri-port parsed-url) 443)))
(my-retrieve-url-using-network-stream network-stream url))))
Now that we have a Lisp STREAM to communicate over, we can establish HTTP/2
connection of class VANILLA-CLIENT-CONNECTION over it, send client request, and
then PROCESS-PENDING-FRAMES until server fully sends the response. That invokes
restart FINISH-STREAM with the processed stream that we handle. We can get the
data from it using DRAKMA-STYLE-STREAM-VALUES.
(defun my-retrieve-url-using-network-stream (lisp-stream url)
(with-http2-connection (connection 'vanilla-client-connection lisp-stream)
(my-send-client-request connection url)
(restart-case
(process-pending-frames connection nil)
(finish-stream (stream)
(drakma-style-stream-values stream)))))
Sending the request involves creating a new HTTP2 stream with OPEN-HTTP2-STREAM
and proper parameters.
(defun my-send-client-request (connection url)
(open-http2-stream connection
(request-headers :GET (puri:uri-path (puri:parse-uri url))
(puri:uri-host (puri:parse-uri url)))
:end-stream t))
2.2 Server tutorials
Server related interfaces are exported from the HTTP2/SERVER package and are
part of the HTTP2/SERVER system. This system is also loaded when HTTP2 is loaded.
2.2.1 Starting HTTP/2 server
[in package HTTP2/SERVER with nicknames HTTP2/SERVER/SHARED, HTTP2/SERVER/POLL, HTTP2/SERVER/THREADED]
Start server on foreground with RUN, or on background with START. You can stop server on background with STOP.
This creates (as of this version) a multithreaded server that serves 404 Not found responses on any request.
(http2/server:start 8443)
==> #<HTTP2/SERVER:DETACHED-TLS-THREADED-DISPATCHER HTTP/2 server on https://localhost:8443/>
==> #<PURI:URI https://localhost:8443/>
;; run curl -k https://localhost:8443/
127.0.0.1:58565 Connected, using VANILLA-SERVER-CONNECTION
127.0.0.1:58565 / [#1] - processing
2.2.2 Define content for HTTP/2 server
[in package HTTP2/SERVER with nicknames HTTP2/SERVER/SHARED, HTTP2/SERVER/POLL, HTTP2/SERVER/THREADED]
To server something else than 404 Not found, you need to define handlers for specific paths. Simple handler definition can look like
(define-exact-handler "/hello-world"
(handler (foo :utf-8 nil)
(with-open-stream (foo foo)
(send-headers
'((:status "200")
("content-type" "text/html; charset=utf-8")))
(format foo "Hello World, this is random: ~a" (random 10)))))
This defines a handler on "/hello-world" path that sends reasonable headers, writes some text to the stream and closes the stream (via WITH-OPEN-STREAM). The text written is passed to the client as data (body).
In general, the handlers are set using DEFINE-PREFIX-HANDLER or
DEFINE-EXACT-HANDLER, and are functions typically created by HANDLER macro,
or (in simple cases) by REDIRECT-HANDLER or SEND-TEXT-HANDLER functions.
[macro] DEFINE-PREFIX-HANDLER PREFIX FN &OPTIONAL CONNECTION
Define function to run when peer closes http stream on
CONNECTION(or any server defined in future) if the path of the stream starts withPREFIX.
[macro] DEFINE-EXACT-HANDLER PATH FN &OPTIONAL CONNECTION
Define function to run when peer closes http stream on
CONNECTION(or any server defined in future) if the path of the stream isPATH.
[type] HANDLER
Function that can be called with
CONNECTIONandHTTP2-STREAMto write a response to the http request described bySTREAMobject.
[macro] HANDLER (FLEXI-STREAM-NAME CHARSET GZIP) &BODY BODY
Return a
HANDLERtype function.This handler, when called, runs
BODYin a context whereFLEXI-STREAM-NAMEis bound to an open flexi stream that can be written to (to write response). On background, written text is converted fromCHARSETto octets, possibly compressed byGZIPand split into frames,and two lexical functions are defined,
SEND-HEADERSandSEND-GOAWAY.
The
SEND-HEADERSsends the provided headers to theSTREAM.The
SEND-GOAWAYsends go away frame to the client to close connection.The handler body needs to close the underlying stream if the response is actually to be sent, or possibly schedule sending more data for later.
[macro] CONSTANT-HANDLER (FLEXI-STREAM-NAME CHARSET GZIP HEADERS) &BODY BODY
Run
BODYto print the output toFLEXI-STREAM-NAMEin compile time. This constant (static) page is served every time as-is.
[function] REDIRECT-HANDLER TARGET &KEY (CODE "301") (CONTENT-TYPE "text/html; charset=UTF-8") CONTENT
A handler that emits redirect response with http status being
CODE, and optionally providesCONTENTwithCONTENT-TYPE.
[function] SEND-TEXT-HANDLER TEXT &KEY (CONTENT-TYPE "text/html; charset=UTF-8") (GZIP
T) ADDITIONAL-HEADERSA handler that returns
TEXTas content ofCONTENT-TYPE.TEXTis evaluated when handler is defined, not when handler is invoked. For content that can change on individual invocations write to the stream.ADDITIONAL-HEADERSare sent along with :status and content-type headers.
[function] SEND-HEADERS STREAM HEADERS &KEY END-STREAM (END-HEADERS
T) &ALLOW-OTHER-KEYSSend
HEADERSto a HTTP2 stream. The stream is returned.The
END-HEADERSandEND-STREAMallow to set the appropriate flags.Inside HANDLER macro, this names a function that has the
STREAMargument implicit and onlyHEADERSand key parameters are to be provided.
[function] SEND-GOAWAY CODE DEBUG-DATA
Start closing connection, sending
CODEandDEBUG-DATAin the go-away frame to peer. Must be called from inside ofHANDLERmacro.
2.2.3 Getting request details
[in package HTTP2/SERVER with nicknames HTTP2/SERVER/SHARED, HTTP2/SERVER/POLL, HTTP2/SERVER/THREADED]
Sometimes you need to get some data from the request.
These data can be carried by querying the HTTP/2 stream object involved. If you
define handlers by HANDLER macro, it is available in a lexically bound STREAM
variable.
(define-exact-handler "/body"
(handler (foo :utf-8 nil)
(with-open-stream (foo foo)
(send-headers
'((:status "200")
("content-type" "text; charset=utf-8")))
(format foo "Hello World, this is a ~s request.~3%content~%~s~3%headers~%~s~%~3%body~%~s~%"
(http2/core::get-method stream)
(http-stream-to-string stream)
(http2/core::get-headers stream)
(http2/core::get-body stream)))))
- [generic-function] GET-PATH OBJECT
[accessor] GET-HEADERS HEADER-COLLECTING-MIXIN (:HEADERS)
List of collected (header . value) pairs. Does not include
:method,:path, etc.
[accessor] GET-METHOD SERVER-STREAM (:METHOD)
The HTTP method ([RFC7231], Section 4)
[accessor] GET-SCHEME SERVER-STREAM (:SCHEME)
Scheme portion of the target URI ([RFC3986], Section 3.1).
Not restricted to "http" and "https" schemed URIs. A proxy or gateway can translate requests for non-HTTP schemes, enabling the use of HTTP to interact with non-HTTP services
[accessor] GET-AUTHORITY SERVER-STREAM (:AUTHORITY)
The authority portion of the target URI ([RFC3986], Section 3.2)
Body of the request
Sometimes there is a body in the client request.
When sending a such a request, you can use CONTENT parameter of the
HTTP2/CLIENT:RETRIEVE-URL, together with CONTENT-TYPE.
(http2/client:retrieve-url "https://localhost:8080/body" :content "Hello")
(http2/client:retrieve-url "https://localhost:8080/body"
:content #(1 2 3) :content-type "application/octet-stream")
When you write a handler for such a request, you should know if you want binary
or text data. The vanilla class for the server streams looks at the headers, and
if they look like UTF-8 (as per IS-UTF8-P), it processes the data as text, if
not, they are collected as binary vector.
When your client systematically send headers that do not make it TEXT and you
want to read text, as last resort change class of your streams to include
FALLBACK-ALL-IS-ASCII (or improve IS-UTF8-P, or add some other decoding function).
If you do not want to see text at all, change class to NOT include
UTF8-PARSER-MIXIN or any other conversion mixin.
[accessor] GET-BODY BODY-COLLECTING-MIXIN (:BODY)
Body of the request as an octet vector.
May be empty if some higher priority mixin (e.g.,
UTF8-PARSER-MIXIN) processed the data.
[function] HTTP-STREAM-TO-STRING HTTP-STREAM
HTTP-STREAMshould be aTEXT-COLLECTING-STREAM.HTTP-STREAM-TO-VECTOR then assembles the text from individual chunks.
[class] FALLBACK-ALL-IS-ASCII
Treat all non-binary data input as ASCII, that is, convert octets to a string with
CODE-CHAR. It is compatible withUTF8-PARSER-MIXINif provided after it in the list of direct superclasses.