safsaf/index.html

781 lines
31 KiB
HTML
Raw Normal View History

2026-04-13 11:24:04 +01:00
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<!-- Created by GNU Texinfo 6.8, https://www.gnu.org/software/texinfo/ -->
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Safsaf</title>
<meta name="description" content="Safsaf">
<meta name="keywords" content="Safsaf">
<meta name="resource-type" content="document">
<meta name="distribution" content="global">
<meta name="Generator" content="makeinfo">
<meta name="viewport" content="width=device-width,initial-scale=1">
<link href="#Top" rel="start" title="Top">
<link href="#Concept-Index" rel="index" title="Concept Index">
<link href="#SEC_Contents" rel="contents" title="Table of Contents">
<link href="#Guidance" rel="next" title="Guidance">
<style type="text/css">
<!--
a.copiable-anchor {visibility: hidden; text-decoration: none; line-height: 0em}
a.summary-letter {text-decoration: none}
blockquote.indentedblock {margin-right: 0em}
div.display {margin-left: 3.2em}
div.example {margin-left: 3.2em}
kbd {font-style: oblique}
pre.display {font-family: inherit}
pre.format {font-family: inherit}
pre.menu-comment {font-family: serif}
pre.menu-preformatted {font-family: serif}
span.nolinebreak {white-space: nowrap}
span.roman {font-family: initial; font-weight: normal}
span.sansserif {font-family: sans-serif; font-weight: normal}
span:hover a.copiable-anchor {visibility: visible}
ul.no-bullet {list-style: none}
-->
</style>
<link rel="stylesheet" type="text/css" href="https://luis-felipe.gitlab.io/texinfo-css/static/css/texinfo-7.css">
</head>
<body lang="en">
<h1 class="settitle" align="center">Safsaf</h1>
<div style="text-align: center;">
<img src="logo.svg" alt="Safsaf" width="200" height="200">
</div>
<div class="top" id="Top">
<div class="header">
<p>
Next: <a href="#Guidance" accesskey="n" rel="next">Guidance</a> &nbsp; [<a href="#SEC_Contents" title="Table of contents" rel="contents">Contents</a>][<a href="#Concept-Index" title="Index" rel="index">Index</a>]</p>
</div>
<span id="Overview"></span><h1 class="top">Overview</h1>
<p>Safsaf is a web framework for Guile Scheme, built on
<a href="https://codeberg.org/guile/fibers">Guile Fibers</a> using the
<a href="https://cbaines.codeberg.page/guile-knots/">Guile Knots</a> web
server.
</p>
<div class="Contents_element" id="SEC_Contents">
<h2 class="contents-heading">Table of Contents</h2>
<div class="contents">
<ul class="no-bullet">
<li><a id="toc-Guidance-1" href="#Guidance">1 Guidance</a>
<ul class="no-bullet">
<li><a id="toc-Getting-Started-1" href="#Getting-Started">1.1 Getting Started</a></li>
<li><a id="toc-Routing-1" href="#Routing">1.2 Routing</a></li>
<li><a id="toc-Handler-Wrappers-1" href="#Handler-Wrappers">1.3 Handler Wrappers</a></li>
<li><a id="toc-Responses-1" href="#Responses">1.4 Responses</a></li>
<li><a id="toc-Request-Parsing-1" href="#Request-Parsing">1.5 Request Parsing</a></li>
<li><a id="toc-Parameter-Parsing-1" href="#Parameter-Parsing">1.6 Parameter Parsing</a></li>
<li><a id="toc-Sessions-1" href="#Sessions">1.7 Sessions</a></li>
<li><a id="toc-Templating-1" href="#Templating">1.8 Templating</a></li>
<li><a id="toc-Static-Files-1" href="#Static-Files">1.9 Static Files</a></li>
</ul></li>
<li><a id="toc-API-1" href="#API">2 API</a>
<ul class="no-bullet">
<li><a id="toc-_0028safsaf_0029" href="#safsaf">2.1 (safsaf)</a>
<ul class="no-bullet">
<li><a id="toc-Procedures" href="#Procedures">2.1.1 Procedures</a></li>
</ul></li>
</ul></li>
<li><a id="toc-Version-History-1" href="#Version-History">Appendix A Version History</a></li>
<li><a id="toc-Copying-Information-1" href="#Copying-Information">Appendix B Copying Information</a></li>
<li><a id="toc-Concept-Index-1" href="#Concept-Index" rel="index">Concept Index</a></li>
<li><a id="toc-Data-Type-Index-1" href="#Data-Type-Index" rel="index">Data Type Index</a></li>
<li><a id="toc-Procedure-Index-1" href="#Procedure-Index" rel="index">Procedure Index</a></li>
<li><a id="toc-Variable-Index-1" href="#Variable-Index" rel="index">Variable Index</a></li>
</ul>
</div>
</div>
<hr>
<div class="chapter" id="Guidance">
<div class="header">
<p>
Next: <a href="#API" accesskey="n" rel="next">API</a>, Previous: <a href="#Top" accesskey="p" rel="prev">Overview</a>, Up: <a href="#Top" accesskey="u" rel="up">Overview</a> &nbsp; [<a href="#SEC_Contents" title="Table of contents" rel="contents">Contents</a>][<a href="#Concept-Index" title="Index" rel="index">Index</a>]</p>
</div>
<span id="Guidance-1"></span><h2 class="chapter">1 Guidance</h2>
<p>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 <a href="#API">API</a>.
</p>
<ul class="section-toc">
<li><a href="#Getting-Started" accesskey="1">Getting Started</a></li>
<li><a href="#Routing" accesskey="2">Routing</a></li>
<li><a href="#Handler-Wrappers" accesskey="3">Handler Wrappers</a></li>
<li><a href="#Responses" accesskey="4">Responses</a></li>
<li><a href="#Request-Parsing" accesskey="5">Request Parsing</a></li>
<li><a href="#Parameter-Parsing" accesskey="6">Parameter Parsing</a></li>
<li><a href="#Sessions" accesskey="7">Sessions</a></li>
<li><a href="#Templating" accesskey="8">Templating</a></li>
<li><a href="#Static-Files" accesskey="9">Static Files</a></li>
</ul>
<hr>
<div class="section" id="Getting-Started">
<div class="header">
<p>
Next: <a href="#Routing" accesskey="n" rel="next">Routing</a>, Up: <a href="#Guidance" accesskey="u" rel="up">Guidance</a> &nbsp; [<a href="#SEC_Contents" title="Table of contents" rel="contents">Contents</a>][<a href="#Concept-Index" title="Index" rel="index">Index</a>]</p>
</div>
<span id="Getting-Started-1"></span><h3 class="section">1.1 Getting Started</h3>
<p>A Safsaf application is a list of routes passed to <code>run-safsaf</code>.
Each route binds an HTTP method and URL pattern to a handler procedure.
The handler receives a Guile <code>&lt;request&gt;</code> and a body port, and
returns two values: a response and a body.
</p>
<div class="example lisp">
<pre class="lisp">(use-modules (safsaf)
(safsaf router)
(safsaf response-helpers))
(define routes
(list
(route 'GET '() index-page
#:name 'index)
(route 'GET '(&quot;hello&quot; name) hello-page)
(route '* '* (lambda (request body-port)
(not-found-response)))))
(define (index-page request body-port)
(html-response '(h1 &quot;Welcome&quot;)))
(define (hello-page request body-port)
(let ((name (assoc-ref (current-route-params) 'name)))
(text-response (string-append &quot;Hello, &quot; name &quot;!&quot;))))
(run-safsaf routes #:port 8080)
</pre></div>
<p>The last route should be a catch-all (<code>'*</code> method, <code>'*</code>
pattern) so that every request is handled. <code>run-safsaf</code> sets up
a Fibers scheduler, starts the HTTP server, and blocks until Ctrl-C.
</p>
<hr>
</div>
<div class="section" id="Routing">
<div class="header">
<p>
Next: <a href="#Handler-Wrappers" accesskey="n" rel="next">Handler Wrappers</a>, Previous: <a href="#Getting-Started" accesskey="p" rel="prev">Getting Started</a>, Up: <a href="#Guidance" accesskey="u" rel="up">Guidance</a> &nbsp; [<a href="#SEC_Contents" title="Table of contents" rel="contents">Contents</a>][<a href="#Concept-Index" title="Index" rel="index">Index</a>]</p>
</div>
<span id="Routing-1"></span><h3 class="section">1.2 Routing</h3>
<span id="Patterns"></span><h4 class="subheading">Patterns</h4>
<p>Route patterns are lists of segments. A string matches literally, a
symbol captures that segment into <code>current-route-params</code>, and a
two-element list <code>(predicate name)</code> captures only when
<var>predicate</var> returns true. A dotted tail captures the remaining
path.
</p>
<div class="example lisp">
<pre class="lisp">;; Literal: /about
(route 'GET '(&quot;about&quot;) about-handler)
;; Capture: /users/:id
(route 'GET '(&quot;users&quot; id) show-user)
;; Predicate: /posts/:id where id is numeric
(route 'GET '(&quot;posts&quot; (,string-&gt;number id)) show-post)
;; Wildcard (rest): /files/* — captures remaining segments
(route 'GET '(&quot;files&quot; . path) serve-file)
</pre></div>
<span id="Route-groups"></span><h4 class="subheading">Route groups</h4>
<p><code>route-group</code> nests routes under a shared prefix:
</p>
<div class="example lisp">
<pre class="lisp">(route-group '(&quot;api&quot; &quot;v1&quot;)
(route 'GET '(&quot;users&quot;) api-list-users)
(route 'GET '(&quot;users&quot; id) api-show-user))
</pre></div>
<p>This matches <code>/api/v1/users</code> and <code>/api/v1/users/:id</code>.
</p>
<span id="Named-routes-and-path_002dfor"></span><h4 class="subheading">Named routes and path-for</h4>
<p>Give a route a <code>#:name</code> and use <code>path-for</code> to generate its
URL, so paths are never hard-coded. The first argument is always a
route group:
</p>
<div class="example lisp">
<pre class="lisp">(define my-routes
(route-group '()
(route 'GET '(&quot;posts&quot; 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 . &quot;42&quot;)))
;; =&gt; &quot;/posts/42&quot;
</pre></div>
<p><code>path-for</code> also accepts <code>#:query</code> and <code>#:fragment</code>
keyword arguments.
</p>
<hr>
</div>
<div class="section" id="Handler-Wrappers">
<div class="header">
<p>
Next: <a href="#Responses" accesskey="n" rel="next">Responses</a>, Previous: <a href="#Routing" accesskey="p" rel="prev">Routing</a>, Up: <a href="#Guidance" accesskey="u" rel="up">Guidance</a> &nbsp; [<a href="#SEC_Contents" title="Table of contents" rel="contents">Contents</a>][<a href="#Concept-Index" title="Index" rel="index">Index</a>]</p>
</div>
<span id="Handler-Wrappers-1"></span><h3 class="section">1.3 Handler Wrappers</h3>
<p>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
<code>wrap-routes</code>.
</p>
<div class="example lisp">
<pre class="lisp">(wrap-routes routes
(make-exceptions-handler-wrapper #:dev? #t)
logging-handler-wrapper)
</pre></div>
<p>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.
</p>
<span id="Per_002dgroup-wrappers"></span><h4 class="subheading">Per-group wrappers</h4>
<p>Apply wrappers to part of the route tree by wrapping a group
separately:
</p>
<div class="example lisp">
<pre class="lisp">(define api-routes
(wrap-routes
(route-group '(&quot;api&quot;)
(route 'GET '(&quot;items&quot;) 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))
</pre></div>
<p>Here CORS headers are added only to <code>/api/*</code> routes, while
logging applies to everything.
</p>
<span id="Security-headers"></span><h4 class="subheading">Security headers</h4>
<p><code>security-headers-handler-wrapper</code> <em>appends</em> its headers to
the response rather than replacing existing ones. If a handler sets
<code>X-Frame-Options</code> itself, both values will appear in the response.
To avoid duplication, either omit the header from the wrapper (pass
<code>#:frame-options #f</code>) or do not set it in the handler.
</p>
<span id="Max-body-size"></span><h4 class="subheading">Max body size</h4>
<p><code>make-max-body-size-handler-wrapper</code> checks the
<code>Content-Length</code> header and rejects requests that exceed the
limit with a 413 response. However, it does <em>not</em> limit chunked
transfer-encoded requests that lack <code>Content-Length</code>. For
untrusted networks, use a reverse proxy (e.g. Nginx&rsquo;s
<code>client_max_body_size</code>) to enforce size limits at the transport
level.
</p>
<hr>
</div>
<div class="section" id="Responses">
<div class="header">
<p>
Next: <a href="#Request-Parsing" accesskey="n" rel="next">Request Parsing</a>, Previous: <a href="#Handler-Wrappers" accesskey="p" rel="prev">Handler Wrappers</a>, Up: <a href="#Guidance" accesskey="u" rel="up">Guidance</a> &nbsp; [<a href="#SEC_Contents" title="Table of contents" rel="contents">Contents</a>][<a href="#Concept-Index" title="Index" rel="index">Index</a>]</p>
</div>
<span id="Responses-1"></span><h3 class="section">1.4 Responses</h3>
<p>Safsaf provides helpers that return <code>(values response body)</code>
directly:
</p>
<div class="example lisp">
<pre class="lisp">;; HTML — streams an SXML tree
(html-response '(div (h1 &quot;Hello&quot;) (p &quot;world&quot;)))
;; JSON — takes a JSON string
(json-response (scm-&gt;json-string '((&quot;ok&quot; . #t))))
;; Plain text
(text-response &quot;pong&quot;)
;; Redirect (default 303 See Other)
(redirect-response &quot;/login&quot;)
(redirect-response &quot;/new-item&quot; #:code 302)
;; Error responses
(not-found-response)
(bad-request-response &quot;Missing field&quot;)
(forbidden-response)
</pre></div>
<p><code>html-response</code>, <code>json-response</code>, <code>text-response</code>, and
<code>redirect-response</code> accept <code>#:code</code> and <code>#:headers</code> for
overrides. The error helpers (<code>not-found-response</code>, etc.) accept
<code>#:headers</code> but have a fixed status code.
</p>
<p>For content negotiation, use <code>negotiate-content-type</code>:
</p>
<div class="example lisp">
<pre class="lisp">(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-&gt;json-string (item-&gt;alist item))))
(else
(html-response `(div (h1 ,(item-title item))))))))
</pre></div>
<hr>
</div>
<div class="section" id="Request-Parsing">
<div class="header">
<p>
Next: <a href="#Parameter-Parsing" accesskey="n" rel="next">Parameter Parsing</a>, Previous: <a href="#Responses" accesskey="p" rel="prev">Responses</a>, Up: <a href="#Guidance" accesskey="u" rel="up">Guidance</a> &nbsp; [<a href="#SEC_Contents" title="Table of contents" rel="contents">Contents</a>][<a href="#Concept-Index" title="Index" rel="index">Index</a>]</p>
</div>
<span id="Request-Parsing-1"></span><h3 class="section">1.5 Request Parsing</h3>
<span id="Form-bodies"></span><h4 class="subheading">Form bodies</h4>
<p><code>parse-form-body</code> reads a URL-encoded POST body and returns an
alist of string pairs:
</p>
<div class="example lisp">
<pre class="lisp">(define (handle-login request body-port)
(let* ((form (parse-form-body request body-port))
(username (assoc-ref form &quot;username&quot;))
(password (assoc-ref form &quot;password&quot;)))
(if (valid-credentials? username password)
(redirect-response &quot;/dashboard&quot;)
(text-response &quot;Invalid login&quot; #:code 401))))
</pre></div>
<span id="Query-strings"></span><h4 class="subheading">Query strings</h4>
<p><code>parse-query-string</code> extracts query parameters from the request
URL:
</p>
<div class="example lisp">
<pre class="lisp">(let ((qs (parse-query-string request)))
(assoc-ref qs &quot;page&quot;)) ;; =&gt; &quot;2&quot; or #f
</pre></div>
<span id="Multipart"></span><h4 class="subheading">Multipart</h4>
<p>For file uploads, use <code>parse-multipart-body</code>:
</p>
<div class="example lisp">
<pre class="lisp">(let* ((parts (parse-multipart-body request body-port))
(form (multipart-text-fields parts))
(file (parts-ref parts &quot;avatar&quot;)))
;; form is an alist of text fields
;; file is a &lt;part&gt; record — read its body with (part-body file)
...)
</pre></div>
<span id="Cookies"></span><h4 class="subheading">Cookies</h4>
<p>Read cookies with <code>request-cookie-ref</code> or
<code>request-cookies</code>. Set them via response headers with
<code>set-cookie-header</code> and <code>delete-cookie-header</code>:
</p>
<div class="example lisp">
<pre class="lisp">(request-cookie-ref request &quot;theme&quot;) ;; =&gt; &quot;dark&quot; or #f
(text-response &quot;ok&quot;
#:headers (list (set-cookie-header &quot;theme&quot; &quot;dark&quot;
#:path &quot;/&quot;
#:http-only #t)))
</pre></div>
<hr>
</div>
<div class="section" id="Parameter-Parsing">
<div class="header">
<p>
Next: <a href="#Sessions" accesskey="n" rel="next">Sessions</a>, Previous: <a href="#Request-Parsing" accesskey="p" rel="prev">Request Parsing</a>, Up: <a href="#Guidance" accesskey="u" rel="up">Guidance</a> &nbsp; [<a href="#SEC_Contents" title="Table of contents" rel="contents">Contents</a>][<a href="#Concept-Index" title="Index" rel="index">Index</a>]</p>
</div>
<span id="Parameter-Parsing-1"></span><h3 class="section">1.6 Parameter Parsing</h3>
<p><code>parse-params</code> 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
<code>&lt;invalid-param&gt;</code>), and options like <code>#:required</code> or
<code>#:default</code>.
</p>
<div class="example lisp">
<pre class="lisp">(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)) ;; =&gt; 1 (integer, not string)
</pre></div>
<p>Built-in processors: <code>as-string</code>, <code>as-integer</code>,
<code>as-number</code>, <code>as-checkbox</code>, <code>as-one-of</code>,
<code>as-matching</code>, <code>as-predicate</code>.
</p>
<span id="Form-params-with-CSRF"></span><h4 class="subheading">Form params with CSRF</h4>
<p>For POST forms, use <code>parse-form-params</code> instead — it
automatically checks the CSRF token (from
<code>csrf-handler-wrapper</code>) before parsing:
</p>
<div class="example lisp">
<pre class="lisp">(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))))
</pre></div>
<p><code>any-invalid-params?</code> returns <code>#t</code> if any value failed
validation. <code>field-errors</code> returns a list of error message
strings for a given field, suitable for rendering next to form inputs.
</p>
<hr>
</div>
<div class="section" id="Sessions">
<div class="header">
<p>
Next: <a href="#Templating" accesskey="n" rel="next">Templating</a>, Previous: <a href="#Parameter-Parsing" accesskey="p" rel="prev">Parameter Parsing</a>, Up: <a href="#Guidance" accesskey="u" rel="up">Guidance</a> &nbsp; [<a href="#SEC_Contents" title="Table of contents" rel="contents">Contents</a>][<a href="#Concept-Index" title="Index" rel="index">Index</a>]</p>
</div>
<span id="Sessions-1"></span><h3 class="section">1.7 Sessions</h3>
<p>Sessions use HMAC-signed cookies via <code>(webutils sessions)</code>.
Set up a session config and apply the wrapper:
</p>
<div class="example lisp">
<pre class="lisp">(define session-mgr
(make-session-config &quot;my-secret-key&quot;
#:cookie-name &quot;my-session&quot;))
(define routes
(wrap-routes my-routes
(make-session-handler-wrapper session-mgr)))
</pre></div>
<p>Inside a handler, <code>(current-session)</code> returns the session data
(an alist) or <code>#f</code> if no valid session exists.
</p>
<p>To set session data, include a <code>session-set</code> header in the
response. To delete, use <code>session-delete</code>:
</p>
<div class="example lisp">
<pre class="lisp">;; Set session
(redirect-response &quot;/&quot;
#: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 &quot;/&quot;
#:headers (list (session-delete session-mgr)))
</pre></div>
<hr>
</div>
<div class="section" id="Templating">
<div class="header">
<p>
Next: <a href="#Static-Files" accesskey="n" rel="next">Static Files</a>, Previous: <a href="#Sessions" accesskey="p" rel="prev">Sessions</a>, Up: <a href="#Guidance" accesskey="u" rel="up">Guidance</a> &nbsp; [<a href="#SEC_Contents" title="Table of contents" rel="contents">Contents</a>][<a href="#Concept-Index" title="Index" rel="index">Index</a>]</p>
</div>
<span id="Templating-1"></span><h3 class="section">1.8 Templating</h3>
<p><code>write-shtml-as-html/streaming</code> works like htmlprag&rsquo;s
<code>write-shtml-as-html</code>, but any procedure in the SHTML tree is
called as <code>(proc port)</code> and can write dynamic content directly.
</p>
<p><code>streaming-html-response</code> wraps this into a response: give it an
SHTML tree (with optional procedure slots) and it returns
<code>(values response body)</code> ready for a handler.
</p>
<div class="example lisp">
<pre class="lisp">(define (base-layout title content-proc)
`(*TOP*
(*DECL* DOCTYPE html)
(html
(head (title ,title))
(body
(nav (a (@ (href &quot;/&quot;)) &quot;Home&quot;))
(main ,content-proc)
(footer (p &quot;Footer&quot;))))))
</pre></div>
<p>The layout is plain SHTML with a procedure in the <var>content-proc</var>
position. Use <code>streaming-html-response</code> to send it:
</p>
<div class="example lisp">
<pre class="lisp">(define (index-page request body-port)
(streaming-html-response
(base-layout &quot;Home&quot;
(lambda (port)
(write-shtml-as-html
`(div (h1 &quot;Welcome&quot;)
(p &quot;Content goes here.&quot;))
port)))))
</pre></div>
<p>You can also call <code>write-shtml-as-html/streaming</code> directly when
you need to write SHTML with procedure slots to an arbitrary port.
</p>
<hr>
</div>
<div class="section" id="Static-Files">
<div class="header">
<p>
Previous: <a href="#Templating" accesskey="p" rel="prev">Templating</a>, Up: <a href="#Guidance" accesskey="u" rel="up">Guidance</a> &nbsp; [<a href="#SEC_Contents" title="Table of contents" rel="contents">Contents</a>][<a href="#Concept-Index" title="Index" rel="index">Index</a>]</p>
</div>
<span id="Static-Files-1"></span><h3 class="section">1.9 Static Files</h3>
<p><code>make-static-handler</code> returns a handler that serves files from a
directory. Pair it with a wildcard route:
</p>
<div class="example lisp">
<pre class="lisp">(route-group '(&quot;static&quot;)
(route 'GET '(. path)
(make-static-handler &quot;./public&quot;
#:cache-control '((max-age . 3600)))))
</pre></div>
<p>This serves <code>/static/css/style.css</code> from
<code>./public/css/style.css</code>. The handler supports
<code>If-Modified-Since</code> for 304 responses.
</p>
<hr>
</div>
</div>
<div class="chapter" id="API">
<div class="header">
<p>
Next: <a href="#Version-History" accesskey="n" rel="next">Version History</a>, Previous: <a href="#Guidance" accesskey="p" rel="prev">Guidance</a>, Up: <a href="#Top" accesskey="u" rel="up">Overview</a> &nbsp; [<a href="#SEC_Contents" title="Table of contents" rel="contents">Contents</a>][<a href="#Concept-Index" title="Index" rel="index">Index</a>]</p>
</div>
<span id="API-1"></span><h2 class="chapter">2 API</h2>
<p>The following is the list of modules provided by this library.
</p>
<ul class="section-toc">
<li><a href="#safsaf" accesskey="1">(safsaf)</a></li>
</ul>
<hr>
<div class="section" id="safsaf">
<div class="header">
<p>
Up: <a href="#API" accesskey="u" rel="up">API</a> &nbsp; [<a href="#SEC_Contents" title="Table of contents" rel="contents">Contents</a>][<a href="#Concept-Index" title="Index" rel="index">Index</a>]</p>
</div>
<span id="g_t_0028safsaf_0029"></span><h3 class="section">2.1 (safsaf)</h3>
<ul class="section-toc">
<li><a href="#Procedures" accesskey="1">Procedures</a></li>
</ul>
<div class="subsection" id="Procedures">
<h4 class="subsection">2.1.1 Procedures</h4>
<dl class="def">
<dt id="index-default_002dmethod_002dnot_002dallowed_002dhandler"><span class="category">Procedure: </span><span><strong>default-method-not-allowed-handler</strong> <em>a b</em><a href='#index-default_002dmethod_002dnot_002dallowed_002dhandler' class='copiable-anchor'> &para;</a></span></dt>
<dd><p>Return a 405 Method Not Allowed response with an Allow header listing
ALLOWED-METHODS.
</p>
</dd></dl>
<dl class="def">
<dt id="index-run_002dsafsaf"><span class="category">Procedure: </span><span><strong>run-safsaf</strong> <em>_ KEY: #:host #:port #:method-not-allowed? #:method-not-allowed-handler #:connection-buffer-size</em><a href='#index-run_002dsafsaf' class='copiable-anchor'> &para;</a></span></dt>
<dd><p>Start a Safsaf web server.
</p>
<p>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.
</p>
<p>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.
</p>
<p>When METHOD-NOT-ALLOWED? is #t (the default), requests that match a
route&rsquo;s path but not its method receive a 405 response with an Allow
header. METHOD-NOT-ALLOWED-HANDLER is a procedure (request
allowed-methods) -&gt; (values response body) that produces the 405
response; the default returns plain text.
</p>
<p>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.
</p>
</dd></dl>
<hr>
</div>
</div>
</div>
<div class="appendix" id="Version-History">
<div class="header">
<p>
Next: <a href="#Copying-Information" accesskey="n" rel="next">Copying Information</a>, Previous: <a href="#API" accesskey="p" rel="prev">API</a>, Up: <a href="#Top" accesskey="u" rel="up">Overview</a> &nbsp; [<a href="#SEC_Contents" title="Table of contents" rel="contents">Contents</a>][<a href="#Concept-Index" title="Index" rel="index">Index</a>]</p>
</div>
<span id="Version-History-1"></span><h2 class="appendix">Appendix A Version History</h2>
<dl compact="compact">
<dt><span><em>Version 0.1</em></span></dt>
<dd><ul>
<li> Initial release.
</li><li> Built on the code of the Guix Data Serivce, plus other web
services like the Guix Build Coordinator and Nar Herder.
</li><li> Written using Claude Opus 4.6 using Claude Code.
</li></ul>
</dd>
</dl>
<hr>
</div>
<div class="appendix" id="Copying-Information">
<div class="header">
<p>
Next: <a href="#Concept-Index" accesskey="n" rel="next">Concept Index</a>, Previous: <a href="#Version-History" accesskey="p" rel="prev">Version History</a>, Up: <a href="#Top" accesskey="u" rel="up">Overview</a> &nbsp; [<a href="#SEC_Contents" title="Table of contents" rel="contents">Contents</a>][<a href="#Concept-Index" title="Index" rel="index">Index</a>]</p>
</div>
<span id="Copying-Information-1"></span><h2 class="appendix">Appendix B Copying Information</h2>
<p>Copyright &copy; 2026 Christopher Baines &lt;mail@cbaines.net&gt;
</p>
<p>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.
</p>
<hr>
</div>
<div class="unnumbered" id="Concept-Index">
<div class="header">
<p>
Next: <a href="#Data-Type-Index" accesskey="n" rel="next">Data Type Index</a>, Previous: <a href="#Copying-Information" accesskey="p" rel="prev">Copying Information</a>, Up: <a href="#Top" accesskey="u" rel="up">Overview</a> &nbsp; [<a href="#SEC_Contents" title="Table of contents" rel="contents">Contents</a>][<a href="#Concept-Index" title="Index" rel="index">Index</a>]</p>
</div>
<span id="Concept-Index-1"></span><h2 class="unnumbered">Concept Index</h2>
<hr>
</div>
<div class="unnumbered" id="Data-Type-Index">
<div class="header">
<p>
Next: <a href="#Procedure-Index" accesskey="n" rel="next">Procedure Index</a>, Previous: <a href="#Concept-Index" accesskey="p" rel="prev">Concept Index</a>, Up: <a href="#Top" accesskey="u" rel="up">Overview</a> &nbsp; [<a href="#SEC_Contents" title="Table of contents" rel="contents">Contents</a>][<a href="#Concept-Index" title="Index" rel="index">Index</a>]</p>
</div>
<span id="Data-Type-Index-1"></span><h2 class="unnumbered">Data Type Index</h2>
<hr>
</div>
<div class="unnumbered" id="Procedure-Index">
<div class="header">
<p>
Next: <a href="#Variable-Index" accesskey="n" rel="next">Variable Index</a>, Previous: <a href="#Data-Type-Index" accesskey="p" rel="prev">Data Type Index</a>, Up: <a href="#Top" accesskey="u" rel="up">Overview</a> &nbsp; [<a href="#SEC_Contents" title="Table of contents" rel="contents">Contents</a>][<a href="#Concept-Index" title="Index" rel="index">Index</a>]</p>
</div>
<span id="Procedure-Index-1"></span><h2 class="unnumbered">Procedure Index</h2>
<table><tr><th valign="top">Jump to: &nbsp; </th><td><a class="summary-letter" href="#Procedure-Index_fn_letter-D"><b>D</b></a>
&nbsp;
<a class="summary-letter" href="#Procedure-Index_fn_letter-R"><b>R</b></a>
&nbsp;
</td></tr></table>
<table class="index-fn" border="0">
<tr><td></td><th align="left">Index Entry</th><td>&nbsp;</td><th align="left"> Section</th></tr>
<tr><td colspan="4"> <hr></td></tr>
<tr><th id="Procedure-Index_fn_letter-D">D</th><td></td><td></td></tr>
<tr><td></td><td valign="top"><a href="#index-default_002dmethod_002dnot_002dallowed_002dhandler"><code>default-method-not-allowed-handler</code></a>:</td><td>&nbsp;</td><td valign="top"><a href="#safsaf">safsaf</a></td></tr>
<tr><td colspan="4"> <hr></td></tr>
<tr><th id="Procedure-Index_fn_letter-R">R</th><td></td><td></td></tr>
<tr><td></td><td valign="top"><a href="#index-run_002dsafsaf"><code>run-safsaf</code></a>:</td><td>&nbsp;</td><td valign="top"><a href="#safsaf">safsaf</a></td></tr>
<tr><td colspan="4"> <hr></td></tr>
</table>
<table><tr><th valign="top">Jump to: &nbsp; </th><td><a class="summary-letter" href="#Procedure-Index_fn_letter-D"><b>D</b></a>
&nbsp;
<a class="summary-letter" href="#Procedure-Index_fn_letter-R"><b>R</b></a>
&nbsp;
</td></tr></table>
<hr>
</div>
<div class="unnumbered" id="Variable-Index">
<div class="header">
<p>
Previous: <a href="#Procedure-Index" accesskey="p" rel="prev">Procedure Index</a>, Up: <a href="#Top" accesskey="u" rel="up">Overview</a> &nbsp; [<a href="#SEC_Contents" title="Table of contents" rel="contents">Contents</a>][<a href="#Concept-Index" title="Index" rel="index">Index</a>]</p>
</div>
<span id="Variable-Index-1"></span><h2 class="unnumbered">Variable Index</h2>
</div>
</div>
</body>
</html>