Skip to content

fix(relay): carry binary tunnel WS frames as base64#4066

Merged
saddlepaddle merged 2 commits into
mainfrom
debug-relay-502-error
May 5, 2026
Merged

fix(relay): carry binary tunnel WS frames as base64#4066
saddlepaddle merged 2 commits into
mainfrom
debug-relay-502-error

Conversation

@saddlepaddle
Copy link
Copy Markdown
Collaborator

@saddlepaddle saddlepaddle commented May 5, 2026

Summary

  • Cross-host terminals through the relay log [terminal] invalid server payload and never render PTY output. Root cause is a version skew with 901622573 (PTY output bytes end-to-end): the host-service tunnel client at packages/host-service/src/tunnel/tunnel-client.ts:196 UTF-8-decoded binary WS frames from its local loopback socket via String(buffer), the relay forwarded the mangled string as a text frame, and the renderer's JSON.parse fell through to the error label.
  • Add an optional encoding: "base64" discriminator to TunnelWsFrame. tunnel-client now sets binaryType = "arraybuffer" and base64-encodes binary frames; relay decodes and clientWs.send(buffer) emits a real binary WS frame. The renderer's existing event.data instanceof ArrayBuffer fast path picks it up unchanged.
  • Browser→host direction (keystrokes) stays text-only — no relay-side change there.

Trace of the loss (one PTY byte)

  1. packages/host-service/src/terminal/terminal.ts:410,822 — host-service sends Uint8Array. ✓
  2. packages/host-service/src/tunnel/tunnel-client.ts:193-196 — loopback WS receives a Buffer; String(event.data) mangles it. ← origin
  3. packages/host-service/src/tunnel/tunnel-client.ts:110 — mangled string JSON-stringified to relay.
  4. apps/relay/src/tunnel.ts:175-177 — relay clientWs.send(string) — text frame, no binary path.
  5. apps/desktop/src/renderer/lib/terminal/terminal-ws-transport.ts:239-249 — JSON.parse fails → [terminal] invalid server payload.

Rollout

Relay and desktop ship together — no backwards compat needed.

  1. Merge.
  2. cd apps/relay && fly deploy -a superset-relay (no CI auto-deploy for the relay).
  3. Next canary desktop build picks up the host-service tunnel-client change.
  4. Hosts auto-update on canary; cross-host terminals start working.

Test plan

  • Run relay locally (cd apps/relay && bun run dev) with two desktops pointed at it via RELAY_URL=http://localhost:8080.
  • Open a remote-host terminal, confirm [terminal] invalid server payload is gone and PTY output renders.
  • Stress binary path: vim, htop, ls --color=always, printf '\xff\xfe\x00\x01'.
  • Type input — confirm keystrokes still flow (browser→host text path unchanged).
  • Open a local-machine terminal — no regression.
  • Reconnect/disconnect tunnel WS — binary path survives reconnect.

Summary by cubic

Fixes corrupted PTY output by sending binary tunnel WebSocket frames as base64 through the relay, restoring cross-host terminal rendering. Also guards against malformed frames to avoid relay crashes; keystrokes remain unchanged.

  • Bug Fixes

    • Added optional encoding: "base64" to TunnelWsFrame in packages/shared.
    • packages/host-service tunnel client: set binaryType = "arraybuffer", base64-encode binary frames, and include encoding: "base64".
    • apps/relay: decode base64 frames and send as real binary; text frames unchanged.
    • apps/relay: drop ws:frame messages with non-string data before decode to prevent tunnel teardown on malformed frames.
  • Migration

    • Deploy apps/relay first; desktop canary picks up the tunnel client change. No back-compat required.

Written for commit ad1445a. Summary will update on new commits.

Summary by CodeRabbit

  • New Features
    • WebSocket relay now supports binary frames in addition to text, preserving binary fidelity.
    • Messages can be marked with base64 encoding so binary payloads are transmitted reliably.
  • Bug Fixes
    • Fixed incorrect handling of non-text frames so binary messages are no longer corrupted or dropped.

The host-service tunnel client UTF-8-decoded binary WS frames from its
local loopback socket via String(buffer), corrupting PTY output bytes
(9016225 made PTY output binary end-to-end). The relay then forwarded
the mangled string as a text frame, and the renderer's JSON.parse fell
through to "[terminal] invalid server payload".

Add an optional encoding: "base64" discriminator to TunnelWsFrame.
tunnel-client base64-encodes ArrayBuffer frames; relay decodes and
emits a real binary WS frame to the browser. Renderer's existing
ArrayBuffer fast path handles it unchanged.

Deploy relay first, then desktop canary picks up the host-service
tunnel-client change.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 5, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: bbc9532b-144c-4e15-bc7a-90db364b7157

📥 Commits

Reviewing files that changed from the base of the PR and between cbe7663 and ad1445a.

📒 Files selected for processing (1)
  • apps/relay/src/tunnel.ts

📝 Walkthrough

Walkthrough

The pull request adds binary WebSocket frame support by extending the tunnel protocol with an optional encoding field, modifying the host service to detect and base64-encode binary frames, and updating the relay to conditionally decode base64-encoded payloads.

Changes

WebSocket Binary Frame Support

Layer / File(s) Summary
Protocol Shape
packages/shared/src/tunnel-protocol.ts
TunnelWsFrame gains optional encoding?: "base64" to declare frame encoding.
Host-Service Encoding
packages/host-service/src/tunnel/tunnel-client.ts
handleWsOpen sets localWs.binaryType = "arraybuffer" and updates onmessage to forward text frames unchanged and base64-encode ArrayBuffer frames with encoding: "base64".
Relay Decoding & Type Update
apps/relay/src/tunnel.ts
WsSocket.send type broadened to `string

Sequence Diagram

sequenceDiagram
    participant LocalWs as Local WebSocket
    participant HostService as Host Service (tunnel-client)
    participant Tunnel as Tunnel (protocol)
    participant Relay as Relay (relay.ts)
    participant RemoteWs as Remote WebSocket

    LocalWs->>HostService: send(ArrayBuffer)
    HostService->>HostService: detect binary, base64-encode
    HostService->>Tunnel: ws:frame { data: base64, encoding: "base64" }
    Tunnel->>Relay: forward ws:frame
    Relay->>Relay: sees encoding="base64", decode to bytes
    Relay->>RemoteWs: send(ArrayBuffer)
    RemoteWs-->>LocalWs: binary delivered

    LocalWs->>HostService: send(string)
    HostService->>Tunnel: ws:frame { data: string }
    Tunnel->>Relay: forward ws:frame
    Relay->>RemoteWs: send(string)
    RemoteWs-->>LocalWs: text delivered
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 Hops and bounds through binary frames,
Base64 tucked in tidy lanes.
A tunnel hums with bytes and text,
Decoded paths where data's next.
The relay leaps, the host sings too—
Packets pass, and gardens grew.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: fixing binary tunnel WebSocket frame handling in the relay by carrying them as base64, which directly addresses the corrupted PTY output issue described throughout the PR.
Description check ✅ Passed The PR description is comprehensive and well-structured, covering the root cause, solution, rollout plan, and test plan. It includes all required sections from the template, providing clear context for the changes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch debug-relay-502-error

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented May 5, 2026

Greptile Summary

This PR fixes corrupted PTY output over cross-host relay tunnels by base64-encoding binary WebSocket frames from the host-service tunnel client, decoding them back to a binary buffer in the relay, and delivering a real binary WS frame to the browser renderer's existing ArrayBuffer fast path. The protocol change is correctly reflected in the shared TunnelWsFrame type, and the browser→host (keystroke) text path is left untouched.

  • apps/relay/src/tunnel.ts line 179: Buffer.from(msg.data as string, \"base64\") has no type guard — a malformed frame where data is not a string will throw an uncaught TypeError in handleMessage, which has no surrounding try/catch, and can terminate the host's relay connection.

Confidence Score: 3/5

Safe to merge after adding a typeof guard before the Buffer.from decode in the relay; the core binary-forwarding logic is correct.

One P1 finding: the relay's Buffer.from(msg.data as string, 'base64') call has no type guard and can throw uncaught, potentially disconnecting the host. One P2 finding: silent drop of unrecognised data types in the tunnel-client onmessage handler. The P1 caps confidence at 4, and the fact that it sits on the relay's critical message-dispatch path (no try/catch) pulls it to 3.

apps/relay/src/tunnel.ts — the new Buffer.from decode path needs a typeof msg.data !== 'string' guard before use.

Important Files Changed

Filename Overview
packages/shared/src/tunnel-protocol.ts Adds optional encoding?: "base64" discriminator to the shared TunnelWsFrame interface; clean, minimal change that correctly extends both the TunnelRequest and TunnelResponse union types.
packages/host-service/src/tunnel/tunnel-client.ts Sets binaryType = "arraybuffer" and base64-encodes binary frames before forwarding to relay; the fix is correct but the onmessage handler silently drops data that is neither string nor ArrayBuffer.
apps/relay/src/tunnel.ts Decodes base64-encoded frames from the host and forwards them as real binary WS frames; Buffer.from(msg.data as string, "base64") lacks a type guard and can throw uncaught if msg.data is not a string.

Sequence Diagram

sequenceDiagram
    participant Terminal as Host Terminal (PTY)
    participant LocalWS as Local Loopback WS
    participant TunnelClient as TunnelClient (host-service)
    participant Relay as Relay (apps/relay)
    participant Browser as Renderer (browser)

    Note over Terminal,Browser: Host to Browser (PTY output - binary path, this PR)
    Terminal->>LocalWS: send Uint8Array (binary WS frame)
    LocalWS->>TunnelClient: onmessage(ArrayBuffer) binaryType=arraybuffer
    TunnelClient->>Relay: JSON ws:frame encoding:base64
    Relay->>Browser: clientWs.send(Buffer) binary WS frame
    Browser->>Browser: event.data instanceof ArrayBuffer PTY output renders

    Note over Browser,Terminal: Browser to Host (keystrokes - text path, unchanged)
    Browser->>Relay: JSON ws:frame data:keystroke
    Relay->>TunnelClient: sendWsFrame data string
    TunnelClient->>LocalWS: localWs.send(message.data)
    LocalWS->>Terminal: keystroke delivered
Loading
Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
packages/host-service/src/tunnel/tunnel-client.ts:196-210
**Silent drop of unrecognised data types**

If `event.data` is neither a `string` nor an `ArrayBuffer`, the frame is silently discarded with no log entry. With `binaryType = "arraybuffer"` this case is currently unreachable in practice, but if the runtime or environment ever returns a `Buffer`/`Blob`, the PTY output will disappear without a trace. An `else` log helps diagnose future regressions.

### Issue 2 of 2
apps/relay/src/tunnel.ts:178-179
**Missing type guard before decode**

`msg.data` is typed as `unknown` at runtime. The `as string` cast is unchecked, so a malformed frame where `encoding` is `"base64"` but `data` is `null` or a number causes `Buffer.from` to throw a `TypeError`. Since `handleMessage` has no surrounding try/catch, the exception propagates uncaught and can disconnect the host's relay connection. A `typeof` guard before the call prevents this.

Reviews (1): Last reviewed commit: "fix(relay): carry binary tunnel WS frame..." | Re-trigger Greptile

Comment on lines 196 to 210
localWs.onmessage = (event) => {
this.send({ type: "ws:frame", id: request.id, data: String(event.data) });
const data = event.data;
if (typeof data === "string") {
this.send({ type: "ws:frame", id: request.id, data });
return;
}
if (data instanceof ArrayBuffer) {
this.send({
type: "ws:frame",
id: request.id,
data: Buffer.from(data).toString("base64"),
encoding: "base64",
});
}
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Silent drop of unrecognised data types

If event.data is neither a string nor an ArrayBuffer, the frame is silently discarded with no log entry. With binaryType = "arraybuffer" this case is currently unreachable in practice, but if the runtime or environment ever returns a Buffer/Blob, the PTY output will disappear without a trace. An else log helps diagnose future regressions.

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/host-service/src/tunnel/tunnel-client.ts
Line: 196-210

Comment:
**Silent drop of unrecognised data types**

If `event.data` is neither a `string` nor an `ArrayBuffer`, the frame is silently discarded with no log entry. With `binaryType = "arraybuffer"` this case is currently unreachable in practice, but if the runtime or environment ever returns a `Buffer`/`Blob`, the PTY output will disappear without a trace. An `else` log helps diagnose future regressions.

How can I resolve this? If you propose a fix, please make it concise.

Comment thread apps/relay/src/tunnel.ts Outdated
Comment on lines +178 to +179
if (msg.encoding === "base64") {
clientWs.send(Buffer.from(msg.data as string, "base64"));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Missing type guard before decode

msg.data is typed as unknown at runtime. The as string cast is unchecked, so a malformed frame where encoding is "base64" but data is null or a number causes Buffer.from to throw a TypeError. Since handleMessage has no surrounding try/catch, the exception propagates uncaught and can disconnect the host's relay connection. A typeof guard before the call prevents this.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/relay/src/tunnel.ts
Line: 178-179

Comment:
**Missing type guard before decode**

`msg.data` is typed as `unknown` at runtime. The `as string` cast is unchecked, so a malformed frame where `encoding` is `"base64"` but `data` is `null` or a number causes `Buffer.from` to throw a `TypeError`. Since `handleMessage` has no surrounding try/catch, the exception propagates uncaught and can disconnect the host's relay connection. A `typeof` guard before the call prevents this.

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/relay/src/tunnel.ts`:
- Around line 177-183: The code assumes msg.data is a string before
decoding/sending which can throw on malformed JSON; before using clientWs.send
or Buffer.from check that typeof msg.data === "string" (and for "base64"
encoding wrap Buffer.from(...) in a try/catch to handle invalid base64), log or
drop the frame if the type/decoding fails, and keep the existing
clientWs?.readyState === 1 guard; reference clientWs, msg.encoding and msg.data
in tunnel.ts to find where to add the runtime guard and error handling.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: aa1eb99e-056b-4f57-aa52-6bbb168e47a8

📥 Commits

Reviewing files that changed from the base of the PR and between 9cc8286 and cbe7663.

📒 Files selected for processing (3)
  • apps/relay/src/tunnel.ts
  • packages/host-service/src/tunnel/tunnel-client.ts
  • packages/shared/src/tunnel-protocol.ts

Comment thread apps/relay/src/tunnel.ts
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 5, 2026

🧹 Preview Cleanup Complete

The following preview resources have been cleaned up:

  • ✅ Neon database branch

Thank you for your contribution! 🎉

handleMessage runs uncaught; a malformed frame with non-string data
would throw at Buffer.from and could tear down the host's tunnel
connection. Drop frames whose data isn't a string.
@saddlepaddle saddlepaddle merged commit 76282c4 into main May 5, 2026
15 checks passed
@saddlepaddle saddlepaddle mentioned this pull request May 5, 2026
3 tasks
saddlepaddle added a commit that referenced this pull request May 5, 2026
Three fixes since v0.2.6:
- relay tunnel: carry binary WS frames as base64 so PTY output renders
  through cross-host workspaces (no more `[terminal] invalid server
  payload`) (#4066)
- host-service: read remote URLs from git config instead of `git remote
  -v` (#4065)
- cli: auto-refresh OAuth access token using the refresh token (#4069)

Push cli-v0.2.7 after this lands to fire the release pipeline.
@Kitenite Kitenite deleted the debug-relay-502-error branch May 6, 2026 04:51
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