From 243f9a8ed584d9fb0fbfec09ba54a2c2d864fe2e Mon Sep 17 00:00:00 2001 From: Julien Calixte Date: Tue, 2 Jun 2026 23:26:13 +0200 Subject: [PATCH] docs: add handoff for longpoll HTTP/2 truncation bug The AuthSession cookie fix is shipped, but PouchDB longpoll requests get truncated at ~43s by Traefik on HTTP/2. HTTP/1.1 holds fine; CouchDB and nginx are clean. Captures the diagnosis trail and the three options for the next session so the work isn't re-derived. --- HANDOFF.md | 94 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 HANDOFF.md diff --git a/HANDOFF.md b/HANDOFF.md new file mode 100644 index 0000000..e4111c1 --- /dev/null +++ b/HANDOFF.md @@ -0,0 +1,94 @@ +# Handoff: PouchDB longpoll truncates at ~43s (Traefik HTTP/2) + +## What's done — original cookie bug is fixed + +The cross-origin `AuthSession` cookie bug from the previous handoff is **fully resolved**, deployed, and verified end-to-end. + +- nginx sidecar fronts CouchDB (`nginx/Dockerfile`, `nginx/default.conf`). +- `proxy_cookie_flags AuthSession secure;` adds `Secure` to the cookie that CouchDB itself refuses to mark (mochiweb only checks the raw socket scheme; `x_forwarded_proto`/`x_forwarded_ssl` are ignored for cookie attribution in CouchDB 3.5). +- `couch_httpd_auth.same_site = none` is now persisted via `couchdb-init` (previously only set live; would vanish on container rebuild because `/opt/couchdb/etc/local.d/` is not in the data volume). +- Coolify domain reassigned in UI from `couchdb` to `nginx:5984`. `couchdb` is now internal-only. + +Verified externally: +``` +set-cookie: AuthSession=...; HttpOnly; SameSite=None; Secure +``` + +## New problem (the one to solve) + +`_changes?feed=longpoll` requests from PouchDB receive a truncated response: +``` +{"results":[ + +``` +…then the connection is torn down before the closing `],"last_seq":"…"}`. Browser surfaces `SyntaxError: JSON.parse: unexpected end of data`. PouchDB can't process replication. + +## Diagnosis (already done — don't repeat) + +The cut happens at **~43s** and is **HTTP/2 specific**. Tested every segment of the chain: + +| Path | Result | +|---|---| +| `curl → couchdb` directly (via `docker exec` on the couchdb container) | Holds indefinitely; ^C to stop | +| `curl → nginx → couchdb` (via `docker run --network container:`) | Holds indefinitely | +| `curl HTTP/2 → Traefik → nginx → couchdb` | Dies at **43.77s** with `HTTP/2 stream … INTERNAL_ERROR (err 2)` | +| `curl --http1.1 → Traefik → nginx → couchdb` | Held for **2:05+** before user ^C'd | + +⇒ **Traefik is the source of the kill, and only for HTTP/2.** CouchDB and nginx are clean. + +The reproducer (cookie is from `POST /_session` saved to `/tmp/c`): +```bash +time curl -v --cookie /tmp/c \ + 'https://couch.apoena.dev/vaquant/_changes?feed=longpoll&since=now&heartbeat=10000&timeout=60000' +``` +With `--http1.1` the same request is fine. + +43s does not match any documented Traefik default. It's a Coolify-shipped or Go-stdlib HTTP/2 server timer. Related but not identical to the regression in [traefik/traefik#11986](https://github.com/traefik/traefik/issues/11986) — that one fires in milliseconds, not ~43s. + +## What's NOT the cause (already ruled out) + +- Not CouchDB's `chttpd.changes_timeout` or any CouchDB knob — direct connection holds. +- Not nginx — `proxy_read_timeout 1h`, `proxy_buffering off`, and direct nginx→couchdb path holds. +- Not the auth cookie — cookie is now valid through the whole chain. +- Not a flaky network — 43s is consistent across runs. +- Not the heartbeat — heartbeat newlines do flow through, they don't keep the H2 stream alive on Traefik's side. + +## Decision pending (asked the user, no answer yet) + +Three options on the table: + +**A. Disable HTTP/2 on Coolify's HTTPS entrypoint** (recommended). +Edit Coolify's Traefik static config, usually at `/data/coolify/proxy/traefik.conf` on the Coolify host. Add to the HTTPS entrypoint's TLS section, restrict ALPN to `http/1.1` only — or set `--entryPoints.https.http2.maxConcurrentStreams=0` or similar. Side effect: all HTTPS traffic on this Coolify server falls back to HTTP/1.1. For PouchDB's request pattern, this is fine; HTTP/2 multiplexing isn't load-bearing. + +**B. Find the specific Traefik HTTP/2 timeout knob.** +The 43s value is not in Traefik's documented defaults. Needs Coolify Traefik config inspection + trial. Higher effort, brittle. + +**C. Live with it.** +PouchDB does retry. Every ~43s of idle replication produces an error + reconnect. Functionally works, log noise + slight latency on resume of activity. + +User leaned toward A but hasn't confirmed. Needs to know if they can edit Coolify's Traefik config. + +## Repo layout (relevant files) + +- `docker-compose.yml` — services: `couchdb` (internal), `nginx` (public via Traefik), `couchdb-init` (one-shot config), `signup` (Gleam service). +- `nginx/Dockerfile` + `nginx/default.conf` — nginx sidecar that adds `Secure` cookie flag. +- `signup/` — Gleam HTTP service for invite-gated user signup; `manifest.toml` is committed (locked dep tree). Uses wisp 2.x, mist 6.x. +- `README.md` — explains the architecture (now including why nginx exists). +- Coolify network UUID `lvw8efvnvsxduodrkg68zul3` is hardcoded in three places (two labels + one network alias); documented in README. + +## Out of scope + +- The frontend/PouchDB app lives in a separate repo (`vaquant`). No changes needed there for the cookie fix; for option C (longpoll workaround at app level) the app would need adjusting. +- CORS is correct, do not touch. +- The signup service has its own deploy story (built from `./signup`), not relevant to this bug. + +## Verify the cookie fix is still working (sanity check before touching anything) + +```bash +curl -i -X POST https://couch.apoena.dev/_session \ + -H 'Content-Type: application/x-www-form-urlencoded' \ + -d "name=admin&password=$COUCHDB_PASSWORD" | grep -i set-cookie +``` +Expect: `... HttpOnly; SameSite=None; Secure`. + +If `Secure` is missing, the Coolify domain assignment slipped off `nginx` and is hitting `couchdb` directly — re-assign in the Domains tab.