๐ŸฅฆNdiro
Chapter 2 of 13

HTTP: the protocol under everything

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.

Anatomy of an exchange

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.

Methods: the verb is part of the contract

MethodMeaningIn Ndiro
GETRead. Must never change state โ€” browsers prefetch and retry GETs freely.Pages, /api/meals, photos
POSTCreate / act.Add a meal, create a share link, run an AI estimate
PUTReplace something that has an address.Edit a meal: PUT /api/meals/<date>/<meal_id>
DELETERemove 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 this app actually returns

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:

CodeMeaningWhere you'll meet it in Ndiro
200OKEverything that worked
302Redirect: "go here instead"The whole OAuth dance; guards bouncing you to /login
304Not Modified โ€” cached copy still goodPhoto proxy when the browser's ETag matches (the storage chapter)
400Your request is malformedMissing meal description, invalid date
401Not signed inAPI calls with a dead session โ€” the JS reacts by redirecting to login
403Signed in, but not allowedPending accounts hitting the API; non-admins hitting admin APIs
404No such thingDead share/invite links โ€” deliberately indistinguishable from each other (the security chapter)
413Body too largeUploads past MAX_CONTENT_LENGTH (16 MB)
429Slow downRate limits โ€” see @limiter.limit(โ€ฆ) decorators
500Server bugHopefully nowhere; anything unhandled
503Not configured / unavailable/login when no Google client ID is set

Statelessness and cookies

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.

๐Ÿ˜ From your world

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 proxy in front, and trusting headers

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:

app.py โ†—
# 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.

๐Ÿ”ฌ Try it

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.)

๐Ÿ