Every arrow in the anatomy chapter's diagram is an HTTP exchange. In its classic form โ HTTP/1.1, which the proxy speaks to this app โ it is a plain-text request/response protocol over TCP (wrapped in TLS as HTTPS), refreshingly simple to read with your eyes. Newer versions change the transport (HTTP/2 multiplexes binary frames, HTTP/3 rides QUIC over UDP) but keep exactly the same request/response model, so everything below transfers.
Here is (roughly) what the browser sends when the log page fetches meals, and what Ndiro answers:
GET /api/meals?days=7&anchor=2026-09-03 HTTP/1.1
Host: your.domain.example โ wherever the instance is deployed
Cookie: session=eyJ1c2VyX2lkIjoiโฆ โ who you are (the identity chapter)
Accept: */*
HTTP/1.1 200 OK
Content-Type: application/json โ headers: metadata about the body
โ blank line, then the body:
{"days": [{"date": "2026-09-03", "meals": [...], "totals": {"fiber_g": 12.5}}], โฆ}
A request is: method + path (with
optional ?query=params) + headers +
optional body. A response is: status
code + headers + body. That's the whole protocol surface this
app uses.
| Method | Meaning | In Ndiro |
|---|---|---|
GET | Read. Must never change state โ browsers prefetch and retry GETs freely. | Pages, /api/meals, photos |
POST | Create / act. | Add a meal, create a share link, run an AI estimate |
PUT | Replace something that has an address. | Edit a meal: PUT /api/meals/<date>/<meal_id> |
DELETE | Remove it. | Delete a meal, revoke a share |
The "GET never writes" rule is load-bearing security here, not
etiquette: Ndiro's CSRF defence (the security
chapter) assumes application data never changes on a GET.
The auth flow holds the two deliberate exceptions:
/logout clears the session, and /callback โ
forced to be a GET by the OAuth redirect โ really writes (it can create
your account and claim an invite), which is why it carries its own
guard; that chapter explains both.
Status codes are the API's error model. Grep app.py for
any of these and you'll land on the policy that produces it:
| Code | Meaning | Where you'll meet it in Ndiro |
|---|---|---|
200 | OK | Everything that worked |
302 | Redirect: "go here instead" | The whole OAuth dance; guards bouncing you to /login |
304 | Not Modified โ cached copy still good | Photo proxy when the browser's ETag matches (the storage chapter) |
400 | Your request is malformed | Missing meal description, invalid date |
401 | Not signed in | API calls with a dead session โ the JS reacts by redirecting to login |
403 | Signed in, but not allowed | Pending accounts hitting the API; non-admins hitting admin APIs |
404 | No such thing | Dead share/invite links โ deliberately indistinguishable from each other (the security chapter) |
413 | Body too large | Uploads past MAX_CONTENT_LENGTH (16 MB) |
429 | Slow down | Rate limits โ see @limiter.limit(โฆ) decorators |
500 | Server bug | Hopefully nowhere; anything unhandled |
503 | Not configured / unavailable | /login when no Google client ID is set |
HTTP has no connections in the session sense: every request stands
alone, and the server keeps no per-client channel state. Continuity is
faked with cookies โ the server sets one
(Set-Cookie: session=โฆ) and the browser automatically
attaches it to every subsequent request to that site. Ndiro's session
cookie contains a signed {'user_id': โฆ} and nothing else;
the identity chapter unpacks it byte by byte.
Think of HTTP like a stateless RPC protocol where the auth token
(the cookie) is attached by the client library on every call โ
except the "client library" is the browser, and it attaches the
cookie for any page that triggers a request to your domain,
including a malicious one. That over-eagerness is the entire CSRF
problem, and why the cookie carries a SameSite
attribute (the security chapter).
The app never speaks TLS itself. A reverse proxy (Caddy on a
Raspberry Pi, or Render's load balancer) terminates HTTPS and forwards
plain HTTP to gunicorn, adding headers like
X-Forwarded-For (the real client IP) and
X-Forwarded-Proto (that the original request was HTTPS).
Flask must be told these headers are trustworthy:
# Behind EXACTLY ONE trusted reverse proxy (Caddy on the Pi / Render's LB).
# x_for=1 makes request.remote_addr the real client so the rate limiter
# isolates clients instead of collapsing everyone into the proxy's IP.
# Do NOT keep x_for=1 if the container is ever exposed without a proxy โ
# clients could then spoof X-Forwarded-For to dodge rate limits.
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1)
Anyone can put any header on a request, so
X-Forwarded-For is only meaningful for the hops you
control. x_for=1 says "exactly one proxy I trust appended
to this list; believe one entry deep, no more". Misconfigure this and
clients can impersonate arbitrary IPs to the rate limiter โ a classic
review catch.
From any terminal, no login needed (substitute the domain you're
reading this on):
curl -i https://your.domain.example/health โ read the
status line, headers, and JSON body. Then
curl -i https://your.domain.example/api/meals and watch
the 401 come back: no cookie, no identity. Add
-H "X-Forwarded-For: 203.0.113.7" to the first and
confirm nothing changes โ behind the trusted proxy your header is
superseded by the proxy's own entry. (That protection is exactly the
topology assumption in the comment above: expose the container
without the proxy and the same header would be
believed.)