Files
coolcouch/HANDOFF.md
Julien Calixte 243f9a8ed5 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.
2026-06-02 23:26:13 +02:00

5.3 KiB

Handoff: PouchDB longpoll truncates at ~43s (Traefik HTTP/2)

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

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