Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/tunnel-response-gzip.md
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.
37 changes: 36 additions & 1 deletion packages/remote-control/src/remote-control.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -217,6 +225,24 @@ export function rewriteRemoteControlResponse(
return body;
}

function acceptsGzipEncoding(headers: readonly [string, string][]): boolean {
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='));
if (quality === undefined || Number(quality.slice(2)) > 0) return true;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Honor explicit gzip exclusions before wildcards

For a valid header such as Accept-Encoding: gzip;q=0, *;q=1, the explicit gzip entry excludes gzip and takes precedence over the wildcard. This loop skips the zero-quality entry and then returns true for *, causing the tunnel to send gzip to a client that explicitly cannot accept it. Resolve gzip's explicit quality first, and consult * only when gzip is absent.

Useful? React with 👍 / 👎.

}
}
return false;
}

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> {
Expand Down Expand Up @@ -818,13 +844,22 @@ 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 &&
body.length >= GZIP_MIN_BODY_BYTES &&
isGzipCompressibleType(contentType) &&
acceptsGzipEncoding(parsed.headers)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Skip gzip for byte-range responses

When a forwarded request contains Range and a text file yields a body of at least 1 KB, this condition gzips the identity slice while preserving the origin's Content-Range. Range is forwarded unchanged, and kap-server emits such 206 responses for filesystem downloads in packages/kap-server/src/routes/workspaceFs.ts:254-260 and packages/kap-server/src/routes/fs.ts:645-651; the offsets and total then describe the uncompressed representation rather than the transmitted gzip representation, so resumed or multipart downloads cannot be safely reassembled. Skip compression for 206/Content-Range responses, or negotiate the encoding before applying the range.

Useful? React with 👍 / 👎.

) {
body = gzipSync(body);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Move unbounded response compression off the event loop

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 gzipSync can 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 👍 / 👎.

headers.push('Content-Encoding', 'gzip');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Mark gzip responses as varying by Accept-Encoding

When a compressible response retains cacheable headers, adding Content-Encoding: gzip without adding or merging Vary: Accept-Encoding lets a browser or intermediary reuse that representation for a request which excludes gzip. This is reachable with the shipped server because packages/kap-server/src/routes/webAssets.ts:57-68 gives hashed assets a one-year immutable cache policy, while an SVG is compressible here but is not rewritten and therefore keeps that policy. Add Accept-Encoding to Vary whenever this branch performs compression.

Useful? React with 👍 / 👎.

}
headers.push('Content-Length', String(body.length));
const statusCode = response.statusCode ?? 502;
const statusMessage = response.statusMessage ?? 'Bad Gateway';
Expand Down
63 changes: 62 additions & 1 deletion packages/remote-control/test/remote-control.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { existsSync, mkdtempSync, rmSync } from 'node:fs';
import { mkdir, readFile, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { gunzipSync } from 'node:zlib';

import {
FileTokenStorage,
Expand Down Expand Up @@ -274,8 +275,20 @@ describe('Remote Control tunnel', () => {
let localHttpRequest: IncomingMessage | undefined;
let localWsRequest: IncomingMessage | undefined;
const localWsServer = new WebSocketServer({ noServer: true });
const assetJs = `const boot = "/assets/boot.js";\n${'const chunk = "/assets/chunk.js";\n'.repeat(120)}`;
const assetPng = Buffer.alloc(4096, 7);
const localServer = createServer((request, response) => {
localHttpRequest = request;
if (request.url === '/assets/index.js') {
response.writeHead(200, { 'Content-Type': 'text/javascript' });
response.end(assetJs);
return;
}
if (request.url === '/assets/logo.png') {
response.writeHead(200, { 'Content-Type': 'image/png' });
response.end(assetPng);
return;
}
response.writeHead(200, {
'Content-Type': 'text/html',
'Cache-Control': 'public, max-age=31536000, immutable',
Expand Down Expand Up @@ -352,7 +365,7 @@ describe('Remote Control tunnel', () => {
expect(handle.url).toContain('?rc=1&from=kimi_code_cli');

const rawRequest = Buffer.from(
'GET / HTTP/1.1\r\nHost: relay.test\r\nAuthorization: Bearer relay-token\r\nCookie: sid=1\r\nOrigin: https://relay.test\r\nConnection: X-Hop\r\nX-Hop: remove\r\nX-Keep: yes\r\n\r\n',
'GET / HTTP/1.1\r\nHost: relay.test\r\nAuthorization: Bearer relay-token\r\nCookie: sid=1\r\nOrigin: https://relay.test\r\nAccept-Encoding: gzip\r\nConnection: X-Hop\r\nX-Hop: remove\r\nX-Keep: yes\r\n\r\n',
);
const splitAt = Math.floor(rawRequest.length / 2);
httpConnections[0]!.send(
Expand Down Expand Up @@ -384,6 +397,8 @@ describe('Remote Control tunnel', () => {
expect(localHttpRequest?.headers['x-keep']).toBe('yes');
expect(response).not.toContain('X-Remove');
expect(response).not.toContain('immutable');
expect(response).not.toContain('Content-Encoding');
expect(localHttpRequest?.headers['accept-encoding']).toBeUndefined();
expect(response).toContain('Cache-Control: no-cache');
expect(response).toContain(`/coding-relay/devices/${handle.deviceId}/boot.js`);

Expand All @@ -400,6 +415,52 @@ describe('Remote Control tunnel', () => {
await rotatedResponsePromise;
await waitFor(() => localHttpRequest?.headers.authorization === 'Bearer rotated-server-token');

const gzipResponsePromise = nextJsonMessage(httpConnections[0]!);
httpConnections[0]!.send(
JSON.stringify({
request_id: 'request-3',
type: 'request',
is_last: true,
body_base64: Buffer.from(
'GET /assets/index.js HTTP/1.1\r\nHost: relay.test\r\nAccept-Encoding: br, gzip\r\n\r\n',
).toString('base64'),
}),
);
const gzipResponse = Buffer.from(
(await gzipResponsePromise)['body_base64'] as string,
'base64',
);
const gzipSeparator = gzipResponse.indexOf('\r\n\r\n');
const gzipHead = gzipResponse.subarray(0, gzipSeparator).toString('latin1');
const gzipBody = gzipResponse.subarray(gzipSeparator + 4);
expect(gzipHead).toContain('HTTP/1.1 200 OK');
expect(gzipHead).toContain('Content-Encoding: gzip');
expect(gzipHead).toContain(`Content-Length: ${gzipBody.length}`);
expect(gunzipSync(gzipBody).toString()).toBe(
assetJs.replaceAll('"/assets/', `"/coding-relay/devices/${handle.deviceId}/assets/`),
);

const binaryResponsePromise = nextJsonMessage(httpConnections[0]!);
httpConnections[0]!.send(
JSON.stringify({
request_id: 'request-4',
type: 'request',
is_last: true,
body_base64: Buffer.from(
'GET /assets/logo.png HTTP/1.1\r\nHost: relay.test\r\nAccept-Encoding: gzip\r\n\r\n',
).toString('base64'),
}),
);
const binaryResponse = Buffer.from(
(await binaryResponsePromise)['body_base64'] as string,
'base64',
);
const binarySeparator = binaryResponse.indexOf('\r\n\r\n');
expect(binaryResponse.subarray(0, binarySeparator).toString('latin1')).not.toContain(
'Content-Encoding',
);
expect(binaryResponse.subarray(binarySeparator + 4).equals(assetPng)).toBe(true);

managementConnections[0]!.send(
JSON.stringify({
type: 'open_ws',
Expand Down
Loading