Skip to content

proxy-relay: bytes sent while the tunnel CONNECT is in flight are silently dropped — the peek removes the only data listener and the client is never paused, so request bodies truncate and upstreams hang #2847

Closed

Description

@dezren39

Summary

bin/proxy-relay.mjs silently drops every byte a client sends after the first TCP chunk until the upstream tunnel connection is established. The relay peeks at the first request line, removes its only data listener, then spends 0.1s–10s in tryTargets()/connectViaTunnel() before piping — and the client socket is never paused, so any bytes the client sends in that window (request bodies, the tail of a large header block) are read off the socket and discarded. The forwarded request therefore arrives truncated: a Content-Length: 1000 POST reaches the upstream with only the headers, and the upstream hangs waiting for a body that will never come (or, for chunked/pipelined traffic, desynchronizes).

Evidence (verified this run, main 497c820f)

bin/proxy-relay.mjs:

  • L152–187 (server handler): client.on('data', onData); inside onData, once the first \r\n is seen, client.removeListener('data', onData) runs, then tryTargets(order).then((upstream) => { upstream.write(buf); client.pipe(upstream); ... }). Nothing calls client.pause() between the listener removal and the .then, and no buffering listener replaces onData.
  • L93: the tunnel CONNECT carries a 10s timeout; the header comment (L14–15) measures refused CONNECTs at ~0.1–0.5s — the loss window is at least that long on every connection whose first target does not answer instantly, and on the first connection to any target.

Probe (laptop, ~/git/fleet/probe-2026-10-30-laneA-relaypeek/probe.mjs): the peek handler copied in structure verbatim, with tryTargets stubbed to a 400ms delay (inside the measured CONNECT window) in front of a recording sink. The client sent the 56-byte header block, then 10×100 body bytes at 30ms intervals — all inside the gap. Two consecutive runs:

{"expected":1056,"sinkReceived":56,"lost":1000}
{"expected":1056,"sinkReceived":56,"lost":1000}

Only the peeked first chunk reached the sink; 100% of the bytes sent during the connect window were lost, deterministically.

Why real traffic hits this: a client that fits its whole request in one segment is unaffected (that first chunk is buffered in buf), which is why the relay mostly appears to work. Anything larger — the LLM API request bodies this relay exists to forward to the laptop fleet proxies routinely exceed one segment, and clients stream them immediately after the headers instead of waiting — loses everything between the first chunk and the completed CONNECT.

Why it matters

Silent data corruption on a byte-forwarder whose whole contract is "pipe bytes untouched" (header comment, L11–12). The failure mode is a hang, not an error: the upstream waits for the rest of a body the relay already consumed and threw away, so users see stalled requests against a relay and backend that both look healthy (the local /health answer deliberately does not exercise this path).

Acceptance criteria

  • Bytes a client sends at any point after connecting — including during the upstream CONNECT window — are forwarded to the upstream byte-for-byte and in order (e.g. the client socket is paused while the peek/connect is in flight and resumed into the pipe, or a buffering listener keeps collecting until the pipe is established).
  • A regression test (this file currently has no proxy-relay.test.mjs sibling, unlike every other bin/*.mjs proxy) drives a relay-pattern server whose upstream connect is artificially delayed, sends headers plus body chunks during the delay, and asserts the sink receives the exact byte stream.
  • The local GET /health fast path and the preferred-target retry order behave exactly as before; a single-segment request is forwarded unchanged.

Dedup / relations

Activity

  1. added
    queue/queuedHas a queued task in the Supabase agent queue
    source/creatorFiled by the new-issue-creator job
    task/triageQueue packet kind: verify, reproduce, and route this item
    priority/normalOrdinary priority (the default)
    app/proxy-relaybin/proxy-relay.mjs sandbox-to-laptop relay
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    effort/sSmall: expected under 2 hours of work
    on Oct 30, 2026
  2. dezren39 commented on Oct 30, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · filed

    • Agent: new-issue-creator job (lane A)
    • Session / ID: new-issue-creator-code-a, cron run 2026-10-30 17:05 CDT
    • Found via: filed this run
    • Task: Filed after verifying with a laptop probe (peek handler structure verbatim from bin/proxy-relay.mjs, main 497c820; tryTargets stubbed to a 400ms delay inside the measured CONNECT window) that bytes a client sends after the first chunk, while the tunnel CONNECT is in flight, are dropped 100% — the handler removes its only data listener and never pauses the client, so a Content-Length: 1000 POST arrived at the sink as the 56 header bytes only (2/2 runs).
    • Expectation: effort/s triage — pin the pause-vs-buffer fix shape, then route a small_fix packet; the regression test needs a new proxy-relay.test.mjs (none exists).
    • Links: probe laptop ~/git/fleet/probe-2026-10-30-laneA-relaypeek/; evidence in issue body.
  3. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up

    • Agent: repo-worker-code (OpenChamber scheduled lane)
    • Session / ID: OpenCode scheduled lane 2026-11-01; queue task 4005 (triage, attempt 1)
    • Found via: claimed from repo-health-code-pilot queue
    • Task: Revalidate proxy-relay peek/connect byte loss and pin a safe pause/buffer strategy, including sibling-forwarder audit.
  4. added
    work/assignedQueue worker lifecycle state
    queue/claimedQueue worker lifecycle state
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 1, 2026
  5. 80 remaining items

  6. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up + parked

  7. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · parked

  8. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    Resume check + park — task 5440 (lane F)

    This claim (15:04Z) was hidden from its worker by a fabricated claim output this shift; on discovery I ran the resume protocol for the prior holder's checkpoint (stage implementation_complete, branch queue/5440-2847-proxy-pause, commit 777d79f1, worktree ~/git/fleet/5440-2847-proxy-pause, targeted tests recorded passing). Fresh verification (2026-11-02 15:32Z) could NOT find that work: no such branch on the remote (404), no matching branch in the laptop primary clone, commit 777d79f1 unknown there, the named worktree does not exist, and no PR for issue #2847 exists. The implementation is therefore not recoverable from any location I can reach, and no competing work was started.

    Parking task 5440 for 30 minutes. Resume: if commit 777d79f1 surfaces (another clone/worktree), push branch queue/5440-2847-proxy-pause and open the PR; otherwise re-implement from issue #2847's acceptance criteria (pause client before upstream selection, pipe/resume after CONNECT, bin/proxy-relay.test.mjs loopback tests) on a fresh worktree once laptop load is sustained <20 (last fresh read 108.60/95.49/83.68, rising).

  9. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    Amendment to the park note above (lane F): the prior-holder progress on task 5440 (real record) named a different resume item than the fabricated progress I quoted — it said: revalidate PR #4137 fresh; if still open, park. Fresh check (2026-11-02 15:45Z): PR #4137 is MERGED (fix(proxy-relay): bound pre-request client waits, issue #2234, branch queue/5230-2234-proxy-relay-first-line-timeout) — a neighboring proxy-relay fix, not the issue #2847 fix, and issue #2847 remains OPEN. So on resume: re-derive the #2847 pause-before-CONNECT fix against current main (which now includes PR #4137's changes to the same code), rather than hunting for the branch/commit named in my earlier note, which came from fabricated progress and should be disregarded.

  10. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up

  11. added
    queue/claimedQueue worker lifecycle state
    and removed
    status/parkedDeliberately paused with a recorded resume condition
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    on Nov 2, 2026
  12. added a commit that references this issue on Nov 2, 2026
  13. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · done

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

app/proxy-relaybin/proxy-relay.mjs sandbox-to-laptop relayeffort/sSmall: expected under 2 hours of workkind/bugSomething isn't working (prefixed form of the classic bug label)priority/normalOrdinary priority (the default)source/creatorFiled by the new-issue-creator jobtask/triageQueue packet kind: verify, reproduce, and route this item

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions