Request lifecycle
Page actions
- View as Markdown (opens in a new tab)
- Ask ChatGPT (opens in a new tab)
- Ask Claude (opens in a new tab)
ChatGPT and Claude are third-party services. The link gives them this page's public address and a short instruction. This site records nothing.
Preview: the published page may be older or missing.
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.
https://docs.gurbanisoul.com/architecture/request-lifecycle/
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.
- Same-origin call. The browser (or a wiki widget) calls /api/ang/712 on the site’s own host. There is no CORS and no API base URL in the frontend: a Vercel rewrite forwards the request — on staging to the context’s own function (here reader); in production today (1.3.10) to all, the whole API as one function, and from the release after 1.3.10 to the context’s function as on staging. The rules are generated from the route table.
- Identity and probes first. H._handle stamps the clock and a request id — an incoming X-Request-Id or x-vercel-id is reused only if it is a plain token, otherwise a uuid4 — then answers /healthz (liveness) and /readyz (every table each enabled context declares is present) before anything else.
- Two API surfaces, one body. /api/v1/* has strict semantics: an unknown endpoint is 404 and every error is {code, message, request_id}. /api/* is the legacy surface whose responses are byte-pinned by the golden contract. Anything else is a static file, served from a realpath jail.
- The dispatcher. api() splits the path, ensures FTS5 is detected, and looks up (first segment, second segment) in ROUTES — 29 routes, each tagged with its bounded context. In split mode a route whose context is not enabled in this process is a 404: a service never answers for another.
- The context reads SQLite. The handler in sggs/reader.py runs parameterised SQL (never string-built; FTS columns allowlisted; every integer through _int()) on a connection opened read-only and immutable, one per handler thread. The app can never write to the scripture.
- Respond: caching and headers. Responses that depend only on the immutable database (ang, shabad, lines, analytics, timing …) are cacheable for the browser and the CDN, with a sha256 ETag that turns a repeat into a 304. Every response carries nosniff, X-Frame-Options DENY, no-referrer, HSTS, the request id and X-Service (which service answered).
- Log the path, never the query. One JSON line per request goes to stderr: method, path, status, milliseconds and the request id. The query string is never written, so what people search for is not recorded anywhere. The verbatim lines of Ang 712 reach the browser.
- Errors and limits. An ApiError keeps its status (a missing composition is 404); a bad parameter is 400 with a short message; anything unexpected is a generic 500 — the detail goes to the server log, never over the wire. The server caps handler threads and times out idle sockets so a slow client cannot hold a slot.
The order, in prose
- Same-origin. The browser calls
/api/ang/712on 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. - Request id, then probes.
H._handlerecords the start time and a request id — an incomingX-Request-Idorx-vercel-idis reused only when it is a plain token — and answers/healthz(liveness) and/readyz(every declared table present) before anything else. - 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. - Dispatch.
api()looks up(first segment, second segment)inROUTES— 29 routes, each tagged with its bounded context. In split mode a route outsideENABLEDis a 404. - The context reads SQLite. The handler runs parameterised SQL on a connection opened
mode=ro&immutable=1withquery_only; the app can never write to the scripture. - Respond. Cacheable segments get
public, max-age=300, s-maxage=3600and a sha256 ETag (a repeat becomes a 304); everything else isno-store. Every response carriesnosniff,X-Frame-Options: DENY,Referrer-Policy: no-referrer, HSTS,X-Request-IdandX-Service. - 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).
- Errors and limits.
ApiErrorkeeps 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:
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)
Sri Guru Granth Sahib Ji — Knowledge Base wiki. Generated from commit c3d9360. Updated 23 October 2026. Verified against a661527 on 2026-09-26. This copy may be out of date; the page is at https://docs.gurbanisoul.com/architecture/request-lifecycle/