- surf: APIDocsProvider (one plugin) supplies a YAML or JSON OpenAPI document,
converted once at boot to compact JSON in source key order; zero or several
providers or a spec that does not convert fail boot
- http.api_docs.enabled/path/title mount GET /api-docs and /api-docs.json
(summercms.apidocs, public, refusing to replace a plugin route)
- the page loads swagger-ui-dist 5.33.1 from jsDelivr with sha384 SRI and
crossorigin on all three assets, a per-request CSP nonce for its inline
script, nosniff and no-referrer
- docs: API documentation page section, README rows, ExampleAPIDocsProvider
- bouncer: Turnstile.Verify posts secret/response/remoteip to siteverify with
a 5 s bound and PHP's open and closed modes (503 unconfigured in production,
422 on a missing, rejected or overlong token, an error or a timeout); errors
never carry the secret or the token
- bouncer: Turnstile.Middleware reads the token from the query, a JSON body or
a form body, restores the body and writes refusals through a published
TurnstileWriter or neutral JSON
- surf: factory turnstile:open|closed from turnstile.secret/turnstile.field
(a published *bouncer.Turnstile wins), client IP with trusted proxies; any
other param fails boot
- docs: Turnstile section, middleware table, README rows
- surf: LoadCORSProfiles reads http.cors_profiles.<name> (http.cors keys
without paths, validated the same way); factory cors:<profile> sets the
profile's headers; an unknown profile fails boot naming plugin and route
- BuildRouter registers OPTIONS <path> (owner surf, recovery only) for every
path whose routes name cors:<profile>; two profiles on one path, an existing
OPTIONS route or overlap with http.cors.paths fail boot
- docs: CORS profiles section, middleware table and README rows
- new module cooler: Install, Handle, OpenRedis (no URL in errors), RateLimitStore
(atomic fixed-window Lua script with MemoryStore semantics, per-instance fallback)
- serve installs cooler and publishes the Redis surf.Store before BuildRouter;
BuildRouter looks up a published Store, route:list keeps the memory store
- config keys http.rate_limit.store and redis.url; go-redis v9.23.0
- cooler README, root table row, surf README and rate-limiting, configuration
and scaling docs
Every 5xx became 499 once the request context was cancelled, so a panic or
a real database error on a request the browser had abandoned passed the
phase gate's 5xx scan. Now a 5xx is logged as 499 only when the client had
already gone when the status was written and the handler did not panic;
recoverJSON and recoverBare mark the access log entry. Every record of an
abandoned request carries client_gone=true. The gate self-test plants a
5xx with client_gone=true and must refuse it.
- one Info record "http request" per request with method, matched route
pattern, status and duration_ms; path without the query only when no
route matched
- the matched route records its pattern through a request-scoped entry,
so locale prefix rewrites do not hide it
- logs through the published *slog.Logger or slog.Default; off by default
- surf README, logging and configuration docs describe the key
- surf mounts GET {storage.uploads.public_path_prefix}/{path...} on
attach.StaticHandlerPublic when storage.uploads.serve is true
- bucket and database resolved per request; X-Content-Type-Options: nosniff
- surf README, storage and configuration docs describe the key
- Add surf.Router.AdminPrefix (additive): the admin mount path and true
when BuildRouter mounted the cabana admin, false otherwise
- serve binds with net.Listen before printing, so a busy port fails
without a misleading URL, then prints the listen address with a
browser URL and an admin status line: the admin URL, a warning naming
summer admin:create when backend_users is empty or missing, or
admin: off with the steps to add an admin controller
- The administrator count is bounded by a 2 s timeout and never fails boot
- route:list ends with the same admin status line
- surf and cabana READMEs and the console and routing docs describe the output
Look up a backpack-published resolver so a compiled translate plugin can
strip an enabled URL prefix and write a validated locale onto context.
Co-authored-by: Cursor <cursoragent@cursor.com>
- compile groups routes ServeMux refuses side by side into overlap families
and registers each under one generated method-less pattern
- the family handler tries members in registration order on literals and
Where constraints, sets their path values and runs their own wrapped chain
- no match answers the bare 404; a method mismatch answers ServeMux's 405
and Allow for the same table without the overlap
- unsupported shapes (same shape, {name...}, shadowing route) fail at boot
- README and docs/services/routing.md describe the behaviour