diff --git a/src/client.ts b/src/client.ts index 2b54875..3eba7b4 100644 --- a/src/client.ts +++ b/src/client.ts @@ -33,6 +33,7 @@ export type TunnelResponse = { } const BACKOFF = [1000, 2000, 4000, 8000, 16000, 30000] +const CHUNK_SIZE = 768 * 1024 // 768KB per chunk const isText = (contentType: string | null): boolean => { if (!contentType) return true @@ -90,7 +91,7 @@ export function connect(options: TunnelOptions): Tunnel { if (msg.id) { onRequest?.(msg as TunnelRequest) const res = await proxy(msg as TunnelRequest) - ws?.send(JSON.stringify(res)) + sendResponse(ws!, res) } } catch (err) { onError?.(err instanceof Error ? err : new Error(String(err))) @@ -108,6 +109,30 @@ export function connect(options: TunnelOptions): Tunnel { } } + function sendResponse(ws: WebSocket, res: TunnelResponse): void { + if (res.body.length <= CHUNK_SIZE) { + ws.send(JSON.stringify(res)) + return + } + + // Send header (no body) + ws.send(JSON.stringify({ + id: res.id, + status: res.status, + headers: res.headers, + isBinary: res.isBinary, + chunked: true, + })) + + // Send body in chunks + for (let i = 0; i < res.body.length; i += CHUNK_SIZE) { + ws.send(JSON.stringify({ id: res.id, c: res.body.slice(i, i + CHUNK_SIZE) })) + } + + // Send end marker + ws.send(JSON.stringify({ id: res.id, done: true })) + } + async function proxy(req: TunnelRequest): Promise { try { const url = `${target}${req.path}` diff --git a/src/server.tsx b/src/server.tsx index d5c05fb..a9ae3e4 100644 --- a/src/server.tsx +++ b/src/server.tsx @@ -17,6 +17,7 @@ type Response = { headers: Record, body: string isBinary?: boolean + chunked?: boolean } type Success = { @@ -29,7 +30,8 @@ const REQUEST_TIMEOUT = 30_000 type Connection = { app: string, ws: any } let connections: Record = {} -const pending = new Map void, subdomain: string }> +const pending = new Map void, subdomain: string }>() +const chunked = new Map, isBinary?: boolean, parts: string[] }>() const app = new Hono @@ -77,6 +79,34 @@ app.get("/tunnel", c => { }, async onMessage(event, _ws) { const msg = JSON.parse(event.data.toString()) + + // Chunked response: header + if (msg.chunked) { + chunked.set(msg.id, { status: msg.status, headers: msg.headers, isBinary: msg.isBinary, parts: [] }) + return + } + + // Chunked response: body chunk + if (msg.c !== undefined) { + chunked.get(msg.id)?.parts.push(msg.c) + return + } + + // Chunked response: end marker + if (msg.done) { + const chunk = chunked.get(msg.id) + if (chunk) { + chunked.delete(msg.id) + const entry = pending.get(msg.id) + if (entry) { + entry.resolve({ id: msg.id, status: chunk.status, headers: chunk.headers, body: chunk.parts.join(''), isBinary: chunk.isBinary }) + pending.delete(msg.id) + } + } + return + } + + // Non-chunked response (backward compatible) const entry = pending.get(msg.id) if (entry) { entry.resolve(msg) @@ -165,6 +195,11 @@ function randomName(): string { export default { port: process.env.PORT || 3100, - websocket, + websocket: { + ...websocket, + maxPayloadLength: 128 * 1024 * 1024, + backpressureLimit: 128 * 1024 * 1024, + idleTimeout: 120, + }, fetch: app.fetch, } \ No newline at end of file