Add chunked response support for large bodies

This commit is contained in:
Chris Wanstrath 2026-05-15 15:37:10 -07:00
parent eac9a8f990
commit 4f463acccf
2 changed files with 63 additions and 3 deletions

View File

@ -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<TunnelResponse> {
try {
const url = `${target}${req.path}`

View File

@ -17,6 +17,7 @@ type Response = {
headers: Record<string, string>,
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<string, Connection> = {}
const pending = new Map<string, { resolve: (res: Response) => void, subdomain: string }>
const pending = new Map<string, { resolve: (res: Response) => void, subdomain: string }>()
const chunked = new Map<string, { status: number, headers: Record<string, string>, 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,
}