Safsaf is a web framework for Guile Scheme, built on Guile Fibers using the Guile Knots web server.
This chapter explains how the pieces of Safsaf fit together. Each section covers one concept with a short code example. For the full list of parameters and options, see API.
A Safsaf application is a list of routes passed to run-safsaf.
Each route binds an HTTP method and URL pattern to a handler procedure.
The handler receives a Guile <request> and a body port, and
returns two values: a response and a body.
(use-modules (safsaf)
(safsaf router)
(safsaf response-helpers))
(define routes
(list
(route 'GET '() index-page
#:name 'index)
(route 'GET '("hello" name) hello-page)
(route '* '* (lambda (request body-port)
(not-found-response)))))
(define (index-page request body-port)
(html-response '(h1 "Welcome")))
(define (hello-page request body-port)
(let ((name (assoc-ref (current-route-params) 'name)))
(text-response (string-append "Hello, " name "!"))))
(run-safsaf routes #:port 8080)
The last route should be a catch-all ('* method, '*
pattern) so that every request is handled. run-safsaf sets up
a Fibers scheduler, starts the HTTP server, and blocks until Ctrl-C.
Next: Handler Wrappers, Previous: Getting Started, Up: Guidance [Contents][Index]
Route patterns are lists of segments. A string matches literally, a
symbol captures that segment into current-route-params, and a
two-element list (predicate name) captures only when
predicate returns true. A dotted tail captures the remaining
path.
;; Literal: /about
(route 'GET '("about") about-handler)
;; Capture: /users/:id
(route 'GET '("users" id) show-user)
;; Predicate: /posts/:id where id is numeric
(route 'GET '("posts" (,string->number id)) show-post)
;; Wildcard (rest): /files/* — captures remaining segments
(route 'GET '("files" . path) serve-file)
route-group nests routes under a shared prefix:
(route-group '("api" "v1")
(route 'GET '("users") api-list-users)
(route 'GET '("users" id) api-show-user))
This matches /api/v1/users and /api/v1/users/:id.
Give a route a #:name and use path-for to generate its
URL, so paths are never hard-coded. The first argument is always a
route group:
(define my-routes
(route-group '()
(route 'GET '("posts" id) show-post #:name 'show-post)))
(define all-routes
(list my-routes
(route '* '* (lambda (r b) (not-found-response)))))
;; In a handler or template:
(path-for my-routes 'show-post '((id . "42")))
;; => "/posts/42"
path-for also accepts #:query and #:fragment
keyword arguments.
A handler wrapper is a procedure that takes a handler and returns a
new handler. It can transform the request on the way in and the
response on the way out. Apply wrappers to a route tree with
wrap-routes.
(wrap-routes routes (make-exceptions-handler-wrapper #:dev? #t) logging-handler-wrapper)
When multiple wrappers are given, the first wraps outermost — it runs first on the request and last on the response. In the example above, exceptions catches errors from the logging wrapper and the inner handler.
Apply wrappers to part of the route tree by wrapping a group separately:
(define api-routes
(wrap-routes
(route-group '("api")
(route 'GET '("items") api-list-items))
cors-handler-wrapper))
(define all-routes
(wrap-routes
(list api-routes
(route 'GET '() index-page)
(route '* '* (lambda (r b) (not-found-response))))
logging-handler-wrapper))
Here CORS headers are added only to /api/* routes, while
logging applies to everything.
security-headers-handler-wrapper appends its headers to
the response rather than replacing existing ones. If a handler sets
X-Frame-Options itself, both values will appear in the response.
To avoid duplication, either omit the header from the wrapper (pass
#:frame-options #f) or do not set it in the handler.
make-max-body-size-handler-wrapper checks the
Content-Length header and rejects requests that exceed the
limit with a 413 response. However, it does not limit chunked
transfer-encoded requests that lack Content-Length. For
untrusted networks, use a reverse proxy (e.g. Nginx’s
client_max_body_size) to enforce size limits at the transport
level.
Next: Request Parsing, Previous: Handler Wrappers, Up: Guidance [Contents][Index]
Safsaf provides helpers that return (values response body)
directly:
;; HTML — streams an SXML tree
(html-response '(div (h1 "Hello") (p "world")))
;; JSON — takes a JSON string
(json-response (scm->json-string '(("ok" . #t))))
;; Plain text
(text-response "pong")
;; Redirect (default 303 See Other)
(redirect-response "/login")
(redirect-response "/new-item" #:code 302)
;; Error responses
(not-found-response)
(bad-request-response "Missing field")
(forbidden-response)
html-response, json-response, text-response, and
redirect-response accept #:code and #:headers for
overrides. The error helpers (not-found-response, etc.) accept
#:headers but have a fixed status code.
For content negotiation, use negotiate-content-type:
(define (show-item request body-port)
(let ((item (fetch-item (assoc-ref (current-route-params) 'id))))
(case (negotiate-content-type request
'(text/html application/json))
((application/json)
(json-response (scm->json-string (item->alist item))))
(else
(html-response `(div (h1 ,(item-title item))))))))
Next: Parameter Parsing, Previous: Responses, Up: Guidance [Contents][Index]
parse-form-body reads a URL-encoded POST body and returns an
alist of string pairs:
(define (handle-login request body-port)
(let* ((form (parse-form-body request body-port))
(username (assoc-ref form "username"))
(password (assoc-ref form "password")))
(if (valid-credentials? username password)
(redirect-response "/dashboard")
(text-response "Invalid login" #:code 401))))
parse-query-string extracts query parameters from the request
URL:
(let ((qs (parse-query-string request))) (assoc-ref qs "page")) ;; => "2" or #f
For file uploads, use parse-multipart-body:
(let* ((parts (parse-multipart-body request body-port))
(form (multipart-text-fields parts))
(file (parts-ref parts "avatar")))
;; form is an alist of text fields
;; file is a <part> record — read its body with (part-body file)
...)
Read cookies with request-cookie-ref or
request-cookies. Set them via response headers with
set-cookie-header and delete-cookie-header:
(request-cookie-ref request "theme") ;; => "dark" or #f
(text-response "ok"
#:headers (list (set-cookie-header "theme" "dark"
#:path "/"
#:http-only #t)))
Next: Sessions, Previous: Request Parsing, Up: Guidance [Contents][Index]
parse-params validates and transforms raw form or query data
according to a declarative spec. Each spec entry names a parameter,
a processor (a procedure that converts a string or returns an
<invalid-param>), and options like #:required or
#:default.
(let ((params (parse-params
`((page ,as-integer #:default 1)
(per-page ,as-integer #:default 20)
(q ,as-string))
(parse-query-string request))))
(assq-ref params 'page)) ;; => 1 (integer, not string)
Built-in processors: as-string, as-integer,
as-number, as-checkbox, as-one-of,
as-matching, as-predicate.
For POST forms, use parse-form-params instead — it
automatically checks the CSRF token (from
csrf-handler-wrapper) before parsing:
(let* ((form (parse-form-body request body-port))
(params (parse-form-params
`((title ,as-string #:required)
(body ,as-string #:required))
form)))
(if (any-invalid-params? params)
;; Re-render the form with errors
(render-form (field-errors params 'title)
(field-errors params 'body))
;; Proceed
(create-item! (assq-ref params 'title)
(assq-ref params 'body))))
any-invalid-params? returns #t if any value failed
validation. field-errors returns a list of error message
strings for a given field, suitable for rendering next to form inputs.
Next: Templating, Previous: Parameter Parsing, Up: Guidance [Contents][Index]
Sessions use HMAC-signed cookies via (webutils sessions).
Set up a session config and apply the wrapper:
(define session-mgr
(make-session-config "my-secret-key"
#:cookie-name "my-session"))
(define routes
(wrap-routes my-routes
(make-session-handler-wrapper session-mgr)))
Inside a handler, (current-session) returns the session data
(an alist) or #f if no valid session exists.
To set session data, include a session-set header in the
response. To delete, use session-delete:
;; Set session
(redirect-response "/"
#:headers (list (session-set session-mgr
'((user-id . 42)))))
;; Read session
(let ((user-id (and (current-session)
(assoc-ref (current-session) 'user-id))))
...)
;; Delete session
(redirect-response "/"
#:headers (list (session-delete session-mgr)))
Next: Static Files, Previous: Sessions, Up: Guidance [Contents][Index]
write-shtml-as-html/streaming works like htmlprag’s
write-shtml-as-html, but any procedure in the SHTML tree is
called as (proc port) and can write dynamic content directly.
streaming-html-response wraps this into a response: give it an
SHTML tree (with optional procedure slots) and it returns
(values response body) ready for a handler.
(define (base-layout title content-proc)
`(*TOP*
(*DECL* DOCTYPE html)
(html
(head (title ,title))
(body
(nav (a (@ (href "/")) "Home"))
(main ,content-proc)
(footer (p "Footer"))))))
The layout is plain SHTML with a procedure in the content-proc
position. Use streaming-html-response to send it:
(define (index-page request body-port)
(streaming-html-response
(base-layout "Home"
(lambda (port)
(write-shtml-as-html
`(div (h1 "Welcome")
(p "Content goes here."))
port)))))
You can also call write-shtml-as-html/streaming directly when
you need to write SHTML with procedure slots to an arbitrary port.
Next: Server-Sent Events, Previous: Templating, Up: Guidance [Contents][Index]
make-static-handler returns a handler that serves files from a
directory. Pair it with a wildcard route:
(route-group '("static")
(route 'GET '(. path)
(make-static-handler "./public"
#:cache-control '((max-age . 3600)))))
This serves /static/css/style.css from
./public/css/style.css. The handler supports
If-Modified-Since for 304 responses.
Previous: Static Files, Up: Guidance [Contents][Index]
(safsaf response-helpers sse) provides Server-Sent Events: a
long-lived HTTP response that streams labelled events to a browser’s
EventSource. Compared to WebSockets it is one-way
(server-to-client) and plain HTTP, which makes it easier to proxy and
requires no new protocol handshake.
Use sse-response to build a streaming response. It takes the
incoming request, a body thunk that is called with an emit
procedure, and a handful of keyword arguments:
(use-modules (safsaf response-helpers sse))
(define (events-handler request body-port)
(sse-response
request
(lambda (emit)
(let loop ((n 1))
(emit #:id (number->string n)
#:event "tick"
#:data (format #f "tick ~a" n))
(sleep 1)
(loop (1+ n))))))
emit accepts the same keyword arguments as
make-sse-event: #:data, #:event, #:id,
#:retry, #:comment. Calls serialise with the internal
keepalive fiber, so the wire output stays well-framed even if keepalive
fires mid-loop. The response sets
content-type: text/event-stream, cache-control: no-cache
and x-accel-buffering: no (for nginx) by default; append to or
override via #:headers.
A client connecting via new EventSource('/events') picks up the
stream and dispatches each tick event to any listener
registered with es.addEventListener('tick', ...).
Browsers and proxies drop idle connections, often after 30–60 seconds.
sse-response sends an SSE comment line every
#:keepalive-interval seconds (default 15) to prevent that. Pass
#f or a non-positive number to disable.
If the connection drops, EventSource reconnects automatically
and sets the Last-Event-ID header to the last id: it
saw. Read it with request-last-event-id:
(define (events-handler request body-port)
(sse-response
request
(lambda (emit)
(let ((since (request-last-event-id request)))
(replay-events-since since emit)
(stream-new-events emit)))))
Because browsers cannot set headers on the initial EventSource
connection, the helper falls back to the last_event_id
query-string parameter by default. Override the parameter name or
disable the fallback with #:query-param.
Use #:retry on sse-response to tell the browser how long
to wait before reconnecting:
(sse-response request body-thunk #:retry 2000)
A write failure — typically because the client went away — raises an
exception from emit. Check for it with
sse-client-disconnected?:
(define (events-handler request body-port)
(sse-response
request
(lambda (emit)
(with-exception-handler
(lambda (exn)
(if (sse-client-disconnected? exn)
'stop
(raise-exception exn)))
(lambda ()
(let loop ()
(emit #:data (next-event))
(loop)))
#:unwind? #t))))
A runnable ticker example lives in examples/sse-ticker/sse-ticker.scm.
Next: Version History, Previous: Guidance, Up: Overview [Contents][Index]
The following is the list of modules provided by this library.
Next: (safsaf handler-wrappers cors), Up: API [Contents][Index]
Return a 405 Method Not Allowed response with an Allow header listing ALLOWED-METHODS.
Start a Safsaf web server.
ROUTES is a list of routes and route-groups (as returned by component constructors). The last route must be a catch-all so that every request is handled.
HEAD requests are handled automatically: when no explicit HEAD route matches, the matching GET handler runs and its response body is discarded. Explicit HEAD routes always take precedence.
When METHOD-NOT-ALLOWED? is #t (the default), requests that match a route’s path but not its method receive a 405 response with an Allow header. METHOD-NOT-ALLOWED-HANDLER is a procedure (request allowed-methods) -> (values response body) that produces the 405 response; the default returns plain text.
When DISABLE-OUTPUT-PORT-BUFFERING? is #t (the default), buffering on the current output and error ports is set to ’none. Guile picks the buffering mode for these ports from the environment (line-buffered on a terminal, block-buffered otherwise), but port buffering is not thread- or fiber-safe, so concurrent writes from multiple fibers can cause mysterious errors.
When called outside a Fibers scheduler, sets up a scheduler, starts the HTTP server, and blocks until Ctrl-C. When called inside an existing scheduler (e.g. within run-fibers), just starts the HTTP server and returns immediately — the caller manages the lifecycle.
PARALLELISM is the number of OS threads the Fibers scheduler runs fibers across, and is forwarded to run-fibers’ #:parallelism. Fibers itself defaults to all available cores, but Safsaf defaults to 1 because in practice a single-threaded scheduler currently scales better for the core server — multi-threaded scheduling adds contention that outweighs the parallelism gains for typical request workloads. Set this higher if profiling shows the single thread is CPU-bound. Only applies when run-safsaf manages the scheduler; when called inside an existing run-fibers, this argument is ignored.
Next: (safsaf handler-wrappers csrf), Previous: (safsaf), Up: API [Contents][Index]
Handler wrapper that adds CORS (Cross-Origin Resource Sharing) headers to responses.
Browsers enforce the Same-Origin Policy: scripts on one origin (scheme + host + port) cannot read responses from a different origin. CORS relaxes this by letting the server declare which origins, methods, and headers are permitted.
For “simple” requests the browser sends the request and checks the response headers. For non-simple requests (e.g. PUT/DELETE, custom headers, or JSON Content-Type) the browser sends a preflight OPTIONS request first. This wrapper handles both cases.
ORIGINS is a list of allowed origin strings, or ’("*") for any. METHODS is a list of allowed method symbols. HEADERS is a list of allowed request header name strings. MAX-AGE is the preflight cache duration in seconds. ALLOW-CREDENTIALS? controls whether credentials (cookies, auth) are allowed cross-origin. Note: cannot be #t when origins is ’("*"). EXPOSE-HEADERS is a list of response header name strings the browser may read from JavaScript.
Next: (safsaf handler-wrappers exceptions), Previous: (safsaf handler-wrappers cors), Up: API [Contents][Index]
Default value:
#f
CSRF token handler wrapper.
Ensures a CSRF token cookie is present on every response (generates one if the request has none). The token is bound to current-csrf-token so handlers and templates can read it via (current-csrf-token).
Cookie attributes: Path is always /. HTTP-ONLY defaults to #t — the double-submit pattern reads the token from (current-csrf-token), not from JavaScript, so there’s no need to expose the cookie to scripts. SAME-SITE accepts ’strict (default), ’lax, ’none, or #f to omit the attribute. SECURE defaults to #f; set #t in production where the site is served over HTTPS.
Token validation is NOT done here — it belongs in the form processing layer. Use parse-form-params from (safsaf params), which automatically checks the submitted token against the cookie token.
Return an SXML hidden input element for the CSRF token. Use in forms:
(csrf-token-field) ⇒ (input (@ (type "hidden")
...)).
Next: (safsaf handler-wrappers locale), Previous: (safsaf handler-wrappers csrf), Up: API [Contents][Index]
Return a render-error procedure that content-negotiates between RENDER-HTML and RENDER-JSON based on the request’s Accept header.
Default HTML error renderer. In dev mode, shows a rich backtrace page. In production, returns a minimal HTML page.
Default JSON error renderer. In dev mode, includes the backtrace. In production, returns only the error message.
Handler wrapper that catches exceptions from HANDLER and returns an error response.
The response format is content-negotiated from the request’s Accept header, choosing between HTML and JSON.
When LOGGER is provided, exceptions are logged through it. Otherwise, the backtrace is written to the current error port. In dev mode (DEV? is #t), the response includes the backtrace and exception details. In production mode, a generic error is returned.
Rendering can be customised at three levels:
#:render-error — full override. A procedure (request code message backtrace-string dev?) -> (values response body) that bypasses content negotiation entirely.
#:render-html — custom HTML rendering. A procedure with the same signature, called when content negotiation selects HTML.
#:render-json — custom JSON rendering. A procedure with the same signature, called when content negotiation selects JSON.
The default RENDER-ERROR content-negotiates between RENDER-HTML and RENDER-JSON. Providing #:render-html or #:render-json replaces just that format; providing #:render-error replaces the entire rendering.
Return a handler wrapper that catches exceptions and returns an error response. See exceptions-handler-wrapper for details.
Next: (safsaf handler-wrappers logging), Previous: (safsaf handler-wrappers exceptions), Up: API [Contents][Index]
Return a handler wrapper that resolves the request locale and binds it
to current-locale for the duration of the handler.
SUPPORTED is a list of locale strings the application recognises. DEFAULT is used when no strategy yields a supported locale. DETECT is an ordered list of strategy symbols; the first strategy to return a supported locale wins. Strategies:
routeReads (current-route-params) for ROUTE-PARAM. The wrapper must
be applied inside the router (i.e. via wrap-routes) for this to
work, since current-route-params is bound by the dispatcher.
cookieReads COOKIE-NAME from the request cookies.
accept-languageParses the Accept-Language request header and returns the best
match against SUPPORTED.
Unsupported values from any strategy are treated as a miss, so the next strategy in DETECT is tried.
Next: (safsaf handler-wrappers max-body-size), Previous: (safsaf handler-wrappers locale), Up: API [Contents][Index]
Handler wrapper that logs each request and response.
Logs at LEVEL (default ’INFO) with method, path, status code, and duration in milliseconds. If LOGGER is given, logs to that logger; otherwise uses the default logger set via set-default-logger!.
Next: (safsaf handler-wrappers security-headers), Previous: (safsaf handler-wrappers logging), Up: API [Contents][Index]
Return a handler wrapper that rejects requests whose Content-Length exceeds MAX-BYTES with a 413 Payload Too Large response.
HANDLER-413 is a handler (request body-port) -> (values response body) called when the limit is exceeded; the default returns plain text.
Note: this checks the Content-Length header only. Chunked transfers without Content-Length are not limited by this wrapper.
Next: (safsaf handler-wrappers sessions), Previous: (safsaf handler-wrappers max-body-size), Up: API [Contents][Index]
Handler wrapper that adds security headers to every response.
All headers are optional and configurable. Pass #f to disable a header. Defaults: X-Content-Type-Options: nosniff X-Frame-Options: DENY Referrer-Policy: strict-origin-when-cross-origin
Not set by default (enable explicitly): Strict-Transport-Security (e.g. "max-age=63072000; includeSubDomains") Cross-Origin-Opener-Policy (e.g. "same-origin") Permissions-Policy (e.g. "camera=(), microphone=()") Content-Security-Policy (e.g. "default-src ’self’; script-src ’self’") Content-Security-Policy-Report-Only — same syntax, for testing policies without enforcing them
Next: (safsaf handler-wrappers trailing-slash), Previous: (safsaf handler-wrappers security-headers), Up: API [Contents][Index]
Undocumented macro.
Default value:
#f
Create a session config for use with session-handler-wrapper.
SECRET-KEY is the HMAC signing key (a string). EXPIRE-DELTA is (days hours minutes), default 30 days. ALGORITHM is the HMAC algorithm, default sha512.
Cookie attributes: SECURE — default #f. Set #t in production where the site is served over HTTPS; this prevents the cookie from being sent over cleartext. HTTP-ONLY — default #t. Hides the cookie from JavaScript so an XSS on the origin can’t exfiltrate the session via document.cookie. SAME-SITE — ’lax (default), ’strict, ’none, or #f to omit. Lax is the right default: it preserves cross-site GET flows like "click email link, land logged in" while blocking cross-site form posts. PATH — default "/". Both the session cookie and the cookie that deletes it must agree on Path for the browser to match them, so this attribute applies to both session-set and session-delete.
Return a handler wrapper that binds session data from CONFIG. See session-handler-wrapper for details.
Return a Set-Cookie header that expires the session cookie. Include in a response headers list: (redirect-response "/" #:headers (list (session-delete cfg)))
Session handler wrapper using signed cookies via (webutils sessions).
Reads the session cookie from the request, verifies the HMAC signature, and binds current-session for the duration of the handler. If no valid session cookie is present, current-session is #f.
Handlers read session data via: (current-session) → session data or #f
To set or delete the session, handlers include the appropriate header in their response using session-set and session-delete:
(redirect-response "/" #:headers (list (session-set config data))) (redirect-response "/" #:headers (list (session-delete config)))
Return a Set-Cookie header that stores signed DATA in the session cookie. DATA can be any Scheme value that can be written and read back. Include in a response headers list: (redirect-response "/" #:headers (list (session-set cfg ’((user-id . 42)))))
Next: (safsaf i18n), Previous: (safsaf handler-wrappers sessions), Up: API [Contents][Index]
Return a handler wrapper that normalizes trailing slashes.
MODE is either ’strip (default) or ’append: ’strip — redirect /foo/ to /foo ’append — redirect /foo to /foo/
The root path / is always left alone.
CODE is the HTTP status code for the redirect (default 301).
Use with wrap-routes: (wrap-routes routes (make-trailing-slash-handler-wrapper #:mode ’append))
Handler wrapper that normalizes trailing slashes in request paths.
MODE is either ’strip (default) or ’append: ’strip — redirect /foo/ to /foo ’append — redirect /foo to /foo/
The root path / is always left alone.
CODE is the HTTP status code for the redirect (default 301).
Next: (safsaf params), Previous: (safsaf handler-wrappers trailing-slash), Up: API [Contents][Index]
Default value:
#f
Given an Accept-Language HEADER and a list of SUPPORTED locale strings,
return the best match (as listed in SUPPORTED) or #f if none
match. Matching is case-insensitive and falls back from a regional
variant to its base language, e.g. a request for en-US will
match a supported "en".
A * in the header picks the first supported locale.
HEADER may be a raw string or Guile’s pre-parsed form (an alist of
(permille . symbol) pairs), so this works with the raw header
value or with (assoc-ref (request-headers request)
'accept-language).
Drop all loaded catalogs. Primarily useful in tests.
Install VALUE (a string or vector of strings) as the translation of MSGID for LOCALE. VALUE is a string for singular entries and a vector for plural entries. Tests can use this to avoid going through the PO parser.
Read PO entries from PORT and install them as translations for LOCALE. Existing entries for LOCALE are preserved (new entries override).
Scan DIR for files matching EXTENSION (default ".po") and install each as the catalog for the locale taken from the filename stem.
locale/fr.po ⇒ catalog for locale "fr"
locale/en-GB.po ⇒ catalog for locale "en-GB"
Normalise an Accept-Language HEADER to a list of (locale . quality) pairs sorted by descending quality, with locale lowercased. HEADER can be:
#f or an empty string (returns '())
parse-accept-language)
(permille . language-symbol) pairs as returned by
Guile’s (web http) header parser, where the permille is an
integer from 0 to 1000.
Parse an Accept-Language HEADER into a list of (locale . quality) pairs sorted by descending quality. LOCALE is lowercased. Entries with q=0 are omitted. Returns ’() for #f or an empty header.
Register a plural rule RULE for LOCALE. RULE is a procedure taking an integer N and returning the plural form index (zero-based).
A rule for "en" would be (lambda (n) (if (= n 1) 0 1)).
Translate MSGID under (current-locale), falling back to MSGID itself
when no translation is installed. ARGS, if any, are interpolated via
(ice-9 format) — use ~a, ~s, etc.
(t "Hello, ~a!" name)
Plural-aware translation. N selects the plural form via the current locale’s plural rule. MSGID-PLURAL is the untranslated plural form used when no translation is installed and N is not 1.
N is passed as the first format argument; ARGS are passed after. Use
~a to insert the count:
(tn "~a item" "~a items" count)
Next: (safsaf response-helpers), Previous: (safsaf i18n), Up: API [Contents][Index]
Undocumented macro.
Undocumented macro.
Undocumented macro.
Undocumented macro.
Return #t if any values in PARSED-PARAMS are invalid.
Undocumented procedure.
Undocumented procedure.
Return a processor that accepts values matching REGEX.
Undocumented procedure.
Return a processor that accepts only values in CHOICES (a list of strings).
Return a processor that accepts values for which PRED returns true.
Undocumented procedure.
Return a list of error message strings for NAME, or ’(). Convenient for rendering form fields with per-field errors.
Check PARSED-PARAMS for mutually exclusive parameter groups. GROUPS is a list of lists of symbols, e.g. ’((limit_results all_results)). If parameters from the same group co-occur, the later ones are replaced with <invalid-param> records.
Return the <invalid-param> record for NAME, or #f if valid or absent.
Serialize PARSED-PARAMS back to a URI query string. Skips invalid params. Handles multi-value (list) entries. Useful for building pagination links that preserve current filters.
Like parse-params but prepends a CSRF token check. Uses current-csrf-token from (safsaf handler-wrappers csrf).
Parse and transform parameters from RAW-PARAMS according to PARAM-SPECS.
RAW-PARAMS is an alist of (string . string) pairs, as returned by parse-query-string or parse-form-body.
PARAM-SPECS is a list of specifications. Each spec is a list whose first element is the parameter name (a symbol), second is a processor procedure (string -> value | <invalid-param>), and the rest are keyword options:
(name processor) ; optional (name processor #:required) ; must be present (name processor #:default value) ; fallback (name processor #:multi-value) ; collect all occurrences (name processor #:multi-value #:default value) ; multi-value with fallback (name processor #:no-default-when (fields) #:default value) ; conditional default
Returns an alist of (symbol . value) pairs. Values that fail validation appear as <invalid-param> records inline. Missing optional params without defaults are omitted.
This record type has the following fields:
valuemessageNext: (safsaf response-helpers sse), Previous: (safsaf params), Up: API [Contents][Index]
Build a new response based on RESPONSE, preserving its version, status code, and reason phrase. HEADERS defaults to the existing headers; override it to modify them.
Use this in handler wrappers that need to adjust headers on an inner handler’s response without losing any response fields.
Return a Set-Cookie header pair that expires cookie NAME. Wraps (webutils cookie) delete-cookie.
Return an HTML response by streaming SHTML to the client. SHTML is an SXML/SHTML tree as accepted by write-shtml-as-html. CHARSET defaults to "utf-8".
Return a JSON response. STR is the JSON string to send.
Write LST as a JSON array to PORT, applying PROC to each element to produce a JSON-serializable value. Each element is written individually via scm->json so the entire array need not be held in memory.
Return a handler that serves static files from ROOT-DIR.
The handler expects route params to contain a wildcard capture (the file path segments). Use with a wildcard route:
(route ’GET ’(. path) (make-static-handler "/path/to/public"))
Supports If-Modified-Since for 304 responses. CACHE-CONTROL, if given, is a Cache-Control value in Guile’s header format — an alist, e.g. ’((max-age . 3600)) or ’((no-cache)).
Works with /gnu/store paths: files with a very low mtime (as produced by the store’s timestamp normalization) use the process start time as Last-Modified instead, so that conditional requests behave sensibly.
Return the most appropriate MIME type symbol for REQUEST from SUPPORTED.
Checks the URL path extension first (.json, .html, .txt) — if present and the implied type is in SUPPORTED, it wins. Otherwise, walks the Accept header and returns the first type that appears in SUPPORTED. Falls back to the first element of SUPPORTED if nothing matches.
EXTENSIONS is an alist mapping file extension strings to MIME type symbols, used for path-based negotiation. Defaults to %negotiation-extensions.
Return a 404 Not Found response.
Return a redirect response to PATH (a string).
Write ALIST as a JSON object to PORT, streaming each value as it is produced. If a value in the alist is a procedure, it is called with PORT so it can write its own JSON representation directly. Otherwise the value is serialized via scm->json.
Return a Set-Cookie header pair suitable for inclusion in a response headers alist. Wraps (webutils cookie) set-cookie.
Example: (values (build-response #:headers (list (set-cookie-header "session" token #:path "/" #:http-only #t #:secure #t))) "ok")
Return a JSON response whose body is written incrementally by THUNK. THUNK is a procedure of one argument (the output port). Use scm-alist->streaming-json and list->streaming-json-array inside THUNK to write JSON without materializing the entire response in memory.
Return a plain text response. STR is the text string to send. CHARSET defaults to "utf-8".
Next: (safsaf router), Previous: (safsaf response-helpers), Up: API [Contents][Index]
Undocumented macro.
Undocumented macro.
Undocumented macro.
Undocumented macro.
Undocumented macro.
Undocumented macro.
Construct a Server-Sent Event value.
Keyword arguments are all optional:
#:dataA string that becomes the event payload. Multi-line strings are split
and each line is emitted as a separate data: line per the SSE
specification, so newlines inside DATA are fine.
#:eventAn event-name string. Consumers can filter by name via
EventSource.addEventListener(name, ...). Must not contain
newlines.
#:idA Last-Event-ID string. The browser echoes this back in the
Last-Event-ID header when it reconnects, enabling replay. Must
not contain newlines.
#:retryA non-negative integer (milliseconds) telling the browser how long to wait before reconnecting after a drop.
#:commentA string sent as an SSE comment (each line prefixed with ‘:’). Useful as a keepalive to stop proxies closing idle streams. Multi-line strings are split one comment line per input line.
At least one of DATA, COMMENT, RETRY, EVENT, or ID must be provided for the event to cause anything observable on the client.
Return the client’s Last-Event-ID as a string, or #f if none.
Checks the standard Last-Event-ID header first. If that’s absent
and QUERY-PARAM is a string, falls back to that query-string parameter —
browsers’ EventSource can’t set headers on the initial
connection, so a query-string carrier is a common way to request replay
on first connect. Pass #:query-param #f to disable the fallback.
The value is returned as a string; the SSE spec treats IDs opaquely. Applications that number events with integers should convert the result themselves.
Return #t if EXN indicates the SSE client has disconnected. Raised from inside an sse-response emit call when a write to the client fails.
Return a streaming Server-Sent Events response.
REQUEST is the incoming <request>; it’s needed so the helper can
flush the underlying socket port after every event — the chunked output
port wrapped around the socket by the web server buffers writes, so
flushing only the body port is not sufficient.
BODY-THUNK is called with one argument, emit, which is a procedure
accepting the same keyword arguments as make-sse-event
(#:data, #:event, #:id, #:retry,
#:comment). Calls to emit are serialised with any
keepalive comments, written to the client, and flushed before returning.
If the client has disconnected, emit raises an exception
satisfying sse-client-disconnected?.
Response headers default to text/event-stream,
cache-control: no-cache and x-accel-buffering: no. HEADERS
is appended to the defaults.
KEEPALIVE-INTERVAL is the number of seconds between SSE comment lines
sent to keep the connection alive (important because HTTP proxies
commonly drop idle connections after 30–60 seconds). Pass #f or
a non-positive number to disable.
RETRY, if set, emits an initial retry: directive telling the
browser how long to wait before reconnecting.
Write EVENT to PORT in SSE wire format and finish with a blank line so the browser dispatches it. Does not flush PORT.
Next: (safsaf templating), Previous: (safsaf response-helpers sse), Up: API [Contents][Index]
Undocumented macro.
Return the list of child routes and groups of ROUTE-GROUP.
Return the name of ROUTE-GROUP, or #f if unnamed.
Return the prefix pattern of ROUTE-GROUP.
Return #t if OBJ is a <route-group>.
Return the handler procedure of ROUTE.
Return the HTTP method of ROUTE.
Return the name of ROUTE, or #f if unnamed.
Return the URL pattern of ROUTE.
Return #t if OBJ is a <route>.
Default value:
#f
Default value:
()
Compile a route tree (route, route-group, or list) into two values: 1. An ordered list of <compiled-route> records ready for matching. 2. A <reverse-routes> record for use with path-for.
The last route must be a catch-all (’* pattern with a rest parameter) so that every request is handled.
Scan COMPILED-ROUTES for routes whose path matches PATH-SEGMENTS, collecting their HTTP methods. The last route (the catch-all) is excluded. Returns a deduplicated list of method symbols, or ’() if no route’s path matches.
Create an empty route group with PREFIX. Children can be added later with route-group-add-children!.
Find the first matching route for METHOD and PATH-SEGMENTS. Returns (values handler bindings) on match, or (values #f #f) on no match.
Generate a URL path for a named route within GROUP.
GROUP is a route-group value. NAME is either a symbol naming a route within GROUP, or a list of symbols for nested lookup where the last element is the route name and preceding elements are child group names.
(path-for routes ’users) (path-for routes ’user ’((id . "42"))) (path-for routes ’(api items) ’((id . "7")))
PARAMS is an alist mapping capture symbols to string values, or to a list of strings for rest parameters.
Optional keyword arguments: #:query — alist of query parameters ((key . value) ...) #:fragment — fragment string (without the leading #) #:relative? — if #t, omit the leading /
Create a route. METHOD is a symbol, list of symbols, or ’* for any. PATTERN is a list of segments: strings (literal), symbols (capture), two-element lists (predicate capture: (proc name)), with optional dotted tail (wildcard capture). HANDLER is a procedure (request body-port) -> (values response body). NAME is an optional symbol used for reverse routing with path-for.
Create a route group. PREFIX is a pattern list (same syntax as route patterns). CHILDREN is an ordered list of routes and route-groups. NAME is an optional symbol for nested path-for lookups.
Append NEW-CHILDREN to GROUP’s child list.
Apply WRAPPERS to every handler in ROUTES, which may be a route, route-group, or list of either. Returns a new structure with wrapped handlers. When multiple wrappers are given, the first wrapper in the list wraps outermost (runs first on the request, last on the response).
Next: (safsaf utils), Previous: (safsaf router), Up: API [Contents][Index]
Return an HTML response that streams SHTML to the client.
SHTML is rendered via write-shtml-as-html/streaming, so any
procedures in the tree are evaluated at output time.
Write SHTML NODE to PORT.
Procedures in the tree are dispatched by arity:
(proc port) and may write HTML
directly to PORT.
In addition to standard SHTML, two extensions are recognised:
(raw HTML) — write HTML to PORT unescaped. Use only with trusted
input.
(doctype NAME) — write <!DOCTYPE NAME>. NAME may be a
symbol or a string. The htmlprag-style (*DECL* DOCTYPE html)
form is also accepted.
(*ENTITY* NAME) — write &NAME;. NAME may be a symbol, a
string, or an integer (for numeric entities).
Attribute values: a string is escaped; #t produces a bare boolean
attribute; #f omits the attribute; any other value is
displayed and escaped.
Previous: (safsaf templating), Up: API [Contents][Index]
Extract text fields from multipart PARTS as an alist of (name . value). File upload parts (those with a filename parameter) are excluded.
Read and parse a URL-encoded form body from REQUEST. Returns an alist of string key-value pairs.
Read and parse a multipart/form-data body from REQUEST. Returns a list of <part> records from (webutils multipart). Use parts-ref, parts-ref-string, part-body, etc. to access parts.
Parse the query string from REQUEST. Returns an alist of string key-value pairs, or ’() if no query string.
Return the value of cookie NAME from REQUEST, or DEFAULT if not found.
Return the cookies from REQUEST as an alist of (name . value) pairs. Returns ’() if no Cookie header is present. Importing (webutils cookie) registers the Cookie header parser with (web http).
Convert a SameSite value to a Set-Cookie extensions alist suitable for the #:extensions kwarg of (webutils cookie) set-cookie.
SAME-SITE accepts ’strict, ’lax, ’none, or #f (omit the attribute).
Next: Copying Information, Previous: API, Up: Overview [Contents][Index]
Next: Concept Index, Previous: Version History, Up: Overview [Contents][Index]
Copyright © 2026 Christopher Baines <mail@cbaines.net>
This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
Next: Procedure Index, Previous: Concept Index, Up: Overview [Contents][Index]
| Jump to: | < |
|---|
| Index Entry | Section | ||
|---|---|---|---|
| | |||
| < | |||
<invalid-param>: | safsaf_params | ||
| | |||
| Jump to: | < |
|---|
Next: Variable Index, Previous: Data Type Index, Up: Overview [Contents][Index]
| Jump to: | A B C D E F G H I J L M N P R S T W |
|---|
| Jump to: | A B C D E F G H I J L M N P R S T W |
|---|
Previous: Procedure Index, Up: Overview [Contents][Index]
| Jump to: | C |
|---|
| Jump to: | C |
|---|