No matching definitions.

tur/httpd

stdlib/httpd.tur

Lightweight HTTP/1.0 and HTTP/1.1 server.

Single-process HTTP server. The listener thread runs a tur/reactor loop that watches the listen socket for READ events; each accepted connection is dispatched (via a shared queue) to a worker thread that reads the request, invokes the handler, and writes the response. HTTP/1.1 keep-alive is supported: a worker loops on the same socket until the client requests Connection: close or a read timeout fires.

Handler calling convention: the server dispatches a handler through the fat-closure protocol (read slot 0 as the thunk, pass the record itself as the env). The closure-entry parameters that take a handler, verifier, or middleware next are annotated ^fat, so a bare *non-capturing* fn is auto-shimmed into a fat closure at the call site -- you no longer have to capture a dummy variable to force heap allocation. Inside the handler, use httpd-req-* to read the parsed request and httpd-resp-*! to set the response.

Since: Phase H1

defn

httpd-register-tls-impl

(httpd-register-tls-impl [wrap-fd :int handshake :int read-fn :int write-fn :int shutdown :int free-fn :int] :nil)

install TLS function pointers into the H5 hook table.

Called once by user code after importing tur/tls (the spice provides a one-liner helper that captures these pointers and forwards them here). Without this call, httpd-new-tls fails cleanly because the function pointer table is all zeros.

wrap-fd(int64_t)(intptr_t)&tls-wrap-fd
handshake(int64_t)(intptr_t)&tls-handshake
read-fn(int64_t)(intptr_t)&tls-read
write-fn(int64_t)(intptr_t)&tls-write
shutdown(int64_t)(intptr_t)&tls-shutdown
free-fn(int64_t)(intptr_t)&tls-free

Since: Phase H5

defn

httpd-set-max-body!

(httpd-set-max-body! [n :int] :int)

cap the request body size the server will read.

A request whose Content-Length exceeds the cap is answered 413 and the connection closed before any of the body is read or allocated. The default is 8 MiB. This is the server-wide ceiling; mw-body-size applies a lower per-route limit on top of it.

nthe new cap in bytes (clamped to [0, 2^31 - 2])

The previous cap.

(httpd-set-max-body! (* 64 1048576))   ; accept uploads up to 64 MiB

Since: security-audit-plan WP4 (M-4)

defn

httpd-set-bind-any!

(httpd-set-bind-any! [on :bool] :bool)

listen on every interface instead of loopback.

Servers bind 127.0.0.1 by default, so a program that starts httpd is not reachable from the network until it says so. Call this (or set the environment variable TUR_HTTPD_BIND_ANY=1) before httpd-new / httpd-new-pool / httpd-new-async to bind 0.0.0.0. TUR_BIND_LOOPBACK, which the test harnesses export, still forces loopback.

ontrue to bind every interface, false for loopback

The previous setting.

(httpd-set-bind-any! true)
(httpd-new 8080 handler)   ; reachable on every interface

Since: security-audit-plan WP4 (M-4)

defn

httpd-set-bind-addr!

(httpd-set-bind-addr! [addr :cstr] :bool)

listen on one address, IPv4 or IPv6.

httpd-set-bind-any! chooses between loopback and every interface; this names the interface -- a numeric address like "192.168.1.5", "::1" or "::" (every IPv6 interface). It applies to the servers created after it (httpd-new, httpd-new-pool, httpd-new-async, ...) and wins over httpd-set-bind-any!. "" clears it, back to that default. An address that does not parse is refused and the setting is left as it was. TUR_BIND_LOOPBACK, which the test harnesses export, still forces loopback -- of the address's own family (127.0.0.1 or ::1).

addra numeric IPv4 or IPv6 address, or "" to clear

true if the address was accepted (or cleared), false if it did not parse.

(httpd-set-bind-addr! "::1")
(httpd-new 8080 handler)   ; IPv6 loopback only

Since: 2026-10-03 (security-audit-plan WP4 residual)

defn

httpd-bind-addr

(httpd-bind-addr :int)

internal: the IPv4 address (host order) to listen on.

defn

httpd-listen-socket

(httpd-listen-socket [port :int] :int)

internal: a bound, listening, non-blocking socket for port on the configured address (httpd-set-bind-addr!, else loopback or every interface per httpd-bind-addr), or -1. Shared by the constructors.

defn

httpd-status-text

(httpd-status-text [status :int] :cstr)

internal: the reason phrase for a status code.

defn

httpd-error-response

(httpd-error-response [status :int buf :ptr<void> cap :int] :int)

internal: a complete close-the-connection response.

Writes "HTTP/1.1 <status> <text>" with an empty body and Connection: close into buf (at most cap bytes) and returns its length.

defn

httpd-parse-head

(httpd-parse-head [c :ptr<void> buf :ptr<void> total :int out :ptr<void>] :int)

internal: parse a request head that is already in memory.

buf holds total bytes (NUL-terminated) that include the whole header block. Fills the connection's method / path / version / req_headers and client_keep_alive, stores the validated Content-Length through out (an int64_t*), and returns the header block's length in bytes. A request the server must refuse returns the negated status to answer it with:

-400  malformed, negative or conflicting Content-Length, or both
      Content-Length and Transfer-Encoding (the request-smuggling shape),
      or a method / version too long for its field
-413  Content-Length above the body cap (httpd-server-set-max-body! for
      this server, else httpd-set-max-body!'s)
-414  a request target too long for the path field
-501  Transfer-Encoding (chunked bodies are not implemented, and reading
      the chunks as the next request would desync the connection)

Pure over its input: no socket, which is what the fuzz target relies on.

defn

httpd-resp-head

(httpd-resp-head [c :ptr<void> is11 :int out :ptr<void>] :ptr<void>)

internal: build the response status line + header block.

Returns a malloc'd buffer (caller frees) holding everything up to and including the blank line, and stores its length through out (an int64_t*); NULL on allocation failure. A user-set Content-Length is dropped (the server owns the byte count), and so is any header whose name or value would break the framing -- a CR or LF in either (response splitting when a handler echoes request data into a header), or a name with a colon or blank in it.

defn

httpd-resp-finalize

(httpd-resp-finalize [c :ptr<void>] :nil)

internal: last word on the status before the head is built.

A 101 Switching Protocols is only honest on a connection that is really being handed off. One set without httpd-conn-upgrade! (a handler bug) is answered 500, and one whose upgrade the server refused (the async server, which cannot hand off a connection yet) is answered 501 -- in both cases with an explanatory body and the connection closed, so the client never believes it upgraded.

A 1xx, 204 or 304 response has no body (RFC 9110), so one a handler set anyway -- a middleware that ran after the status was decided, say -- is dropped here rather than written as bytes the client would read as the start of the next response.

defn

httpd-conn-release

(httpd-conn-release [c :ptr<void>] :nil)

internal: free everything a request attached to conn.

The body, the response body, both header lists, the cached JSON body, the multipart parts, the attribute list, the owned accessor strings and an upgrade callback that never ran. Leaves conn reusable (every freed field is reset).

defn

httpd-handle

(httpd-handle [fd :int handler :int tls :int max-body :int read-ms :int] :nil)

internal: serve one or more HTTP requests on fd, then close fd. Loops while the client keeps the connection alive.

HTTP/1.1 keep-alive semantics:

- HTTP/1.1: connection kept alive unless Connection: close.
- HTTP/1.0: connection closed unless Connection: keep-alive.
- Each request head must arrive whole within the read timeout
  (`read-ms`, else httpd-set-read-timeout!'s, 5 s by default); that
  also bounds how long an idle keep-alive connection is held.  A head
  that is still incomplete at the deadline is answered 408, one larger
  than 256 KiB is answered 431, and an idle connection is just closed.
- Bytes a client pipelined behind one request are kept for the next.

When tls is non-zero it is an opaque TLS connection handle obtained from the spice's tls-wrap-fd + tls-handshake; reads and writes are routed through the registered TLS ops instead of recv/send.

A handler that called httpd-conn-upgrade! gets its response written and then the connection handed to its callback; one that called httpd-conn-detach! has taken the connection already. Either way this function neither closes the fd nor releases the TLS state afterwards.

max-body is the server's own body cap (-1: httpd-set-max-body!'s).

Since: Phase H1 (TLS path: Phase H5; upgrade, deadline: tourist-on-stdlib-httpd H1)

defn

httpd-worker

(httpd-worker [arg :ptr<void>] :ptr<void>)

internal: worker thread function.

Pops fds from the shared queue and processes them sequentially via httpd-handle. Exits when the queue is closed and drained.

Since: Phase H3

defn

httpd-accept-cb

(httpd-accept-cb [env :ptr<void> id :int events :int user :int] :nil)

internal reactor READ callback for the listen fd.

Drains the listen queue, pushing each accepted fd onto the shared worker queue and signalling one waiting worker. env[1] is the HttpdBlock pointer (synthetic fat closure built in httpd-new).

NOTE: the trailing user/user-data slot is typed : int DELIBERATELY (an exception to the no-lazy-:int rule): this defn is hand-packed into a fat box's slot 0 (see the clos[0] = ... below / reactor.c park boxes), and the reactor invokes slot 0 through the fat-entry CARRIER convention (typedefs in src/async/reactor.c) -- every value parameter is int64_t. A ptr<void> spelling emits a void * C parameter, which -fsanitize=function flags (and CFI/CET-BTI/WASM reject) at every dispatch.

Since: Phase H2

defn

httpd-new-pool

(httpd-new-pool [port :int workers :int ^fat handler :(fn [ptr<void>] nil)])

create an httpd instance with a configurable worker count.

At most 512 accepted connections wait for a free worker; past that a connection is answered 503 and closed. httpd-new-pool-with-limit sets another cap.

portTCP port to listen on (0 = kernel-assigned)
workersnumber of worker threads in the pool (must be > 0)
handlerfat closure: (fn [conn :ptr<void>] :nil)

ptr<void> -- opaque Httpd handle, or nil on error

Since: Phase H3

defn

httpd-new-pool-with-limit

(httpd-new-pool-with-limit [port :int workers :int ^fat handler :(fn [ptr<void>] nil) max-pending :int])

httpd-new-pool with a pending-connection cap.

Each worker serves one connection at a time; accepted connections wait in a queue for a free worker, each holding a descriptor. When max-pending connections are already waiting, a new one is answered 503 Service Unavailable and closed instead of queued.

portTCP port to listen on (0 = kernel-assigned)
workersnumber of worker threads in the pool (must be > 0)
handlerfat closure: (fn [conn :ptr<void>] :nil)
max-pendingqueued-connection cap (0 = unlimited; httpd-new-pool
uses 512)

ptr<void> -- opaque Httpd handle, or nil on error

(let [h (httpd-new-pool-with-limit 0 4 handler 64)]
  (httpd-run h)
  (httpd-free h))

Since: 2026-10-03 (security-audit-plan WP4 residual)

defn

httpd-new

(httpd-new [port :int ^fat handler :(fn [ptr<void>] nil)])

create a new httpd instance with the default worker pool size (4).

portTCP port to listen on (0 = kernel-assigned)
handlerfat closure: (fn [conn :ptr<void>] :nil)

ptr<void> -- opaque Httpd handle, or nil on error

(let [h (httpd-new 0 (fn [conn :ptr<void>] :nil
                       (httpd-resp-status! conn 200)
                       (httpd-resp-body! conn "OK")))]
  (httpd-run h)
  (httpd-free h))

Since: Phase H1

defn

httpd-new-tls

(httpd-new-tls [port :int workers :int ^fat handler :(fn [ptr<void>] nil) ctx :int])

create an httpd instance with TLS termination.

The accept loop is identical to the plaintext path; each accepted fd is wrapped with tls-wrap-fd + driven through tls-handshake on the worker that pops it. Reads and writes inside httpd-handle then go through the registered TLS ops instead of recv/send.

Prerequisites:

* The tur/tls spice must be linked (otherwise the function-pointer
  table is all zeros and this constructor returns NULL).
* The user must call (tls-httpd-init) once -- exported by the
  spice's tls/httpd module -- before calling httpd-new-tls.
portTCP port to listen on (0 = kernel-assigned)
workersnumber of worker threads (must be > 0)
handlerfat closure: (fn [conn :ptr<void>] :nil)
ctxTLS context handle from tls-ctx-new with cert + key loaded

ptr<void> -- opaque Httpd handle, or nil if ctx is 0 or TLS ops have not been registered.

(tls-httpd-init)
(let [ctx (tls-ctx-new)]
  (tls-ctx-load-cert-pem ctx "cert.pem")
  (tls-ctx-load-key-pem  ctx "key.pem")
  (let [h (httpd-new-tls 8443 4 my-handler ctx)]
    (httpd-run h)
    (httpd-free h)
    (tls-ctx-free ctx)))

Since: Phase H5

defn

httpd-port

(httpd-port [h :ptr<void>] :int)

return the port the server is listening on.

Useful when httpd-new was called with port 0 (kernel-assigned).

hhttpd handle returned by httpd-new

int -- the actual listening port, or -1 on error

Since: Phase H1

defn

httpd-run

(httpd-run [h :ptr<void>])

run the listener reactor until httpd-stop is called.

Since: Phase H1

defn

httpd-stop

(httpd-stop [h :ptr<void>])

signal the listener to stop accepting new connections.

Thread-safe. Stops the listener reactor and closes the shared queue so workers exit when drained.

Since: Phase H1

defn

httpd-free

(httpd-free [h :ptr<void>] :nil)

free all resources after httpd-run returns.

Joins the worker threads, removes the reactor source, frees the reactor and synthetic accept closure, closes the listen socket, drains and frees the shared queue, and frees the HttpdBlock.

Since: Phase H1

defn

httpd-set-read-timeout!

(httpd-set-read-timeout! [ms :int] :int)

set how long the server waits on a client.

A request head must arrive whole within this time (else 408), each read of a request body may wait this long, an idle keep-alive connection is closed after it, and on a TLS server it bounds the handshake. The default is 5000 ms. This is the process-wide value; a pool server can carry its own with httpd-server-set-read-timeout!. The async server uses it for every park on a client read or write.

msthe timeout in milliseconds (clamped to [1, 3600000])

The previous timeout in milliseconds.

(httpd-set-read-timeout! 30000)   ; slow clients get 30 s

Since: tourist-on-stdlib-httpd H1

defn

httpd-server-set-read-timeout!

(httpd-server-set-read-timeout! [h :ptr<void> ms :int] :nil)

give one pool server its own read timeout.

Overrides httpd-set-read-timeout! for the server h (from httpd-new, httpd-new-pool, httpd-new-pool-with-limit or httpd-new-tls), so two servers in one process can differ. Call it before httpd-run.

ha pool server handle
msthe timeout in milliseconds (clamped to [1, 3600000]); 0 goes
back to following httpd-set-read-timeout!
(httpd-server-set-read-timeout! h 500)

Since: tourist-on-stdlib-httpd H1

defn

httpd-server-set-max-body!

(httpd-server-set-max-body! [h :ptr<void> n :int] :nil)

give one pool server its own body cap.

Overrides httpd-set-max-body! for the server h: a request whose Content-Length exceeds it is answered 413 before any of the body is read. Call it before httpd-run.

ha pool server handle
nthe cap in bytes (clamped to [0, 2^31 - 2]); negative goes back to
following httpd-set-max-body!
(httpd-server-set-max-body! h 1048576)   ; 1 MiB for this server

Since: tourist-on-stdlib-httpd H1

defn

httpd-linger-drain

(httpd-linger-drain [fd :int] :nil)

internal: half-close fd and read what the peer is still sending, briefly, so the close that follows does not reset the connection over unread data (a reset can discard the error response the client has not read yet). Bounded to 256 KiB and 500 ms.

defn

httpd-conn-fd

(httpd-conn-fd [conn :ptr<void>] :int)

the client socket behind a request.

connconnection handle from the worker

The file descriptor, or -1 once httpd-conn-close! has closed it.

(httpd-conn-fd c)   ; => 7

Since: tourist-on-stdlib-httpd H1

defn

httpd-conn-tls

(httpd-conn-tls [conn :ptr<void>] :ptr<void>)

the TLS state behind a request, or NULL on plaintext.

The handle the registered TLS implementation's wrap op returned for this connection (with tls/httpd from the tls spice: a tls_conn_t*).

connconnection handle from the worker

The TLS handle, or NULL for a plaintext connection.

(httpd-conn-tls c)   ; => NULL on a plaintext connection

Since: tourist-on-stdlib-httpd H1

defn

httpd-conn-read

(httpd-conn-read [conn :ptr<void> buf :ptr<void> len :int] :int)

read up to len raw bytes from the connection.

Goes through the registered TLS ops on a TLS connection, recv(2) otherwise. Meant for a connection that has been handed off; during an ordinary request the server has already read the request.

connconnection handle
bufdestination buffer
lenits capacity in bytes

Bytes read (> 0), 0 when the peer closed, negative on error or timeout.

(httpd-conn-read c buf 4096)

Since: tourist-on-stdlib-httpd H1

defn

httpd-conn-write

(httpd-conn-write [conn :ptr<void> buf :ptr<void> len :int] :int)

write all len bytes to the connection.

Loops over short writes (a TLS record carries at most 16 KiB). Goes through the registered TLS ops on a TLS connection, send(2) otherwise.

connconnection handle
bufsource bytes (may contain NUL)
lenbyte count

len once everything is written, -1 on error.

(httpd-conn-write c frame frame-len)

Since: tourist-on-stdlib-httpd H1

defn

httpd-conn-clear-timeout

(httpd-conn-clear-timeout [fd :int] :nil)

internal: drop the server's SO_RCVTIMEO from a socket it is handing to someone else.

defn

httpd-set-rcvtimeo

(httpd-set-rcvtimeo [fd :int ms :int] :nil)

internal: set a socket's receive timeout to ms milliseconds (0 = none). POSIX takes a struct timeval; winsock takes a DWORD of milliseconds, and reads a timeval's first word as one (5 s became 5 ms).

defn

httpd-upgrade-refuse

(httpd-upgrade-refuse [c :ptr<void>] :nil)

internal: the async server cannot hand a connection off yet. Marks the conn (httpd-resp-finalize answers a 101 with 501) and says why on stderr, once per process.

defn

httpd-conn-upgrade!

(httpd-conn-upgrade! [conn :ptr<void> ^fat on-upgrade :(fn [ptr<void>] nil)] :bool)

hand the connection to on-upgrade after this response.

Call from a handler, after setting the response that accepts the upgrade (for a WebSocket: status 101 plus Upgrade, Connection and Sec-WebSocket-Accept headers). When the handler returns the server writes that response -- without a Content-Length or a body -- and then, on the same worker thread, calls on-upgrade with the conn. The request accessors still work inside the callback. From then on the callback owns the socket and its TLS state: the server neither reads another request from it nor closes it. Release them with httpd-conn-close! (or, for a transport kept past the callback, with the TLS spice's own shutdown/free and close(2)).

The callback blocks its worker for as long as it runs. If the response cannot be written the callback is not called and the connection is closed as usual. Only pool servers can hand a connection off; on the async server this returns false (see the section notes).

connconnection handle from the worker
on-upgrade(fn [ptr<void>] nil), called with the conn; owned by the
server from this call on (dropped after it runs)

true when the hand-off is scheduled, false when it was refused.

(httpd-resp-status! c 101)
(httpd-resp-header! c "Upgrade" "websocket")
(httpd-resp-header! c "Sec-WebSocket-Accept" accept)
(httpd-conn-upgrade! c (fn [c2 : ptr<void>] : nil
                         (run-session c2)
                         (httpd-conn-close! c2)))

Since: tourist-on-stdlib-httpd H1

defn

httpd-conn-detach!

(httpd-conn-detach! [conn :ptr<void>] :bool)

take the connection over, effective now.

For a handler that writes its own response straight to the transport (httpd-conn-fd / httpd-conn-tls, or httpd-conn-write) and keeps using the connection. When the handler returns the server writes nothing, reads nothing more and closes nothing: the fd and its TLS state belong to the caller, who must release them (httpd-conn-close!). The socket loses the server's read timeout at once.

connconnection handle from the worker

true when the connection is now the caller's, false when refused (the async server, a conn already handed off, or a closed one).

(when (httpd-conn-detach! c)
  (httpd-conn-write c reply reply-len)
  (httpd-conn-close! c))

Since: tourist-on-stdlib-httpd H1

defn

httpd-conn-upgraded?

(httpd-conn-upgraded? [conn :ptr<void>] :bool)

has this connection been (or will it be) handed off?

True after httpd-conn-upgrade! or httpd-conn-detach! succeeded.

connconnection handle

true when the server will not write a response of its own framing or close the connection.

(when (httpd-conn-upgraded? c) (println "hijacked"))

Since: tourist-on-stdlib-httpd H1

defn

httpd-conn-close!

(httpd-conn-close! [conn :ptr<void>] :bool)

release a connection that has been handed off.

Sends the TLS close_notify and frees the TLS state (when there is one) and closes the socket. Only acts on a conn the caller owns -- inside an httpd-conn-upgrade! callback, or after httpd-conn-detach! -- because during an ordinary request the server owns the descriptor and closing it under the server is a double close. Idempotent.

connconnection handle

true when it closed the connection, false when there was nothing of the caller's to close.

(httpd-conn-close! c)

Since: tourist-on-stdlib-httpd H1

defn

httpd-req-method

(httpd-req-method [conn :ptr<void>] :cstr)

return the HTTP method from the parsed request.

Since: Phase H1

defn

httpd-req-path

(httpd-req-path [conn :ptr<void>] :cstr)

return the request-target path from the parsed request.

Since: Phase H1

defn

httpd-req-version

(httpd-req-version [conn :ptr<void>] :cstr)

return the HTTP version from the parsed request line.

cstr -- "HTTP/1.0", "HTTP/1.1", etc., or "" if unparsed

Since: Phase H4

defn

httpd-req-body

(httpd-req-body [conn :ptr<void>] :cstr)

return the request body (NUL-terminated copy).

Returns the body bytes read after the headers, sized by the Content-Length request header. Returns an empty string when there was no Content-Length or it was zero.

cstr -- the request body bytes (NUL-terminated)

Since: Phase H4

defn

httpd-req-body-len

(httpd-req-body-len [conn :ptr<void>] :int)

return the request body length in bytes.

int -- number of bytes in the body (0 if no body)

Since: Phase H4

defn

httpd-resp-status-get

(httpd-resp-status-get [conn :ptr<void>] :int)

return the currently-set response status code.

Returns 0 if no status has been set yet (the default applied during response writing is 200 OK).

Since: Phase H6

defn

httpd-resp-status!

(httpd-resp-status! [conn :ptr<void> status :int] :nil)

set the HTTP response status code.

Since: Phase H1

defn

httpd-resp-close!

(httpd-resp-close! [conn :ptr<void>] :nil)

close the connection after this response.

The response goes out with Connection: close and the server closes the connection instead of waiting for another keep-alive request. A Connection header a handler sets itself does not do this (the server owns that header); this does.

connconnection handle from the worker
(httpd-resp-close! c)

Since: tourist-on-stdlib-httpd H2

defn

httpd-resp-body!

(httpd-resp-body! [conn :ptr<void> body :cstr] :nil)

set the HTTP response body (copies the string).

Since: Phase H1

defn

httpd-resp-body-bytes!

(httpd-resp-body-bytes! [conn :ptr<void> bytes :ptr<void> len :int] :nil)

binary-safe response body setter.

Like httpd-resp-body! but takes a (ptr, byte-count) pair instead of a NUL-terminated cstr, so the body may contain embedded NUL bytes (e.g. gzip output, raw binary payloads). Copies len bytes from bytes into a fresh response-body allocation; the writer emits exactly len bytes regardless of any embedded NULs.

connconnection handle from the worker
bytespointer to raw payload (may contain NUL bytes)
lenbyte count to copy
(httpd-resp-body-bytes! c gzip-data gzip-len)

Since: Phase H8 (M6)

defn

httpd-req-header

(httpd-req-header [conn :ptr<void> name :cstr] :cstr)

look up a request header by name (case-insensitive).

Returns the empty string if the header is absent.

connconnection handle from the worker
nameheader name to look up (case-insensitive)

The header value as a cstr (pointer into per-request storage; valid only for the duration of the handler call). Empty string if absent.

(let [ua (httpd-req-header c "User-Agent")] ...)

Since: Phase H8 (M0)

defn

httpd-req-header?

(httpd-req-header? [conn :ptr<void> name :cstr] :int)

presence test for a request header.

connconnection handle from the worker
nameheader name (case-insensitive)

1 if present, 0 otherwise.

(when (= 1 (httpd-req-header? c "Authorization")) ...)

Since: Phase H8 (M0)

defn

httpd-resp-header!

(httpd-resp-header! [conn :ptr<void> name :cstr value :cstr] :nil)

set (or replace) a response header.

If name is already present on the response, its value is replaced. Otherwise a new entry is appended. Use httpd-resp-header-add! for truly repeatable headers like Set-Cookie or Vary.

connconnection handle from the worker
nameheader name (case-insensitive on the dedup check)
valueheader value
(httpd-resp-header! c "Content-Type" "application/json")

Since: Phase H8 (M0)

defn

httpd-resp-header-add!

(httpd-resp-header-add! [conn :ptr<void> name :cstr value :cstr] :nil)

append a response header without de-duplication.

Use for headers that legitimately repeat -- Set-Cookie, Vary, WWW-Authenticate -- where each entry is independent. For single- valued headers, prefer httpd-resp-header!.

connconnection handle from the worker
nameheader name
valueheader value
(httpd-resp-header-add! c "Set-Cookie" "session=abc; Path=/")
(httpd-resp-header-add! c "Set-Cookie" "csrf=xyz; Path=/")

Since: Phase H8 (M0)

defn

httpd-resp-vary!

(httpd-resp-vary! [conn :ptr<void> token :cstr] :nil)

add a token to the response's Vary header, once.

Vary lists the request headers a response depends on, and several middleware add to it (CORS adds Origin, compression Accept-Encoding). This merges: the token is appended to an existing Vary value unless it is already listed (case-insensitive), and a Vary: * is left alone.

connconnection handle from the worker
tokena header name, e.g. "Origin"
(httpd-resp-vary! c "Accept-Encoding")
(httpd-resp-vary! c "Origin")          ; Vary: Accept-Encoding, Origin

Since: tourist-on-stdlib-httpd H4

defn

httpd-req-header-raw

(httpd-req-header-raw [conn :ptr<void> name :cstr] :cstr)

internal: a request header's value, or NULL when the request has no such header (case-insensitive name). Borrowed.

defstruct

CookieOpts

(defstruct CookieOpts [])

fully-specified Set-Cookie attributes.

Field conventions:

path       -- "" omits the Path attribute
domain     -- "" omits the Domain attribute
max-age    -- -1 omits the Max-Age attribute; 0 expires immediately
secure     -- 0 / 1: include the "Secure" flag
http-only  -- 0 / 1: include the "HttpOnly" flag
same-site  -- "" omits; "Lax" / "Strict" / "None" otherwise

Since: Phase H8 (M2)

defn

httpd-req-form

(httpd-req-form [conn :ptr<void> field :cstr] :cstr)

look up a single URL-encoded form field by name.

Walks the request body (which must already have been received -- the worker reads Content-Length bytes) looking for name=value separated by &. The value is percent-decoded and + is converted to per the application/x-www-form-urlencoded convention.

connconnection handle from the worker
fieldform field name (case-sensitive, plain ASCII)

The decoded value as a heap-allocated cstr (per-call ownership; do not free explicitly -- the runtime drops it after the handler returns). Empty string "" when the field is absent or the body is empty.

(let [user (httpd-req-form c "username")
      pw   (httpd-req-form c "password")] ...)

Since: Phase H8 (M3)

defn

httpd-form-parse-raw

(httpd-form-parse-raw [body :ptr<void> len :int field :cstr] :cstr)

internal: one field of a URL-encoded body.

The walk every form accessor shares (httpd-req-form, httpd-string's httpd-req-form-raw, tourist's form-param): name=value pairs separated by &, the value percent-decoded with + as a space. Names are compared as written (case-sensitive, not decoded).

Returns a fresh malloc'd decoded value (a present-empty field is a fresh ""), which the caller frees, or NULL when the body is empty or the field is absent.

defn

httpd-req-json

(httpd-req-json [conn :ptr<void>] :int)

lazily parse the request body as JSON.

First call invokes json/decode on the request body and caches the resulting node tree on the connection (req_json_cache). Subsequent calls return the cached pointer. The iteration cleanup in httpd-handle calls json/free so handlers never free explicitly.

Returns 0 (nil tree) when the body is absent or json/decode fails on the input; use mw-json-body if you want a 400 short-circuit on malformed JSON.

connconnection handle from the worker

The parsed JSON node tree as an :int (the json stdlib carrier), or 0 when there is no body / parse failed.

(let [doc (httpd-req-json c)
      nm  (json/get-string (json/get! doc "name"))]
  (httpd-resp-body! c nm))

Since: Phase H8 (M3)

defn

mw-json-body

(mw-json-body [^fat next :(fn [ptr<void>] nil)] :ptr<void>)

pre-parse the JSON body, 400 on failure.

Wraps a base handler so that, before next runs, the request body is parsed via httpd-req-json. On parse failure (json/decode returned 0 AND the body is non-empty) the middleware sets status 400 with a small text body and does NOT call next. On success the cached tree is available to the inner handler via httpd-req-json, which returns the same pointer.

Tip: only wrap routes that genuinely require a JSON body; an empty body or a route that does not expect JSON should not 400. Check (= "application/json" (httpd-req-header c "Content-Type")) at the route layer if you want stricter gating.

(let [composed (compose-middleware base mw-json-body)]
  (httpd-new 0 composed))

Since: Phase H8 (M3)

defn

httpd-mw-json-body-precheck

(httpd-mw-json-body-precheck [conn :ptr<void>] :int)

internal: returns 1 when the body is non-empty but json/decode failed, 0 otherwise. Used by mw-json-body to drive the 400 short-circuit; lives as a fixed-arity helper so the if-form inside mw-json-body can stay in Turmeric.

defstruct

CorsOpts

(defstruct CorsOpts [])

mw-cors configuration.

Field conventions:

allow-origin      -- "*" for any origin (no credentials), or one origin,
                     or a comma-separated list of origins, each matched
                     exactly and echoed back with `Vary: Origin`
allow-methods     -- comma-separated list emitted on preflight
allow-headers     -- comma-separated list emitted on preflight
expose-headers    -- comma-separated list emitted on normal responses
                     (or "" to omit)
allow-credentials -- emit Allow-Credentials: true (not with "*")
max-age           -- preflight cache lifetime in seconds, or -1 to omit

tourist-on-stdlib-httpd H4

Since: Phase H8 (M4); origin list and bool credentials since

defn

default-cors-opts

(default-cors-opts :CorsOpts)

permissive defaults suitable for development.

allow-origin = "*", standard set of methods, Content-Type allowed, no expose-headers, no credentials, no max-age. Tighten for prod.

Since: Phase H8 (M4)

defn

cors-blank?

(cors-blank? [s :cstr] :bool)

internal: is this string NULL, empty or all blanks?

defn

cors-wildcard?

(cors-wildcard? [allow-origin :cstr] :bool)

internal: is this allow-origin the "*" wildcard?

defn

cors-opts

(cors-opts [allow-origin :cstr allow-methods :cstr allow-headers :cstr expose-headers :cstr allow-credentials :bool max-age :int] :(Result CorsOpts cstr))

build a CorsOpts, refusing an invalid combination.

The checked counterpart of (make-struct CorsOpts ...): credentials with the "*" origin is an error (the CORS spec forbids it and browsers reject the response), as is an empty allow-origin.

allow-origin"*", one origin, or a comma-separated list
allow-methodscomma-separated methods for preflight
allow-headerscomma-separated request headers for preflight
expose-headerscomma-separated response headers, or ""
allow-credentialssend Access-Control-Allow-Credentials: true
max-agepreflight cache seconds, or -1

(Result CorsOpts cstr) -- ok(opts), or err(reason).

(cors-opts "https://app.example.com, https://admin.example.com"
           "GET, POST" "Content-Type" "" true 600)
(cors-opts "*" "GET" "" "" true -1)   ; => err

Since: tourist-on-stdlib-httpd H4

defopaque

CorsConfig

(defopaque CorsConfig :int)

internal: one mw-cors instance's own copy of its settings.

A heap block HttpdCorsCfg (see httpd-cors-config-new) that lives until process exit.

defn

httpd-cors-config-new

(httpd-cors-config-new [allow-origin :cstr allow-methods :cstr allow-headers :cstr expose-headers :cstr allow-credentials :bool max-age :int] :CorsConfig)

internal: copy CORS settings into a heap block.

Credentials with the "*" origin are dropped (fail closed) with a line on stderr. The block is registered for release at process exit.

defn

httpd-mw-cors-is-preflight

(httpd-mw-cors-is-preflight [conn :ptr<void>] :int)

internal: 1 iff this is a CORS preflight.

defn

httpd-mw-cors-allow-origin

(httpd-mw-cors-allow-origin [conn :ptr<void> cfg :CorsConfig] :bool)

internal: set Access-Control-Allow-Origin (and Allow-Credentials) for this request per the config; returns true when the origin was allowed. A listed-origin config always adds Vary: Origin, allowed or not, since the answer depends on it.

defn

httpd-mw-cors-preflight

(httpd-mw-cors-preflight [conn :ptr<void> cfg :CorsConfig] :nil)

internal: answer a preflight (204).

defn

httpd-mw-cors-decorate

(httpd-mw-cors-decorate [conn :ptr<void> cfg :CorsConfig] :nil)

internal: decorate a normal-flow response.

defn

mw-cors-with

(mw-cors-with [allow-origin :cstr allow-methods :cstr allow-headers :cstr expose-headers :cstr allow-credentials :bool max-age :int ^fat next :(fn [ptr<void>] nil)] :ptr<void>)

CORS middleware factory (positional args).

(mw-cors-with ao am ah eh ac ma) is a one-arg function suitable for compose-middleware: given a downstream handler next, returns a wrapped handler that either short-circuits a preflight or decorates the normal response. mw-cors-opts is the CorsOpts-valued form.

See CorsOpts / default-cors-opts for field semantics.

(compose-middleware base
  (mw-cors-with "https://app.example.com, https://admin.example.com"
                "GET, POST, OPTIONS"
                "Content-Type, Authorization"
                "" true 60))

tourist-on-stdlib-httpd H4

Since: Phase H8 (M4); origin list and bool credentials since

defn

mw-cors

(mw-cors [^fat next :(fn [ptr<void>] nil)] :ptr<void>)

CORS middleware with permissive defaults.

(mw-cors next) applies the default-cors-opts settings: allow-origin *, standard methods, Content-Type allowed, no credentials. Use mw-cors-opts for customised settings.

(compose-middleware base mw-cors)

Since: Phase H8 (M4)

defn

mw-cors-opts

(mw-cors-opts [opts :CorsOpts ^fat next :(fn [ptr<void>] nil)] :ptr<void>)

CORS middleware factory configured by a CorsOpts struct.

(mw-cors-opts opts next) returns a wrapped handler; the curried form (mw-cors-opts opts) is a one-arg middleware value suitable for compose-middleware:

(compose-middleware base (mw-cors-opts my-opts))

Build opts with cors-opts to have an invalid combination refused up front; options that slip through (made with make-struct) are served fail-closed as described in the section notes.

optsa CorsOpts value (see default-cors-opts, cors-opts)
nextdownstream handler closure

A wrapped handler closure (:ptr<void>) suitable for httpd-new / compose-middleware.

(let [opts     (ok-val (cors-opts "https://app.example.com" "GET, POST"
                                  "Content-Type" "" true 600))
      composed (compose-middleware base (mw-cors-opts opts))]
  (httpd-new 0 composed))

Since: Phase H8 (M4)

defn

cstr-eq-const-time

(cstr-eq-const-time [a :cstr b :cstr] :int)

length-then-constant-time XOR cstring compare.

Returns 1 when both strings have the same length AND all bytes match, 0 otherwise. The byte comparison runs over the full length even on the first mismatch, hiding the position of the difference from a timing observer. The length check is variable-time (different lengths => clearly different secrets), which is the standard trade-off for cstring credential checks.

Since: Phase H8 (M5)

defn

httpd-mw-basic-auth-check

(httpd-mw-basic-auth-check [conn :ptr<void> verifier :int] :int)

internal: decode + dispatch the verifier.

defn

httpd-basic-auth-decode-raw

(httpd-basic-auth-decode-raw [hdr :cstr] :cstr)

internal: the user:pass an Authorization header carries.

Accepts Basic <base64> (scheme case-insensitive). Shared by mw-basic-auth and tourist's basic-auth. Returns a fresh malloc'd user:pass string the caller frees, or NULL when the header is NULL, not Basic, not valid base64, has no ':' in it, or decodes to a NUL byte (which would let "secret\0junk" be checked as "secret").

Since: tourist-on-stdlib-httpd H5

defn

httpd-mw-basic-auth-challenge

(httpd-mw-basic-auth-challenge [conn :ptr<void> realm :cstr] :nil)

internal: emit 401 + WWW-Authenticate.

defn

mw-basic-auth

(mw-basic-auth [realm :cstr ^fat verifier :(fn [cstr cstr] int) ^fat next :(fn [ptr<void>] nil)] :ptr<void>)

HTTP Basic Auth middleware factory.

realm is the value placed in the WWW-Authenticate: Basic realm="..." challenge. verifier is a closure of type (fn [user :cstr pass :cstr] :int) returning 1 to authorise and 0 to reject. Use cstr-eq-const-time in the verifier body for the password check. The verifier parameter is ^fat, so a bare non-capturing verifier is auto-shimmed into a fat closure -- it need not capture a free variable to be dispatched safely.

ALWAYS run Basic Auth behind TLS (see httpd-new-tls); the encoding is reversible and traverses the wire in clear text otherwise.

;; Compare BOTH fields every time and combine the results: returning
;; early on a wrong username tells a caller which usernames exist.
(let [verify (fn [u :cstr p :cstr] :int   ; captures nothing -- fine
               (let [u-ok (cstr-eq-const-time u "admin")
                     p-ok (cstr-eq-const-time p "s3cret")]
                 (* u-ok p-ok)))
      composed (compose-middleware base (mw-basic-auth "app" verify))]
  (httpd-new 0 composed))

Since: Phase H8 (M5)

defn

httpd-req-multipart-parse

(httpd-req-multipart-parse [conn :ptr<void>] :int)

internal: parse the body on first access. Returns 1 on success (parts list populated), 0 on absent / malformed / non-multipart content.

defn

httpd-req-file

(httpd-req-file [conn :ptr<void> field :cstr] :ptr<void>)

look up a multipart part by form field name.

Triggers a lazy parse of the request body on first call; subsequent calls reuse the cached part list (freed in the iteration cleanup).

Returns an opaque part handle (:ptr<void>) usable with the httpd-part-* accessors, or 0 (NULL) when the field is absent / the body is not multipart / parsing failed.

(let [p (httpd-req-file c "upload")]
  (when (not= 0 p)
    (println (httpd-part-filename p))
    (println (int->cstr (httpd-part-data-len p)))))

Since: Phase H8 (M7)

defn

httpd-part-name

(httpd-part-name [part :ptr<void>] :cstr)

form field name of a part (or "" if missing).

defn

httpd-part-filename

(httpd-part-filename [part :ptr<void>] :cstr)

file name attribute (or "" for non-file parts).

defn

httpd-part-content-type

(httpd-part-content-type [part :ptr<void>] :cstr)

per-part Content-Type (or "" if absent).

defn

httpd-part-data

(httpd-part-data [part :ptr<void>] :ptr<void>)

raw byte buffer (binary-safe, may contain NUL).

defn

httpd-part-data-len

(httpd-part-data-len [part :ptr<void>] :int)

byte count of the part's content.

defn

httpd-async-fiber-body

(httpd-async-fiber-body [self :ptr<void> user-data :int] :nil)

internal: per-request fiber entry point.

Runs the non-blocking version of httpd-handle on the accepted fd: sets O_NONBLOCK, reads up to end-of-headers (parking on EWOULDBLOCK), reads body, dispatches the handler, writes the response (parking on EWOULDBLOCK), and loops on keep-alive. Mirrors httpd-handle. NOTE: the trailing user/user-data slot is typed : int DELIBERATELY (an exception to the no-lazy-:int rule): this defn is hand-packed into a fat box's slot 0 (see the clos[0] = ... below / reactor.c park boxes), and the reactor invokes slot 0 through the fat-entry CARRIER convention (typedefs in src/async/reactor.c) -- every value parameter is int64_t. A ptr<void> spelling emits a void * C parameter, which -fsanitize=function flags (and CFI/CET-BTI/WASM reject) at every dispatch.

defn

httpd-async-accept-cb

(httpd-async-accept-cb [env :ptr<void> id :int events :int user :int] :nil)

internal: reactor READ callback on the listen fd.

Drains accept() and spawns a fiber per accepted connection. Honors the A4 in-flight cap: when ha->in_flight >= ha->max_in_flight, sends a quick HTTP/1.0 503 and closes instead of spawning. NOTE: the trailing user/user-data slot is typed : int DELIBERATELY (an exception to the no-lazy-:int rule): this defn is hand-packed into a fat box's slot 0 (see the clos[0] = ... below / reactor.c park boxes), and the reactor invokes slot 0 through the fat-entry CARRIER convention (typedefs in src/async/reactor.c) -- every value parameter is int64_t. A ptr<void> spelling emits a void * C parameter, which -fsanitize=function flags (and CFI/CET-BTI/WASM reject) at every dispatch.

defn

httpd-new-async-with-limit

(httpd-new-async-with-limit [port :int ^fat handler :(fn [ptr<void>] nil) max-in-flight :int])

create an async (fiber-per-request) httpd handle.

Binds the listener to a single-thread reactor + LocalFiberGroup. Each accepted connection runs as a fiber that uses non-blocking I/O and parks on the reactor when it would block; handlers may also park explicitly via the httpd-await-* primitives.

portTCP port to listen on (0 = kernel-assigned)
handlerfat closure: (fn [conn :ptr<void>] :nil)

ptr<void> -- HttpdAsync handle (or NULL on error)

Lifecycle:

(let [h (httpd-new-async 0 handler)]
  (httpd-run-async h)         ;; blocks until httpd-stop-async fires
  (httpd-async-free h))

Since: Phase H8 (A0+A1)

defn

httpd-new-async

(httpd-new-async [port :int ^fat handler :(fn [ptr<void>] nil)])

create an async server capped at 512 in-flight requests.

Shortcut for (httpd-new-async-with-limit port handler 512): past 512 live request fibers a new connection is answered 503 and closed. 512 sits under the common 1024 open-file soft limit, so a flood is refused before it exhausts the process's descriptors. Use the -with-limit variant to choose another cap (0 = unlimited).

Since: Phase H8 (A0+A1)

defn

httpd-run-async

(httpd-run-async [h :ptr<void>])

drive the async server until stopped.

Blocks the calling thread, running the fiber-per-request loop. Returns when httpd-stop-async fires (or the reactor exits for any other reason).

Since: Phase H8 (A0+A1)

defn

httpd-stop-async

(httpd-stop-async [h :ptr<void>])

request a graceful shutdown of the async server.

Sets the stop flag, removes the listener source from the reactor, and wakes the reactor so httpd-run-async returns. In-flight fibers finish naturally when their reads/writes complete.

Since: Phase H8 (A0+A1)

defn

httpd-async-free

(httpd-async-free [h :ptr<void>] :nil)

tear down an async server handle.

Free the fiber group, reactor, and synthetic closures. Call only after httpd-run-async has returned.

Since: Phase H8 (A0+A1)

defn

httpd-async-port

(httpd-async-port [h :ptr<void>] :int)

read the bound TCP port on an async server.

Useful when port=0 was passed to httpd-new-async (kernel-assigned).

Since: Phase H8 (A0+A1)

defn

httpd-await-readable

(httpd-await-readable [conn :ptr<void>] :nil)

suspend the current fiber until conn fd is readable.

Wraps local-park-fd for the READ event with no timeout. Must be called from inside a request fiber spawned by httpd-new-async; in the blocking thread-pool path it is a no-op (the fiber group is NULL).

Since: Phase H8 (A0)

defn

httpd-await-writable

(httpd-await-writable [conn :ptr<void>] :nil)

suspend until conn fd is writable.

defn

httpd-await-timer

(httpd-await-timer [conn :ptr<void> ms :int] :nil)

suspend the current fiber for ms milliseconds.

Built on local-park-fd with timeout-only: the reactor still polls the conn fd (no events of interest); when the timeout elapses the fiber resumes. Suitable for per-request deadlines, exponential back-off, etc.

Since: Phase H8 (A0)

defn

router-new

(router-new :ptr<void>)

create an empty router.

ptr<void> -- opaque Router handle

Since: Phase H6

defn

router-add

(router-add [r :ptr<void> method :cstr pattern :cstr ^fat handler :(fn [ptr<void>] nil)] :nil)

register a route with the given method, pattern, and handler.

The handler closure is called when an incoming request's method matches and its path matches the pattern. Path segments starting with ':' capture the corresponding request segment; the value can be read inside the handler via httpd-param.

rrouter handle
methodHTTP method ("GET", "POST", etc.)
patternpath pattern (e.g. "/users/:id")
handlerfat closure: (fn [conn :ptr<void>] :nil)

Since: Phase H6

defn

router-free

(router-free [r :ptr<void>] :nil)

free a router and all registered routes.

rrouter handle

Since: Phase H6

defn

router-dispatch

(router-dispatch [r :ptr<void> conn :ptr<void>] :nil)

match the current request against the router's table.

On match: attaches captured path params to conn, calls the route's handler with conn, then frees the params. On no match: sets a 404 response with body "Not Found".

rrouter handle
connconnection handle (the one passed to the httpd handler)

Since: Phase H6

defn

httpd-param

(httpd-param [conn :ptr<void> name :cstr] :cstr)

read a captured route parameter set by router-dispatch.

connconnection handle passed to the route handler
nameparameter name (e.g. "id" for a ":id" pattern segment)

cstr -- the captured value, or "" if no parameter with that name

;; pattern "/users/:id" matched against "/users/42"
(httpd-param conn "id")  ; => "42"

Since: Phase H6

defn

httpd-call

(httpd-call [handler :int conn :ptr<void>] :nil)

invoke a Turmeric handler closure on a connection.

handler is an already-erased fat-closure handle (:int) -- typically a captured next threaded through a middleware, or a value produced by compose-middleware. Handlers entering the server from user code arrive through a ^fat parameter (httpd-new, the mw-* factories, mw-basic-auth) that auto-shims a bare non-capturing fn, so by the time it reaches httpd-call it already carries a valid { thunk, env } layout.

handlerhandler closure value (int)
connconnection handle from the worker

Since: Phase H7

defmacro

defroute

(defroute [router method pattern handler])

macro sugar for (router-add router method pattern handler).

(defroute r "GET" "/" (fn [c :ptr<void>] :nil ...))

Since: Phase H6

defn

httpd-mw-mono-ms

(httpd-mw-mono-ms :int)

monotonic clock helper (milliseconds). Internal.

defn

httpd-mw-log-line

(httpd-mw-log-line [c :ptr<void> elapsed-ms :int] :cstr)

internal: the log line for a completed request.

METHOD PATH -> STATUS BYTES MSms, then ip=ADDR when mw-trust-proxy resolved a client address and rid=ID when mw-request-id assigned an id. No trailing newline. The string belongs to the request.

defn

httpd-mw-log-emit

(httpd-mw-log-emit [c :ptr<void> elapsed-ms :int] :nil)

emit one log line for a completed request. Internal.

defn

mw-log

(mw-log [^fat next :(fn [ptr<void>] nil)] :ptr<void>)

request logging middleware (stdout).

Wraps a base handler so every request prints one line to stdout after the handler returns. The line carries the request method, path, the response status (defaulting to 200 if unset), the response body length in bytes, and the elapsed wall time in milliseconds -- then ip=ADDR when an mw-trust-proxy outside it resolved the client address, and rid=ID when an mw-request-id outside it assigned a request id. mw-log-to sends the same line somewhere else.

nextdownstream handler closure (fat-closure :int)

A wrapped handler closure (:int) with the same calling convention as handlers passed to httpd-new / httpd-new-pool.

(let [base     (fn [c :ptr<void>] :nil
                 (httpd-resp-status! c 200)
                 (httpd-resp-body!   c "hi"))
      composed (mw-log base)]
  (httpd-new 0 composed))

Since: Phase H8 (M1)

defn

httpd-mw-log-sink-call

(httpd-mw-log-sink-call [sink :int line :cstr] :nil)

internal: hand a line to a mw-log-to sink closure.

defn

mw-log-to

(mw-log-to [^fat sink :(fn [cstr] nil) ^fat next :(fn [ptr<void>] nil)] :ptr<void>)

request logging middleware with your own sink.

mw-log's line (see mw-log), handed to sink instead of printed: write it to a file, a structured logger, a ring buffer for a test. The string is borrowed and lives until the request is finished -- copy it to keep it. The sink runs on the worker that served the request, so several can run at once.

sink(fn [cstr] nil) called once per request with the line (no newline)
nextdownstream handler closure

A wrapped handler closure for httpd-new / compose-middleware.

(mw-log-to (fn [line : cstr] : nil (eprintln line)) base)

Since: tourist-on-stdlib-httpd H4

defmacro

compose-middleware

(compose-middleware [base & mws])

wrap a base handler with multiple middlewares.

basebase handler closure (:int)
mwsvariadic middleware closures (each (fn [next :int] :int)),
applied right-to-left so the leftmost ends up outermost.

The fully-wrapped handler closure (:int).

(let [composed (compose-middleware base mw-log)]
  (httpd-new 0 composed))

;; Multiple middlewares (mw-log is outermost):
(let [composed (compose-middleware base mw-log mw-cors mw-auth)]
  (httpd-new 0 composed))

compose-middleware -- nest multiple middlewares around a base handler.

Macro: (compose-middleware base mw1 mw2 mw3) expands to (mw1 (mw2 (mw3 base))), i.e. each mw_i is called as a one-argument function with the wrapped downstream handler. The leftmost middleware ends up as the OUTERMOST wrapper -- its pre-hook runs first and its post-hook runs last.

Each mw_i is either a name callable with one argument -- typically a defn like mw-log (shape (defn mw-log [next :int] ... (fn [c] ...))) -- or a call form missing its last argument, like (mw-cors-opts opts), which gets the wrapped downstream handler appended as that argument: (compose-middleware base (mw-cors-opts o) (mw-rate-limit r)) is (mw-cors-opts o (mw-rate-limit r base)). That makes every layer a direct call, so each ^fat next parameter accepts the handler the layer inside it returned. (It used to expand to ((mw-cors-opts o) inner), which type-checked for one such form but not for two.) A (fn ...) literal is called with the handler instead.

Since: Phase H8 (M8 core)

defopaque affine

Handler

(defopaque Handler :int :affine)

an owned, composed request-handler onion (the result of compose-middleware-of / a folded middleware chain).

:affine (use-at-most-once, move-only) is the key: a bound Handler the owner neither MOVES (hands to a server via httpd-handler-carrier) nor BORROWS (invokes via (:: h :int) / httpd-call) is AUTO-DROPPED at scope exit through its Drop instance -- TUR_CLOSURE_DROP walks the captured next boxes down to the base handler, reclaiming the whole onion. This retires the explicit httpd-mw-drop for the runtime-built-chain case: bind the fold result as a Handler and the compiler drops it for you. The carrier is the same int64 fat-closure handle every httpd handler rides, so a Handler bridges to :int / (fn [int] int) by ascription.

Since: automatic R3a (closure-drop-glue)

defn

httpd-handler-drop

(httpd-handler-drop [h :Handler] :void)

internal: reclaim a composed Handler onion.

Routes to TUR_CLOSURE_DROP, which recovers the env's drop-glue header and walks the owning captures (the wrapped downstream handlers) before freeing -- the whole onion down to the base, released exactly once. Consumes its affine argument. This is the Drop instance's method; it is not normally called by hand (the scope-exit auto-drop calls it).

Since: automatic R3a (closure-drop-glue)

definstance

Drop[Handler]

(definstance Drop [Handler])

reclaim the onion when a bound Handler dies unmoved/unborrowed.

defn

httpd-handler-carrier

(httpd-handler-carrier [h :Handler] :(fn [ptr<void>] nil))

CONSUME a Handler, yielding its raw int64 carrier for a server constructor (^fat handler : (fn [ptr<void>] nil)) or any :int handler sink that TAKES OWNERSHIP.

Because the parameter is the affine Handler itself, passing a bound Handler here MOVES it -- which suppresses the scope-exit auto-drop, so the server (whose httpd-free / httpd-async-free already TUR_CLOSURE_DROPs its handler) becomes the SOLE owner with no double-free. Use this at a server handoff; use (:: h :int) (a non-consuming read) only for a BORROW where the owner keeps the Handler and lets it auto-drop.

hthe composed Handler onion (consumed).

The raw int64 fat-closure carrier.

(let [composed (compose-middleware-of base mw1 mw2)]
  (httpd-new-pool 0 1 (httpd-handler-carrier composed)))

Since: automatic R3a (closure-drop-glue)

defopaque affine

ClosureChain

(defopaque ClosureChain :int :affine)

an owned runtime-built middleware chain: a cons list whose heads are fresh, uniquely-owned factory closures and whose spine cells the builder owns. httpd-mw-fold only BORROWS it (applies each head once); once the fold has produced its onion the whole chain -- heads and cells -- is dead.

:affine (same discipline as Handler): a bound ClosureChain the builder neither MOVES nor BORROWS ((:: chain :int)) is AUTO-DROPPED at scope exit via its Drop instance, which walks the spine dropping each head (TUR_CLOSURE_DROP) and freeing each cell. This is the automatic-R3a replacement for the explicit httpd-mw-free-chain.

SOUNDNESS: sound iff each head is a DISTINCT, uniquely-owned closure (a fresh (make-mw) per cell) -- the same contract httpd-mw-free-chain documents. Do NOT cons the same closure value into the list twice (double-drop), and do NOT wrap a chain whose heads are the borrowed & mws of compose-middleware-of (those heads belong to the variadic caller; use httpd-mw-free-spine, which frees ONLY the cells).

Since: automatic R3a (closure-drop-glue)

defn

httpd-chain-drop

(httpd-chain-drop [c :ClosureChain] :void)

internal: reclaim a ClosureChain (drop each head factory via TUR_CLOSURE_DROP + free each spine cell). Consumes its affine argument; it is the Drop instance's method, normally invoked by the scope-exit auto-drop.

Since: automatic R3a (closure-drop-glue)

definstance

Drop[ClosureChain]

(definstance Drop [ClosureChain])

reclaim a runtime-built chain when its builder binding dies unmoved/unborrowed.

defn

httpd-mw-apply

(httpd-mw-apply [mw :int cur :int] :int)

internal: apply one fat-closure middleware to a handler.

The middleware is a fat closure of shape (fn [next :int] :ptr<void> ...); this performs a single fat-closure dispatch -- the same ascription idiom as httpd-call -- and returns the wrapped handler. It holds no cons-layout knowledge; the list walk lives in httpd-mw-fold. The caller guards against a null mw.

The dispatch ascription MUST name the middleware's REAL type, (fn [int] ptr<void>) -- the emitted call goes through a typed-thunk typedef derived from this ascription, and the lambda's definition is emitted from its own declared type. The old (fn [int] int) spelling made the two disagree on the return type: works on every SysV/AAPCS ABI (same return slot), but it is UB that -fsanitize=function reports and that CFI / CET-BTI / WASM call_indirect turn into a hard failure. The wrapped handler then crosses back to the :int carrier via a value ascription, which is a representation read, not a function-type cast. See docs/archive/emitter-thunk-type-return-mismatch.md.

defn

httpd-mw-fold

(httpd-mw-fold [base :int mws :int] :int)

internal: right-fold a cons-list of fat-closure middlewares over a base handler. Each cell holds a fat closure value of shape (fn [next :int] :ptr<void> ...). Applied right-to-left so the list head becomes the outermost wrapper. Null cells are skipped. Unbounded -- the walk recurses over the cons-list abstraction (list-head/list-tail), so there is no fixed element cap.

defn

httpd-mw-free-spine

(httpd-mw-free-spine [mws :int] :nil)

internal: free the cons-cell spine of a variadic middleware rest-list, leaving the head VALUES untouched. compose-middleware-of owns the fresh & mws list; httpd-mw-fold only walks it (applying each head once), so once the fold has built the wrapper chain the spine cells are dead and can be reclaimed. The head factories are borrowed (the caller frees them at its own scope exit), so this frees ONLY the cells, never the heads.

defn

httpd-mw-drop

(httpd-mw-drop [handler :int] :nil)

SUPERSEDED: reclaim a raw-:int composed handler onion.

Retired in favor of the Handler auto-drop (automatic R3a): bind the fold result as a Handler ((:: (httpd-mw-fold base chain) :Handler), or use compose-middleware-of, which returns Handler) and the compiler drops the onion for you at scope exit -- no explicit call, and no risk of forgetting it. Kept only for a legacy raw-:int onion that was never bound as a Handler; new code should not use it.

Releases the outermost wrapper AND walks the captured downstream next boxes via the env's drop-glue header, so the whole onion (down to the base handler) is freed.

handlerthe composed handler closure (:int); 0 is a no-op.

nothing.

Example (legacy -- prefer the Handler auto-drop):

(let [composed (:: (httpd-mw-fold base chain) :Handler)]
  (httpd-call (:: composed :int) conn))   ; composed auto-drops at scope exit

Since: R3a; superseded by Handler auto-drop (automatic R3a).

defn

httpd-mw-free-chain

(httpd-mw-free-chain [mws :int] :nil)

SUPERSEDED: free a runtime-built middleware cons list (the INPUT to httpd-mw-fold): drop each head factory closure AND free each spine cell.

Retired in favor of the ClosureChain auto-drop (automatic R3a half 2): bind the runtime chain as a ClosureChain ((:: (build-chain n (tnil)) :ClosureChain)) and the compiler walks the spine, dropping each head and freeing each cell, at scope exit -- no explicit call. This function's body is exactly the Drop [ClosureChain] method; kept only for a legacy raw-:int chain that was never bound as a ClosureChain.

Unlike httpd-mw-free-spine (which leaves the heads, because the variadic compose-middleware-of caller owns and frees them), a hand-built runtime chain (e.g. from a build-chain loop) is owned entirely by its builder: the fold only BORROWS each factory (applies it once, discards it), so once the fold has produced the composed onion the whole list -- factories and cells -- is dead and the builder reclaims it. Sound iff each head is a distinct, uniquely-owned closure (a fresh (make-mw) per cell); do NOT cons the same closure value into the list twice (that would double-drop it).

mwsthe middleware cons list (:int); 0 is a no-op.

nothing.

Example (legacy -- prefer the ClosureChain auto-drop):

(let [chain    (:: (build-chain 70 (tnil)) :ClosureChain)
      composed (:: (httpd-mw-fold base (:: chain :int)) :Handler)]
  (httpd-call (:: composed :int) conn))  ; composed + chain auto-drop at scope exit

Since: R3a; superseded by ClosureChain auto-drop (automatic R3a half 2).

defn

compose-middleware-of

(compose-middleware-of [A])

runtime variadic form of compose-middleware.

This is a defn, not a macro. Use when the chain is built at runtime (e.g. dependent on config, feature flags, or a runtime-chosen subset). Apply order matches compose-middleware: leftmost = outermost.

Each rest argument must be a *fat closure value* of shape (fn [next :int] :ptr<void> ...) -- i.e. a closure that captures at least one variable. A bare top-level defn name is NOT a fat closure; passing one will compile (thanks to the docs/archive/history/variadic-rest-closure-cast-plan.md fix) but the fat- closure dispatch dereferences code bytes as the fn pointer and BUS-faults. Bridge a bare defn through a let-captured fn literal:

(let [_ 0
      mw-log-val (fn [n :int] :ptr<void> (mw-log n))
      _ 0]
  (compose-middleware-of base mw-log-val ...))

For statically-known chains the macro form compose-middleware remains the better choice -- it accepts bare defns and catches non-callable mistakes at elaboration.

basebase handler, a (fn [ptr<void>] nil) closure
mwsvariadic fat-closure middlewares; type variable A unifies
to whatever closure type the call site uses.

The fully-wrapped handler onion as an owned Handler -- bind it and the compiler auto-drops it at scope exit (no explicit httpd-mw-drop), or hand it to a server via httpd-handler-carrier (which moves it, suppressing the auto-drop so the server owns it).

docs/archive/history/variadic-rest-closure-cast-plan.md.
Returns `Handler` since automatic R3a (closure-drop-glue).

Since: Phase H8 (M8 core); requires the V1 cast fix in

defn

httpd-mw-content-length

(httpd-mw-content-length [conn :ptr<void>] :int)

internal: read Content-Length as int64, or -1.

Returns -1 when the header is absent or unparseable. A non-negative return value is the announced body size in bytes.

defn

mw-body-size

(mw-body-size [max-bytes :int ^fat next :(fn [ptr<void>] nil)] :ptr<void>)

reject requests with Content-Length > max-bytes with 413.

When the request advertises a Content-Length greater than max-bytes this middleware responds with status 413 ("Payload Too Large") and a short plain-text body, and does NOT invoke next. Requests without a Content-Length header, or with a value at or below the cap, are passed through unchanged.

The cap is on the announced size. It runs after the server has read the body, so it limits what a handler accepts, not what the server buffers -- httpd-set-max-body! is the ceiling that stops the read itself.

max-bytesmaximum accepted Content-Length in bytes (>= 0)
nextdownstream handler closure (fat-closure :int)

A wrapped handler closure (:ptr<void>) suitable for httpd-new / compose-middleware.

(let [composed (mw-body-size 1048576 base)]  ; 1 MiB cap
  (httpd-new 0 composed))

Since: Phase H8 (MW1)

defn

httpd-set-attr!

(httpd-set-attr! [conn :ptr<void> key :cstr val :cstr] :nil)

attach a key/value pair to the current request.

Overwrites any previously-set value for the same key (case-sensitive). The key and value are copied into per-request storage; the original cstrings can be reused or freed by the caller. The attribute lives until the handler returns, then is freed alongside the request / response header lists.

connhttpd connection pointer
keyattribute name (cstr)
valattribute value (cstr)
(httpd-set-attr! c "user" "alice")

Since: Phase H8 (MW2)

defn

httpd-req-attr

(httpd-req-attr [conn :ptr<void> key :cstr] :cstr)

retrieve a previously set request attribute.

Returns the empty string ("") if the key has not been set. The returned pointer is into per-request storage and is only valid until the handler returns.

connhttpd connection pointer
keyattribute name (cstr, case-sensitive)

The attribute value as a cstr, or "" when absent.

(let [u (httpd-req-attr c "user")] ...)

Since: Phase H8 (MW2)

defn

httpd-req-remote-ip

(httpd-req-remote-ip [conn :ptr<void>] :cstr)

return the client's IP address as a cstr.

Uses getpeername(2) on the connection fd. IPv4 addresses are returned in dotted-quad form ("203.0.113.4"); IPv6 in colon-hex form. Returns the empty string ("") on lookup failure. The returned pointer is into per-request storage and is valid only for the duration of the handler call.

connhttpd connection pointer

The client IP as a cstr, or "" on failure.

(let [ip (httpd-req-remote-ip c)] ...)

Since: Phase H8 (MW2)

defstruct

RateLimitOpts

(defstruct RateLimitOpts [requests :int window-s :int])

mw-rate-limit configuration.

Field conventions:

requests -- max requests per window (>= 1)
window-s -- window length in seconds (>= 1)

Since: Phase H8 (MW2)

defn

httpd-mw-ratelimit-new

(httpd-mw-ratelimit-new [requests :int window-s :int] :int)

internal: allocate a fresh RL state struct.

defn

httpd-mw-ratelimit-check

(httpd-mw-ratelimit-check [state :int ip :cstr] :int)

internal: bump the counter for ip; return 0 if the request is allowed, or N>0 = the seconds until the window ends when the request must be rejected.

defn

httpd-mw-ratelimit-emit-429

(httpd-mw-ratelimit-emit-429 [conn :ptr<void> retry-s :int] :nil)

internal: write the 429 response.

defn

mw-rate-limit

(mw-rate-limit [opts :RateLimitOpts ^fat next :(fn [ptr<void>] nil)] :ptr<void>)

sliding-window IP-based rate limiter middleware.

Each mw-rate-limit instance allocates its own counter table. When the same client IP exceeds requests requests within window-s seconds the middleware responds with 429 Too Many Requests plus a Retry-After: <seconds> header without invoking next.

Multiple mw-rate-limit instances get independent counters; share by reusing the same wrapped closure across compositions.

The client is keyed by httpd-req-ip: the peer address, or -- behind an mw-trust-proxy that runs outside this middleware -- the client address it read from X-Forwarded-For. Without that, every client behind one proxy shares the proxy's bucket.

optsRateLimitOpts
nextdownstream handler closure (fat-closure :int)

A wrapped handler closure (:ptr<void>) suitable for httpd-new / compose-middleware.

(let [composed (mw-rate-limit (make-struct RateLimitOpts 60 60) base)]
  (httpd-new 0 composed))

Since: Phase H8 (MW2)

defn

httpd-mw-static-mime

(httpd-mw-static-mime [path :cstr] :cstr)

internal: map an extension to a Content-Type.

defn

httpd-mw-static-try

(httpd-mw-static-try [conn :ptr<void> root :cstr] :int)

internal: attempt to serve path from root relative to the request req-path. Returns 1 on success (response written), 0 if the file is not found / forbidden.

defn

httpd-static-listing!

(httpd-static-listing! [conn :ptr<void> dir :cstr path :cstr plen :int] :bool)

internal: answer with an HTML listing of dir (an already-resolved directory), titled with the first plen bytes of path. Dotfiles are left out; directories come first, then files, each group byte-wise. Returns false (nothing written) when dir cannot be read.

defn

httpd-static-serve-with-flags!

(httpd-static-serve-with-flags! [conn :ptr<void> root :cstr rel :cstr index-file :cstr listing :bool] :bool)

internal: httpd-static-serve-with! with the StaticOpts fields passed one by one (index-file "" = none).

defn

httpd-static-serve!

(httpd-static-serve! [conn :ptr<void> root :cstr rel :cstr] :bool)

answer a request with a file under a root directory.

mw-static's file serving, for callers that pick the path themselves (a framework mapping /assets/* onto a directory, say). rel is joined onto root (its query string ignored); the file must resolve, symlinks and all, to a regular file inside root, and no segment of rel may start with '.' (.well-known excepted) or contain a backslash. A hit sets status 200, the body (binary-safe), Content-Type from the extension and a strong ETag of "<size hex>-<mtime hex>"; an If-None-Match naming that tag (or *) gets 304 with the ETag and no body.

connconnection handle from the worker
rootfilesystem directory to serve from
relthe path to look up under it, e.g. "/css/site.css"

bool -- true when it answered (200 or 304), false on a miss (nothing written).

(when (not (httpd-static-serve! c "./public" "/index.html"))
  (httpd-resp-status! c 404))

Since: tourist-on-stdlib-httpd H5

defstruct

StaticOpts

(defstruct StaticOpts [index :cstr listing :bool])

what the static path does with a request for a directory.

Fields:

index   -- a file to serve for a directory path, e.g. "index.html";
           "" serves none
listing -- render an HTML listing when there is no index
(mw-static-with (StaticOpts "index.html" true) "./public" base)

Since: project-less-tools-plan PT0

defn

httpd-static-serve-with!

(httpd-static-serve-with! [conn :ptr<void> opts :StaticOpts root :cstr rel :cstr] :bool)

httpd-static-serve!, with an index and a listing.

The same file path, containment and dotfile rules as httpd-static-serve!. A request that resolves to a directory, which httpd-static-serve! treats as a miss, is answered according to opts:

1. without a trailing '/' -- 301 to the same path with one, so relative
   links in the page resolve inside the directory;
2. with `(.index opts)` naming a regular file in it -- that file, ETag
   and all;
3. with `(.listing opts)` -- an HTML listing (text/html, no ETag):
   directories first, names escaped and percent-encoded, size and
   modification time, dotfiles left out;
4. otherwise a miss.

With (StaticOpts "" false) it is exactly httpd-static-serve!.

connconnection handle from the worker
optsthe index file and whether to list a directory
rootfilesystem directory to serve from
relthe path to look up under it, e.g. "/docs/"

bool -- true when it answered (200, 301 or 304), false on a miss.

(when (not (httpd-static-serve-with! c (StaticOpts "index.html" true) "./site" (httpd-req-path c)))
  (httpd-resp-status! c 404))

Since: project-less-tools-plan PT0

defn

httpd-mw-static-fallback

(httpd-mw-static-fallback [conn :ptr<void> root :cstr] :nil)

internal: when the response status is 404, try to serve a file from root; otherwise no-op. Always returns nil.

defn

httpd-mw-static-fallback-with

(httpd-mw-static-fallback-with [conn :ptr<void> root :cstr index-file :cstr listing :bool] :nil)

internal: mw-static-with's fallback; the StaticOpts fields passed one by one.

defn

mw-static

(mw-static [root-dir :cstr ^fat next :(fn [ptr<void>] nil)] :ptr<void>)

serve files from a root directory when next returns 404.

Composes after a router: next is consulted first; only when next sets the response status to 404 (the default-handler signal that no route matched) does mw-static attempt to serve a file. The request path is joined onto root-dir and .. segments are rejected as a path-traversal guard. The Content-Type is inferred from the file extension; an ETag of "<size>-<mtime>" is emitted and honoured (If-None-Match matching returns 304 without resending the body).

root-dirfilesystem path to serve files from
nextdownstream handler closure (fat-closure :int)

A wrapped handler closure (:ptr<void>).

(let [composed (mw-static "./public" router-handler)]
  (httpd-new 0 composed))

Since: Phase H8 (MW2)

defn

mw-static-with

(mw-static-with [opts :StaticOpts root-dir :cstr ^fat next :(fn [ptr<void>] nil)] :ptr<void>)

mw-static, with an index file and a directory listing.

mw-static treats a request for a directory as a miss. This one answers it the way opts says (httpd-static-serve-with! has the rules): a 301 to add the trailing '/', the index file when there is one, else a listing when (.listing opts) is set. Both are opt-in so a framework composing mw-static after its router does not grow a listing by surprise.

optsthe index file name ("" for none) and whether to list
root-dirfilesystem path to serve files from
nextdownstream handler closure

A wrapped handler closure (:ptr<void>).

(httpd-new 8000 (mw-static-with (StaticOpts "index.html" true) "." not-found))

Since: project-less-tools-plan PT0

defn

httpd-mw-recover-500

(httpd-mw-recover-500 [conn :ptr<void>] :nil)

internal: write a 500 response unless the handler already set a status before panicking. Always returns nil.

defn

mw-recover

(mw-recover [^fat next :(fn [ptr<void>] nil)] :ptr<void>)

catch a downstream panic and respond 500.

Wraps a base handler so that a panic (or any unwind) raised inside next is caught at the middleware boundary. Without it a panicking handler unwinds out of the worker and takes the process (or at best the connection) with it; with it the request gets a 500 and the server keeps serving.

Put it OUTERMOST in the middleware chain -- it can only recover panics raised by handlers it wraps, and an outer middleware's post-processing still runs on the 500 response.

nextdownstream handler closure (fat-closure :int)

A wrapped handler closure (:ptr<void>).

(let [base     (fn [c :ptr<void>] :nil (panic "boom"))
      composed (compose-middleware base mw-recover)]
  (httpd-new 0 composed))
; => client sees "500 Internal Server Error"; the server stays up

Since: Phase H8 (MW3)

defn

httpd-resp-has-header?

(httpd-resp-has-header? [conn :ptr<void> name :cstr] :bool)

has the response a header of this name yet?

Case-insensitive. Middleware that supplies a default (a security header, an ETag) checks this first so a value the route set wins.

connconnection handle from the worker
nameheader name

true when the response carries at least one header named name.

(when (not (httpd-resp-has-header? c "Cache-Control"))
  (httpd-resp-header! c "Cache-Control" "no-store"))

Since: tourist-on-stdlib-httpd H4

defstruct

SecureHeadersOpts

(defstruct SecureHeadersOpts [])

mw-secure-headers configuration.

Field conventions ("" or -1 leaves a header out):

content-type-options       -- send X-Content-Type-Options: nosniff
frame-options              -- X-Frame-Options ("DENY", "SAMEORIGIN")
referrer-policy            -- Referrer-Policy
cross-origin-opener-policy -- Cross-Origin-Opener-Policy
hsts-max-age               -- Strict-Transport-Security max-age in
                              seconds, or -1 (default) for none
hsts-include-subdomains    -- add includeSubDomains to the HSTS header
content-security-policy    -- Content-Security-Policy, "" (default) for none

HSTS and CSP are opt-in: HSTS switched on by accident locks a domain to https for its max-age, and a wrong CSP default breaks pages silently.

Since: tourist-on-stdlib-httpd H4

defn

default-secure-headers-opts

(default-secure-headers-opts :SecureHeadersOpts)

the safe defaults.

nosniff, X-Frame-Options: DENY, Referrer-Policy: no-referrer, Cross-Origin-Opener-Policy: same-origin; no HSTS, no CSP.

SecureHeadersOpts

(mw-secure-headers-opts (default-secure-headers-opts) base)

Since: tourist-on-stdlib-httpd H4

defopaque

SecureHeadersConfig

(defopaque SecureHeadersConfig :int)

internal: one mw-secure-headers instance's own copy of its headers (a heap block that lives until process exit).

defn

httpd-secure-headers-config-new

(httpd-secure-headers-config-new [content-type-options :bool frame-options :cstr referrer-policy :cstr cross-origin-opener-policy :cstr hsts-max-age :int hsts-include-subdomains :bool content-security-policy :cstr] :SecureHeadersConfig)

internal: build the header list once.

defn

httpd-mw-secure-headers-apply

(httpd-mw-secure-headers-apply [conn :ptr<void> cfg :SecureHeadersConfig] :nil)

internal: add each configured header the response does not carry already.

defn

mw-secure-headers-opts

(mw-secure-headers-opts [opts :SecureHeadersOpts ^fat next :(fn [ptr<void>] nil)] :ptr<void>)

security headers on every response (helmet).

After next returns, adds each header the options configure unless the response already has one of that name -- a route that sets its own X-Frame-Options keeps it. See SecureHeadersOpts for the headers and why HSTS and CSP are off unless asked for.

optsSecureHeadersOpts (default-secure-headers-opts for the defaults)
nextdownstream handler

A wrapped handler closure.

(let [opts (make-struct SecureHeadersOpts true "SAMEORIGIN" "no-referrer"
                        "same-origin" 31536000 true "default-src 'self'")]
  (compose-middleware base (mw-secure-headers-opts opts)))

Since: tourist-on-stdlib-httpd H4

defn

mw-secure-headers

(mw-secure-headers [^fat next :(fn [ptr<void>] nil)] :ptr<void>)

mw-secure-headers-opts with the safe defaults.

(compose-middleware base mw-secure-headers)

Since: tourist-on-stdlib-httpd H4

defn

httpd-mw-request-id-assign!

(httpd-mw-request-id-assign! [conn :ptr<void>] :nil)

internal: pick the request's ID and echo it.

An incoming X-Request-Id is kept only if it is 1-128 characters of [A-Za-z0-9._-] -- anything else (a CRLF, a 1 KiB value, a quote) is replaced, so a client cannot inject into logs or headers through it. Otherwise 128 random bits, as 32 hex digits.

defn

httpd-req-id

(httpd-req-id [conn :ptr<void>] :(Option cstr))

the request's ID, when mw-request-id assigned one.

connconnection handle from the worker

(Option cstr) -- some(id), valid for the rest of the request, or none when no mw-request-id ran.

(let [id (httpd-req-id c)]
  (when (some? id) (println (unwrap id))))

Since: tourist-on-stdlib-httpd H4

defn

mw-request-id

(mw-request-id [^fat next :(fn [ptr<void>] nil)] :ptr<void>)

give every request an ID and echo it in X-Request-Id.

Keeps a well-formed incoming X-Request-Id (1-128 of [A-Za-z0-9._-]), else generates 128 random bits as hex. The ID is readable downstream with httpd-req-id, mw-log puts it on each line, and the response carries it (a route may still replace the header).

nextdownstream handler

A wrapped handler closure.

(compose-middleware base mw-log mw-request-id)   ; mw-log sees the ID

Since: tourist-on-stdlib-httpd H4

defstruct

TrustProxyOpts

(defstruct TrustProxyOpts [trusted :cstr])

which peers may tell the server who the client is.

trusted is a comma-separated list of addresses and CIDR ranges, IPv4 or IPv6 ("10.0.0.0/8, 192.0.2.7, fd00::/8"), and the keywords loopback, linklocal and uniquelocal (the private ranges). The default trusts nobody, so X-Forwarded-For and X-Forwarded-Proto are ignored.

Since: tourist-on-stdlib-httpd H4

defn

default-trust-proxy-opts

(default-trust-proxy-opts :TrustProxyOpts)

trust no proxy.

TrustProxyOpts with an empty trusted list.

(mw-trust-proxy (default-trust-proxy-opts) base)   ; peer address only

Since: tourist-on-stdlib-httpd H4

defn

httpd-ip-parse

(httpd-ip-parse [s :cstr len :int out :ptr<void>] :bool)

internal: parse a numeric IPv4 or IPv6 address (the first len bytes of s, optionally in [brackets]) into 16 bytes, IPv4 as ::ffff:a.b.c.d. False when it is not an address.

defn

httpd-trust-parse

(httpd-trust-parse [trusted :cstr out :ptr<void> cap :int] :int)

internal: parse a trusted list into cap records {16-byte address, prefix bits} at out; returns the count, or -1 when an entry is neither an address, a CIDR nor a keyword.

defn

trust-proxy-opts

(trust-proxy-opts [trusted :cstr] :(Result TrustProxyOpts cstr))

build a TrustProxyOpts, refusing an entry that does not parse.

trustedcomma-separated addresses / CIDRs / keywords (see
TrustProxyOpts)

(Result TrustProxyOpts cstr) -- ok(opts), or err naming the problem.

(trust-proxy-opts "loopback, 10.0.0.0/8")
(trust-proxy-opts "10.0.0.0/33")          ; => err

Since: tourist-on-stdlib-httpd H4

defopaque

TrustProxyConfig

(defopaque TrustProxyConfig :int)

internal: one mw-trust-proxy instance's parsed ranges.

defn

httpd-trust-proxy-config-new

(httpd-trust-proxy-config-new [trusted :cstr] :TrustProxyConfig)

internal: parse the trusted list once. An unparsable list trusts nobody (and says so on stderr).

defn

httpd-mw-trust-proxy-resolve!

(httpd-mw-trust-proxy-resolve! [conn :ptr<void> cfg :TrustProxyConfig] :nil)

internal: work out the client address and protocol and record them on the request.

An untrusted peer is the client, and its forwarding headers are ignored. A trusted peer's X-Forwarded-For is walked right to left: each hop that is itself trusted is skipped, and the first that is not is the client (a hop that does not parse ends the walk at the last good one). X-Forwarded-Proto is believed only from a trusted peer, and only as http or https.

defn

mw-trust-proxy

(mw-trust-proxy [opts :TrustProxyOpts ^fat next :(fn [ptr<void>] nil)] :ptr<void>)

resolve the client address and protocol behind proxies.

Put it outermost: middleware inside it (mw-rate-limit, mw-log) and the handler then see the real client through httpd-req-ip / httpd-req-proto. With the default options (or without this middleware) those return the peer address and the connection's own protocol.

optsTrustProxyOpts (build with trust-proxy-opts to have a bad entry
refused; an unparsable list trusts nobody)
nextdownstream handler

A wrapped handler closure.

(compose-middleware base
  (mw-trust-proxy (ok-val (trust-proxy-opts "loopback")))
  (mw-rate-limit (make-struct RateLimitOpts 100 60)))

Since: tourist-on-stdlib-httpd H4

defn

httpd-req-ip

(httpd-req-ip [conn :ptr<void>] :cstr)

the client's IP address.

What mw-trust-proxy resolved, when it ran; otherwise the peer address (httpd-req-remote-ip). The string lives as long as the request.

connconnection handle from the worker

The address as a cstr, or "" when it cannot be read.

(httpd-req-ip c)   ; => "203.0.113.9" behind a trusted proxy

Since: tourist-on-stdlib-httpd H4

defn

httpd-req-proto

(httpd-req-proto [conn :ptr<void>] :cstr)

"https" or "http": how the client reached the server.

A trusted proxy's X-Forwarded-Proto when mw-trust-proxy ran, else whether this connection is TLS.

connconnection handle from the worker

"https" or "http".

(when (cstr-eq? (httpd-req-proto c) "http") (redirect-to-https c))

Since: tourist-on-stdlib-httpd H4

defn

httpd-mw-etag-apply

(httpd-mw-etag-apply [conn :ptr<void>] :nil)

internal: tag a 200 GET/HEAD response and answer a matching If-None-Match with 304.

defn

mw-etag

(mw-etag [^fat next :(fn [ptr<void>] nil)] :ptr<void>)

ETags and 304s for dynamic responses.

After next returns a 200 to a GET or HEAD, sets a weak ETag computed from the body (unless the route set an ETag itself, which is then the one compared), and answers a matching If-None-Match with 304 and no body -- the client reuses its copy. The body is still built every time; this saves the transfer, not the work. mw-static does the same for files from their size and mtime.

nextdownstream handler

A wrapped handler closure.

(compose-middleware api-handler mw-etag)

Since: tourist-on-stdlib-httpd H4

defn

httpd-mw-timeout-run

(httpd-mw-timeout-run [conn :ptr<void> next :int ms :int] :nil)

internal: run next on a helper thread against a private copy of the request, and wait up to ms for it.

In time, the copy's response (status, headers, body, the attributes and strings it made) moves onto the real conn. Late, the real conn is answered 503 and the copy is left to the helper thread, which frees it when the handler finally returns. The copy has no socket (fd -1), so a late handler cannot write to a connection that has moved on, and cannot upgrade it.

defn

httpd-mw-timeout-thread

(httpd-mw-timeout-thread [arg :ptr<void>] :ptr<void>)

internal: the helper thread's body.

defn

httpd-mw-timeout-free

(httpd-mw-timeout-free [arg :ptr<void>] :nil)

internal: release a job and its copy of the request.

defn

mw-timeout

(mw-timeout [ms :int ^fat next :(fn [ptr<void>] nil)] :ptr<void>)

answer 503 when a handler overruns ms milliseconds.

Runs next on a helper thread against a private copy of the request and waits up to ms. In time, its response is used as usual. Late, the client gets 503 Service Unavailable at the deadline -- but the handler KEEPS RUNNING to completion on its thread: threads cannot be cancelled, so a deadline answers the client early, it does not stop the work. The late handler's response is discarded; it cannot reach the client (its copy has no socket) or upgrade the connection. Let late handlers finish before httpd-free, which releases the closures they run.

On the async server it runs next inline with no deadline.

msthe deadline in milliseconds
nextdownstream handler

A wrapped handler closure.

(compose-middleware slow-report-handler (mw-timeout 2000))

Since: tourist-on-stdlib-httpd H4

Internal definitions