Skip to content

fix(collab): HTTP-layer resource caps — proxy body limit + SSE connection caps#41

Merged
hblanken merged 1 commit into
collabfrom
fix/collab-http-resource-caps
Jun 14, 2026
Merged

fix(collab): HTTP-layer resource caps — proxy body limit + SSE connection caps#41
hblanken merged 1 commit into
collabfrom
fix/collab-http-resource-caps

Conversation

@hblanken

Copy link
Copy Markdown

Stability hardening pass (PR 2 of 5). Bounds two unbounded HTTP resources on the single-replica task.

S3 — preview proxy request-body limit

handlePreviewHttp forwarded request bodies of any size to the in-container dev server. A multi-GB upload (buggy SPA, or hostile POST) would buffer through opencode's heap before the upstream's own 413, swinging the whole task into memory pressure.

Now: reject declared Content-Length > 50 MB at the proxy edge with a 413 before opening the upstream connection. 50 MB is generous — real frontend uploads go straight to S3, not through the dev server.

S4 — SSE connection caps

Each iframe / participant holds a long-lived SSE stream + 20-s heartbeat timer. Two new bounds:

  • Per-session concurrency cap (8) — realistic max is 2-3 users × 2-3 tabs; the 9th connection gets a 429 with a "close a tab" hint. Caps the fd + per-stream-closure footprint a reconnect storm or many-tabs client could pile on.
  • Idle recycle (2 h) — a stream that has delivered zero real events (keepalives don't count) for 2 h is closed server-side. The SPA auto-reconnects and re-syncs via the connect-time snapshot, so an active session is never interrupted — only quiet/abandoned background tabs get recycled, capping max connection age.

Refactor: cancel() and the idle-recycle path now share one guarded teardown() (clear heartbeat, unregister, set offline, broadcast typing_stop + participant_left) so neither double-runs.

Files

  • preview-router.tsMAX_PREVIEW_BODY_BYTES + 413 guard in handlePreviewHttp
  • router.tsMAX_SSE_PER_SESSION cap + SSE_IDLE_DISCONNECT_MS recycle + shared teardown()

Test plan

  • Deploy off this branch
  • curl -X POST the preview proxy with a Content-Length: 60000000 header → 413 before the request reaches the dev server
  • Normal preview browsing + small POSTs unaffected
  • Open 9 tabs on one collab session → 9th gets 429 "too many open connections"; closing a tab frees the slot
  • Leave a session idle 2 h → CloudWatch shows SSE idle recycle; the still-open tab silently reconnects (no visible UX change)
  • Active session (events flowing) is NOT recycled at the 2 h mark
  • Clean tab close still fires participant_left exactly once (no double-teardown)

🤖 Generated with Claude Code

…tion caps

Two unbounded HTTP resources bounded on the single-replica task.

S3 — preview proxy request-body limit
  handlePreviewHttp forwarded request bodies of any size to the in-
  container dev server.  A multi-GB upload (buggy SPA, or hostile POST)
  would buffer through opencode's heap before the upstream's own 413,
  swinging the whole task into memory pressure.  Reject declared
  Content-Length > 50 MB at the proxy edge with a 413 before opening
  the upstream connection.  50 MB is generous — real frontend uploads
  go straight to S3, not through the dev server.

S4 — SSE connection caps
  Each iframe / participant holds a long-lived SSE stream with a 20-s
  heartbeat timer.  Two new bounds:
    - Per-session concurrency cap (8).  Realistic max is 2-3 users ×
      2-3 tabs; the 9th connection gets a 429 with a "close a tab"
      hint.  Caps the fd + per-stream-closure footprint a reconnect
      storm or many-tabs client could pile on.
    - Idle recycle (2 h).  A stream that has delivered zero REAL events
      (keepalives don't count) for 2 h is closed server-side.  The SPA
      auto-reconnects and re-syncs via the connect-time snapshot, so an
      active session is never interrupted — only quiet/abandoned
      background tabs get recycled, capping max connection age.

  Refactor: cancel() and the idle-recycle path now share one guarded
  teardown() (clear heartbeat, unregister, set offline, broadcast
  typing_stop + participant_left) so neither double-runs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@hblanken hblanken merged commit 4d0a31a into collab Jun 14, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant