Reject oversized messages without disconnecting the host (#6285)

* fix(client): reject a message over the daemon's size limit instead of reconnecting in a loop

The daemon closes the WebSocket with 1009 when a message exceeds its 100 MiB
limit. A workspace or agent creation that loses its connection mid-request
stays pending and, after reconnecting, finds no record of itself on the
daemon (the oversized frame was never read), so it resubmits the same frame
and the connection drops again every couple of seconds until the app reloads.

The client now checks each outbound frame against the same limit, which
protocol exports and the daemon sets explicitly, and fails that request with
a size error while the connection stays up.

Refs #5501

* test(client): cover the UTF-8 byte count at the message size limit

* fix(client): reject oversized relay messages without disconnecting

Check the encrypted wire size synchronously before submission, so a frame below the direct limit cannot close the relay and leave workspace creation resubmitting forever. Keep the relay limit shared with the existing checkout diff budget.

---------

Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
This commit is contained in:
paseo-bot[bot]
2026-10-14 08:20:40 +02:00
committed by GitHub
parent 019e5279e9
commit fc8f085b9e
10 changed files with 215 additions and 11 deletions
+3 -2
View File
@@ -1,3 +1,4 @@
import { MAX_RELAY_WEBSOCKET_MESSAGE_BYTES } from "@getpaseo/protocol/websocket-message-limit";
import { resolve, dirname, basename } from "path";
import { existsSync, realpathSync } from "fs";
import { open as openFile, readFile, stat as statFile } from "fs/promises";
@@ -2152,13 +2153,13 @@ export async function getCheckoutSnapshotFacts(
const PER_FILE_DIFF_MAX_BYTES = 1024 * 1024; // 1MB
const TOTAL_DIFF_MAX_BYTES = 2 * 1024 * 1024; // 2MB
const RELAY_MAX_FRAME_BYTES = 32 * 1024 * 1024;
const CHECKOUT_DIFF_FRAME_HEADROOM_BYTES = 1024 * 1024;
// Temporary until diffs load lazily per file. The Paseo relay's 32 MiB frame limit is
// binding: string frames are encrypted and base64-encoded. Reserve 1 MiB plaintext for
// the surrounding WebSocket JSON envelope after inverting that exact wire expansion.
export const CHECKOUT_DIFF_MAX_STRUCTURED_BYTES =
maxBase64EncryptedPlaintextByteLength(RELAY_MAX_FRAME_BYTES) - CHECKOUT_DIFF_FRAME_HEADROOM_BYTES;
maxBase64EncryptedPlaintextByteLength(MAX_RELAY_WEBSOCKET_MESSAGE_BYTES) -
CHECKOUT_DIFF_FRAME_HEADROOM_BYTES;
interface StructuredDiffAccumulator {
files: ParsedDiffFile[];