Repository navigation
feat(remote-control): compress textual tunnel responses with gzip #3707
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 3 commits
53c99ab
8cff3d6
4a483c4
d1c57e6
fbdf79c
82b77dc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "@moonshot-ai/kimi-code": patch | ||
| --- | ||
|
|
||
| Compress Remote Control tunnel responses with gzip. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,6 +2,7 @@ import { hostname, platform } from 'node:os'; | |
| import { join } from 'node:path'; | ||
| import { request as httpRequest, validateHeaderName, validateHeaderValue } from 'node:http'; | ||
| import { setTimeout as sleep } from 'node:timers/promises'; | ||
| import { gzipSync } from 'node:zlib'; | ||
|
|
||
| import { | ||
| createKimiDeviceId, | ||
|
|
@@ -59,6 +60,13 @@ const BLOCKED_RESPONSE_HEADERS = new Set([ | |
| 'transfer-encoding', | ||
| 'upgrade', | ||
| ]); | ||
| const GZIP_MIN_BODY_BYTES = 1024; | ||
| const GZIP_COMPRESSIBLE_TYPES = new Set([ | ||
| 'application/javascript', | ||
| 'application/json', | ||
| 'application/xml', | ||
| 'image/svg+xml', | ||
| ]); | ||
|
|
||
| interface RelayMessage { | ||
| readonly type: string; | ||
|
|
@@ -217,6 +225,27 @@ export function rewriteRemoteControlResponse( | |
| return body; | ||
| } | ||
|
|
||
| function acceptsGzipEncoding(headers: readonly [string, string][]): boolean { | ||
| let wildcard = false; | ||
| for (const [name, value] of headers) { | ||
| if (name.toLowerCase() !== 'accept-encoding') continue; | ||
| for (const token of value.split(',')) { | ||
| const [encoding, ...params] = token.trim().toLowerCase().split(';'); | ||
| if (encoding !== 'gzip' && encoding !== '*') continue; | ||
| const quality = params.map((param) => param.trim()).find((param) => param.startsWith('q=')); | ||
| const acceptable = quality === undefined || Number(quality.slice(2)) > 0; | ||
| if (encoding === 'gzip') return acceptable; | ||
| wildcard = wildcard || acceptable; | ||
| } | ||
| } | ||
| return wildcard; | ||
| } | ||
|
|
||
| function isGzipCompressibleType(contentType: string): boolean { | ||
| const mime = contentType.split(';', 1)[0]!.trim().toLowerCase(); | ||
| return mime.startsWith('text/') || GZIP_COMPRESSIBLE_TYPES.has(mime); | ||
| } | ||
|
|
||
| export async function startRemoteControl( | ||
| options: RemoteControlOptions, | ||
| ): Promise<RemoteControlHandle> { | ||
|
|
@@ -818,13 +847,33 @@ function requestLocalHttp( | |
| response.once('end', () => { | ||
| const contentType = response.headers['content-type'] ?? ''; | ||
| const receivedBody = Buffer.concat(chunks); | ||
| const body = | ||
| let body = | ||
| response.headers['content-encoding'] === undefined | ||
| ? rewriteRemoteControlResponse(contentType, receivedBody, publicPrefix) | ||
| : receivedBody; | ||
| const rewritten = body !== receivedBody; | ||
| const headers = filterResponseHeaders(response.rawHeaders, rewritten); | ||
| if (rewritten) headers.push('Cache-Control', 'no-cache'); | ||
| if ( | ||
| response.headers['content-encoding'] === undefined && | ||
| response.statusCode !== 206 && | ||
| body.length >= GZIP_MIN_BODY_BYTES && | ||
| isGzipCompressibleType(contentType) && | ||
| acceptsGzipEncoding(parsed.headers) | ||
| ) { | ||
| body = gzipSync(body); | ||
| headers.push('Content-Encoding', 'gzip'); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a compressible response retains cacheable headers, adding Useful? React with 👍 / 👎. |
||
| let varyCovers = false; | ||
| for (let index = 0; index < headers.length; index += 2) { | ||
| if (headers[index]!.toLowerCase() !== 'vary') continue; | ||
| const tokens = headers[index + 1]! | ||
| .toLowerCase() | ||
| .split(',') | ||
| .map((token) => token.trim()); | ||
| if (tokens.includes('*') || tokens.includes('accept-encoding')) varyCovers = true; | ||
| } | ||
| if (!varyCovers) headers.push('Vary', 'Accept-Encoding'); | ||
| } | ||
| headers.push('Content-Length', String(body.length)); | ||
| const statusCode = response.statusCode ?? 502; | ||
| const statusMessage = response.statusMessage ?? 'Bad Gateway'; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When Remote Control returns a large textual workspace/file download, this synchronously compresses the entire response with no upper size limit; the filesystem routes stream arbitrarily large files, so
gzipSynccan block the shared Node event loop for seconds, stalling tunnel WebSocket traffic, heartbeat timers, and other kap-server requests. Use asynchronous/streaming gzip, or impose a bounded size above which compression is skipped.AGENTS.md reference: AGENTS.md:L29-L29
Useful? React with 👍 / 👎.