Skip to content

Architecture

Request lifecycle

2 min readUpdated Verified against a661527 on 2026-09-26

Page actions

ChatGPT and Claude are third-party services. The link gives them this page's public address and a short instruction. This site records nothing.

One request, GET /api/ang/712, followed from the browser through the host rewrite, the request handler, the dispatcher and the bounded context to SQLite and back, in the order the code does it.

Step through what webapp/serve.py does with one request. Everything in the poster is read from the code named in its footer; the wiki gate keeps the footer honest.

Request lifecycle The browser calls /api same-origin; the host rewrites it to the API; H._handle assigns a request id, answers liveness and readiness first, then routes /api/v1 or /api through the api() dispatcher and the ROUTES table into the bounded context that owns the route; the context reads the read-only, immutable SQLite database; _respond adds caching, an ETag, the security headers, X-Request-Id and X-Service; the access log records the path only. Errors map to 404, 400 or a generic 500. 08 · Request lifecycle What happens to GET /api/ang/712 between the browser and the database — in theorder the code does it Browser or app — GET /api/ang/712Browser or appGET /api/ang/712Vercel rewrite, same origin — to a function: reader, or allVercel rewrite, same originto a function: reader, or allA request id, then the probes — /healthz · /readyzA request id, then the probes/healthz · /readyz/api/v1/* or /api/* — same bodies; v1 errors strict/api/v1/* or /api/*same bodies; v1 errors strictThe dispatcher — ROUTES[(seg1, seg2)]The dispatcherROUTES[(seg1, seg2)]SELECT … WHERE ang = ?The reader context — sggs/reader.pyThe reader contextsggs/reader.pySQLite, read-only — mode=ro&immutable=1SQLite, read-onlymode=ro&immutable=1_respond — cache policy, ETag → 304_respondcache policy, ETag → 304_sec_headers — nosniff, DENY, HSTS, ids_sec_headersnosniff, DENY, HSTS, ids200 · 304Browser or app — Ang 712, every line verbatimBrowser or appAng 712, every line verbatimThe access log — the path, never the queryThe access logthe path, never the queryA bounded server — 48 workers, 15 s idle timeoutA bounded server48 workers, 15 s idle timeoutErrors — 404 · 400 · a generic 500Errors404 · 400 · a generic 500 a personexternal systemcomponent in this projectdata at restexplanationgate: fails the build or the deployflow: data or control moves this wayreference: pins, reads or derives from Source of truth: webapp/serve.py · webapp/sggs/core.py ·gateway/routes.json · tools/gen_gateway.py v1.3.10 · verified 2026-09-27 · d85c2adSri Guru Granth Sahib Ji — Knowledge Base · engineering wiki
Poster 08 — the request lifecycle, from the browser to SQLite and back · open full size

The order, in prose

  1. Same-origin. The browser calls /api/ang/712 on the site’s own host. A Vercel rewrite, generated from the route table (bounded contexts), forwards it to the API. No CORS, no API base URL in the frontend.
  2. Request id, then probes. H._handle records the start time and a request id — an incoming X-Request-Id or x-vercel-id is reused only when it is a plain token — and answers /healthz (liveness) and /readyz (every declared table present) before anything else.
  3. Two API surfaces. /api/v1/* is strict: unknown endpoint → 404; every error is {error: {code, message, request_id}}. /api/* is the legacy surface whose bodies the golden contract pins byte for byte. Anything else is a static file from a realpath jail.
  4. Dispatch. api() looks up (first segment, second segment) in ROUTES — 29 routes, each tagged with its bounded context. In split mode a route outside ENABLED is a 404.
  5. The context reads SQLite. The handler runs parameterised SQL on a connection opened mode=ro&immutable=1 with query_only; the app can never write to the scripture.
  6. Respond. Cacheable segments get public, max-age=300, s-maxage=3600 and a sha256 ETag (a repeat becomes a 304); everything else is no-store. Every response carries nosniff, X-Frame-Options: DENY, Referrer-Policy: no-referrer, HSTS, X-Request-Id and X-Service.
  7. Log the path only. One JSON line per request with method, path, status, milliseconds and the id. The query string is never written (ADR-0009).
  8. Errors and limits. ApiError keeps its status; a bad parameter is a 400 with a short message; anything unexpected is a generic 500 with the detail on the server log only. Handler threads are capped (SGGS_MAX_WORKERS) and idle sockets time out after 15 s.

Try it

With serve.py running locally:

Terminal window
curl -si 'http://localhost:7777/api/ang/712' | sed -n '1,12p'

Look for X-Request-Id, X-Service: all, Cache-Control: public, max-age=300, s-maxage=3600 and the ETag. Send the ETag back in If-None-Match and the answer is a 304.

Related: Engineering invariants · the API reference on this site (generated from contract/openapi.json)