-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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 all 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,8 @@ 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 { promisify } from 'node:util'; | ||
| import { gzip } from 'node:zlib'; | ||
|
|
||
| import { | ||
| createKimiDeviceId, | ||
|
|
@@ -59,6 +61,14 @@ 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', | ||
| ]); | ||
| const gzipAsync = promisify(gzip); | ||
|
|
||
| interface RelayMessage { | ||
| readonly type: string; | ||
|
|
@@ -217,6 +227,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> { | ||
|
|
@@ -816,24 +847,48 @@ function requestLocalHttp( | |
| response.on('data', (chunk: Buffer | string) => chunks.push(Buffer.from(chunk))); | ||
| response.once('error', reject); | ||
| response.once('end', () => { | ||
| const contentType = response.headers['content-type'] ?? ''; | ||
| const receivedBody = Buffer.concat(chunks); | ||
| const 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'); | ||
| headers.push('Content-Length', String(body.length)); | ||
| const statusCode = response.statusCode ?? 502; | ||
| const statusMessage = response.statusMessage ?? 'Bad Gateway'; | ||
| resolve( | ||
| Buffer.concat([ | ||
| void (async (): Promise<Buffer> => { | ||
| const contentType = response.headers['content-type'] ?? ''; | ||
| const receivedBody = Buffer.concat(chunks); | ||
| 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'); | ||
| const negotiated = | ||
| response.headers['content-encoding'] === undefined && | ||
| response.statusCode !== 206 && | ||
| body.length >= GZIP_MIN_BODY_BYTES && | ||
| isGzipCompressibleType(contentType); | ||
| if (negotiated) { | ||
| 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'); | ||
|
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.
The follow-up adds Useful? React with 👍 / 👎. |
||
| } | ||
| if (negotiated && acceptsGzipEncoding(parsed.headers)) { | ||
| body = await gzipAsync(body); | ||
| headers.push('Content-Encoding', 'gzip'); | ||
| for (let index = headers.length - 2; index >= 0; index -= 2) { | ||
| if (headers[index]!.toLowerCase() === 'etag') headers.splice(index, 2); | ||
| } | ||
| } | ||
| headers.push('Content-Length', String(body.length)); | ||
| const statusCode = response.statusCode ?? 502; | ||
| const statusMessage = response.statusMessage ?? 'Bad Gateway'; | ||
| return Buffer.concat([ | ||
| Buffer.from(`HTTP/1.1 ${statusCode} ${statusMessage}\r\n${headerLines(headers)}\r\n\r\n`), | ||
| body, | ||
| ]), | ||
| ); | ||
| ]); | ||
| })().then(resolve, reject); | ||
| }); | ||
| }, | ||
| ); | ||
|
|
||
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 a client resumes a gzip-capable textual attachment larger than 1 KB, the initial 200 response is gzipped while retaining
Accept-Ranges: bytes, but this guard forces the subsequent 206 response back to identity encoding. TheContent-Rangeemitted bypackages/kap-server/src/routes/files.ts:145-158andpackages/kap-server/src/routes/sessionMedia.ts:83-97therefore indexes different bytes from the representation being resumed, so a downloader following the advertised range support can append incompatible data or fail the resume. The currentresponse.statusCode !== 206guard is fresh evidence beyond the earlier issue: it prevents compressing only the partial response while leaving the full response range-advertised and compressed; strip range support from compressed 200 responses or keep encoding/range selection consistent.Useful? React with 👍 / 👎.