Add chunked response support for large bodies
This commit is contained in:
parent
eac9a8f990
commit
4f463acccf
|
|
@ -33,6 +33,7 @@ export type TunnelResponse = {
|
||||||
}
|
}
|
||||||
|
|
||||||
const BACKOFF = [1000, 2000, 4000, 8000, 16000, 30000]
|
const BACKOFF = [1000, 2000, 4000, 8000, 16000, 30000]
|
||||||
|
const CHUNK_SIZE = 768 * 1024 // 768KB per chunk
|
||||||
|
|
||||||
const isText = (contentType: string | null): boolean => {
|
const isText = (contentType: string | null): boolean => {
|
||||||
if (!contentType) return true
|
if (!contentType) return true
|
||||||
|
|
@ -90,7 +91,7 @@ export function connect(options: TunnelOptions): Tunnel {
|
||||||
if (msg.id) {
|
if (msg.id) {
|
||||||
onRequest?.(msg as TunnelRequest)
|
onRequest?.(msg as TunnelRequest)
|
||||||
const res = await proxy(msg as TunnelRequest)
|
const res = await proxy(msg as TunnelRequest)
|
||||||
ws?.send(JSON.stringify(res))
|
sendResponse(ws!, res)
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
onError?.(err instanceof Error ? err : new Error(String(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> {
|
async function proxy(req: TunnelRequest): Promise<TunnelResponse> {
|
||||||
try {
|
try {
|
||||||
const url = `${target}${req.path}`
|
const url = `${target}${req.path}`
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ type Response = {
|
||||||
headers: Record<string, string>,
|
headers: Record<string, string>,
|
||||||
body: string
|
body: string
|
||||||
isBinary?: boolean
|
isBinary?: boolean
|
||||||
|
chunked?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
type Success = {
|
type Success = {
|
||||||
|
|
@ -29,7 +30,8 @@ const REQUEST_TIMEOUT = 30_000
|
||||||
|
|
||||||
type Connection = { app: string, ws: any }
|
type Connection = { app: string, ws: any }
|
||||||
let connections: Record<string, Connection> = {}
|
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
|
const app = new Hono
|
||||||
|
|
||||||
|
|
@ -77,6 +79,34 @@ app.get("/tunnel", c => {
|
||||||
},
|
},
|
||||||
async onMessage(event, _ws) {
|
async onMessage(event, _ws) {
|
||||||
const msg = JSON.parse(event.data.toString())
|
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)
|
const entry = pending.get(msg.id)
|
||||||
if (entry) {
|
if (entry) {
|
||||||
entry.resolve(msg)
|
entry.resolve(msg)
|
||||||
|
|
@ -165,6 +195,11 @@ function randomName(): string {
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
port: process.env.PORT || 3100,
|
port: process.env.PORT || 3100,
|
||||||
websocket,
|
websocket: {
|
||||||
|
...websocket,
|
||||||
|
maxPayloadLength: 128 * 1024 * 1024,
|
||||||
|
backpressureLimit: 128 * 1024 * 1024,
|
||||||
|
idleTimeout: 120,
|
||||||
|
},
|
||||||
fetch: app.fetch,
|
fetch: app.fetch,
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue
Block a user