Handlers
A handler is a procedure. The server calls it with each request and sends back what it returns.
(import (rnrs) (http-server))
(serve
(lambda (request)
(make-response 200 #f '()
(string->utf8 (string-append "You asked for " (request-path request) "\n")))))
serve listens on 127.0.0.1:8080 unless CHEZ_HTTP_HOST and
CHEZ_HTTP_PORT say otherwise (Configuration). It runs
until the process gets SIGTERM or SIGINT (Ctrl-C), then finishes the
requests in flight and returns (Connections). It writes one
line to standard output for each thing that happens, starting with
listening 127.0.0.1 8080.
Once serve has returned, Ctrl-C ends the process at once, so in a REPL it
ends Chez too.
Reading the request
For GET /a/b?x=1 HTTP/1.1 with Host: example.com, sent from
192.0.2.7 port 54321 to the server at 198.51.100.1 port 8080:
| Call | Gives | Here |
|---|---|---|
(request-method r) | the method, a symbol | GET |
(request-path r) | the path | "/a/b" |
(request-query r) | what follows the first ?, or #f with no ? | "x=1" |
(request-header r "host") | the first header with that name, or #f | "example.com" |
(request-header-values r "host") | every header with that name, in order; '() with none | ("example.com") |
(request-header-list r) | every header, in order, as (name . value) | (("Host" . "example.com")) |
(request-host r) | the site the request is for | "example.com" |
(request-http-major r) | the version's first number | 1 |
(request-http-minor r) | the version's second number | 1 |
(request-has-body? r) | whether a body follows the head | #f |
(request-remote-address r) | the address of the connection's other end, or #f | "192.0.2.7" |
(request-remote-port r) | its port, or #f | 54321 |
(request-local-address r) | the address the connection came in on, or #f | "198.51.100.1" |
(request-local-port r) | its port, or #f | 8080 |
- The method is one of the 36 the parser knows, in capitals:
GET,POST,PUT, the WebDAV methods and the rest. Any other, such asgetorFOO, gets400 Bad Requestbefore your handler runs. - Header names match in any case:
"content-type"findsContent-Type.request-header-listgives each name as it was sent, so fold its case before you compare it. - A list such as
Connectionmay come on several lines.request-headerreads the first;request-header-valuesreads them all, andrequest-header-listgives each line as its own pair. request-header-listgives every header as the client sent it, the ones about this connection only among them, such asConnectionandTransfer-Encoding(RFC 9110 §7.6.1). A proxy removes those before it passes the rest on, and gives the body it sends its ownContent-LengthorTransfer-Encoding.- The spaces around a header's value are not part of it.
- Nothing is decoded:
/a%20breads"/a%20b". Decode it yourself if you need to (Requests). - Each byte is one character (Latin-1), so every byte reads back as it was sent. To read UTF-8 text, turn the string back into bytes with a Latin-1 transcoder and decode those.
- A proxy names the site in the target itself:
GET http://example.com/a. Then the path is"/a"and the host is"example.com", whatever theHostline says (RFC 9112). An empty path, as inGET http://example.com, reads"/". OPTIONS *has the path"*", andCONNECT example.com:443the path"".- With an empty
Host:, or none (HTTP/1.0), the host is"". - The version is the one the request names:
HTTP/1.2reads1and2, though the server answers it as HTTP/1.1. The first number is always1: the server refuses any other version before your handler runs (Connections). - A body follows when the request is chunked or has a
Content-Lengthabove 0.request-has-body?is the server's own answer, soContent-Length: 00is no body, and a chunked body counts even when it turns out empty. Answer a reader to read it (Bodies and streaming). - The addresses belong to the connection, not the request: the server reads
them once, when it accepts the connection, so every request on it reads
the same. One is
#fwhen the server can't say, as when the client reset the connection before the server read its address. - An address is text:
"192.0.2.7","::1". A server listening on::takes IPv4 clients too, and reads their addresses, and its own end's, as"::ffff:192.0.2.7". - Behind a proxy, the other end is the proxy (Deploying).
Each call makes its string when you ask, so a handler pays only for what it
reads. request-has-body? makes nothing: the server decides it once.
Keep strings, not the request
A request holds on to the bytes it arrived in, up to
CHEZ_HTTP_READ_BUFFER_SIZE (16 KiB). To keep something for later, keep the
strings you read, not the request: a request you keep keeps its bytes too.
Answering
(make-response status reason headers body):
status: a number from 200 to 599;reason: a bytevector, such as(string->utf8 "OK"), or#ffor the usual one;headers: a list of(name . value)pairs of bytevectors;body: a bytevector,#ffor none, a producer that streams it, or asized-bodythat streams it with its length (Bodies and streaming).
A handler can answer with three other things instead of a response:
| Answer | When | Page |
|---|---|---|
a reader, from read-body or read-whole-body | to read the request's body | Bodies and streaming |
(awaiting thunk) | to wait for something, or read the body as it comes, before answering | Waiting |
(switch-protocols headers take-over) | to take the connection for another protocol | Upgrades |
What the server adds to your response, such as Date and Content-Length,
and what it refuses to send, is in Responses. If your handler
raises, the client gets 500 Internal Server Error and the server logs
handler-failed.
When the response is done
Returning a response doesn't tell you it reached the client. To find out,
register a procedure with (request-on-done! request proc). The server calls
it once, when the response is done, with how it ended:
proc gets | When |
|---|---|
written | every write of your response completed |
client-closed | the client closed or reset the connection before your response was written, or a write to it failed |
failed | the server ended it: your code raised (a 500, or a stream cut short), the server sent its own status instead of yours (a body too big or too slow), or a write timed out |
;; log each request's path, how it ended, and how long it took
(lambda (request)
(let ((start (real-time)))
(request-on-done! request
(lambda (how)
(display (list (request-path request) how (- (real-time) start)))
(newline)))
(make-response 200 #f '() (string->utf8 "hello\n"))))
real-time is Chez's, from (chezscheme).
- Any code that holds the request may register: the handler, its reader, its producer, a thunk it waits in. Register as many as you like: the newest is called first, so a middleware that registers first hears last.
- The server calls them just after the response ends, once the event loop's turn is over, as it calls handlers. Like a handler, a procedure must be quick, and it cannot wait. On a kept-alive connection they run before the next request's handler, and for an upgrade, before your take-over.
- If your code is still running when the response ends, such as a reader or producer that waits (Waiting), the server calls them once that code stops, so nothing they close is still in use.
- A client that only half-closes its connection still reads, so its response
is written, and
procgetswritten. - If one raises, the server logs
on-done-failed, still calls the others, and carries on. - One registered after the response is done is called at once, inside the code that registers it, so each is called exactly once. If it raises, that code raises.
- A request the server refuses before your handler runs, such as one with a malformed head, never reaches your code, so nothing hears about it.
Testing a handler
(http-server testing) runs your handler on requests your test writes as
bytes, and reads back what a client would: see
Testing handlers.
Keep it quick
Your handler runs on the server's event loop. While it runs, no other
connection is served. Do quick work in place: build a page, read a small
file. To wait for something, such as another request, a timer or a DNS
lookup, answer with awaiting (Waiting).
When the server will not start
A malformed setting, such as CHEZ_HTTP_PORT=http or a CHEZ_HTTP_HOST
that is not an IP address, stops serve before it listens. It raises an
&http-server-config-error that names the variable;
http-server-config-error? checks for one.
serve also takes the environment as an optional second argument, a
procedure from a variable's name to its value or #f. A program can use it
to take settings from its command line, as
file-server.sps does:
(serve handler
(lambda (name)
(if (string=? name "CHEZ_HTTP_PORT") "8000" (getenv name))))
getenv here is Chez's, from (chezscheme).