writeonce/docs/examples/writeonce-framework/http/nego.wo
shoney.arickathil 82713e4010 feat: framework v1 slice 2 — the remaining ledger, ten items
- After seam: interface After + Aw + use_after; dispatch funnels every
  response (handler/short-circuit/404/405) through the after chain;
  the WS 101 sentinel skips it (never serialized)
- http/secure.wo: SecurityHeaders (nosniff/DENY/referrer; HSTS stays
  at the TLS proxy), Cors (preflight 204 before + origin stamp after,
  one class both halves), HostAllow (421), client_ip (XFF parsing —
  peer VERIFY stays story 35)
- http/nego.wo: accepts() (exact, type/*, */*; q stripped not ranked),
  etag_for (quoted base64 sha256), with_etag (If-None-Match -> 304)
- router: *rest wildcard (last segment, empty rest matches), Group
  (prefix + routes + group middleware) + Gmw prefix-scoped entries,
  App.mount; new App fields carry defaults so standing ctor literals
  keep compiling
- Req grows ctx bag; parse rejects duplicate Content-Length (400,
  RFC 9112 §6.3)
- web-app exercises all of it; gate grows 26 -> 38 checks (wildcards,
  group+ctx, etag+304, 406/200 negotiation, sec headers, 421,
  preflight+origin stamp, dup-CL 400)
- ledger rows flipped; dep graph section 3 grown (slice-2 done nodes,
  crypto gate cleared, cookie/CSRF/session/webhook/JWT now ready)
- merges: chat-ws-lifecycle (digests for ETag; WS + lifecycle ride
  along) + site-sample (second consumer gate); battery 13/13

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-23 06:45:51 +02:00

49 lines
No EOL
1.8 KiB
Text

-- http/nego.wo — framework v1 slice 2: response-side content negotiation
-- and ETag / conditional requests (the crypto slice's first ledger
-- consumer beyond the WS handshake).
-- Does the request accept this media type? Absent Accept = yes (RFC 9110
-- §12.5.1: no header means anything goes). Matching is exact, type/*,
-- or */*; q-values are stripped, not ranked — v1 answers CAN I send
-- this, not WHICH ONE is best (a ranking negotiation waits for an app
-- that serves alternates).
pub fn accepts(req: Req, mtype: Text) -> Bool {
let acc = req.headers["accept"];
if acc == nil { return true; }
let slash = index_of(mtype, "/");
let major = mtype;
if slash >= 0 { major = substr(mtype, 0, slash); }
for part in split(to_lower("${acc}"), ",") {
let item = trim(part);
let semi = index_of(item, ";");
if semi >= 0 { item = trim(substr(item, 0, semi)); }
if item == mtype { return true; }
if item == "*/*" { return true; }
if item == "${major}/*" { return true; }
}
return false;
}
-- A strong ETag for a body: quoted base64 of its SHA-256. Deterministic,
-- content-addressed — two identical bodies share one tag across
-- restarts and shards.
pub fn etag_for(body: Text) -> Text {
return "\"${base64_encode(sha256(bytes_of_text(body)))}\"";
}
-- Stamp the response's ETag and collapse it to 304 when the request's
-- If-None-Match already has it. The 304 keeps the etag header and
-- drops the body (RFC 9110 §15.4.5). Call it last in a handler:
-- return with_etag(req, ok_json(body));
pub fn with_etag(req: Req, take r: Resp) -> Resp {
let tag = etag_for(r.body);
r.headers["etag"] = tag;
let inm = req.headers["if-none-match"];
if inm != nil {
if trim(inm) == tag {
r.status = 304;
r.body = "";
}
}
return r;
}