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.
This commit is contained in:
Julien Calixte
2026-06-02 23:26:13 +02:00
parent 1b3fa2c540
commit 243f9a8ed5

94
HANDOFF.md Normal file
View File

@@ -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":[
<heartbeat newlines>
```
…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:<nginx>`) | 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.