Merge pull request #916 from pikasTech/fix/issue912-cloud-web-static-compression

fix: gzip cloud web static assets
This commit is contained in:
Lyon
2026-06-05 15:25:53 +08:00
committed by GitHub
2 changed files with 102 additions and 4 deletions
+48 -3
View File
@@ -1,6 +1,8 @@
import { createServer } from "node:http";
import { readFile, stat } from "node:fs/promises";
import path from "node:path";
import { gzip } from "node:zlib";
import { promisify } from "node:util";
import { isCloudWebSseRoute, proxyCloudApiRequest } from "./cloud-web-proxy.mjs";
import { cloudWebProxyRoutePolicy } from "./cloud-web-routes.mjs";
@@ -11,6 +13,8 @@ const readOnlyRpcMethods = new Set([
"audit.event.query",
"evidence.record.query"
]);
const gzipAsync = promisify(gzip);
const GZIP_MIN_BYTES = 1024;
export async function serveCloudWeb(options) {
const server = createCloudWebServer(options);
@@ -62,11 +66,14 @@ export function createCloudWebServer({
const info = await stat(candidate);
if (!info.isFile()) continue;
const body = await readFile(candidate);
const type = contentType(candidate);
const encoded = await encodeStaticBody(request, body, type);
response.writeHead(200, {
"content-type": contentType(candidate),
"content-length": body.length
"content-type": type,
...encoded.headers,
"content-length": encoded.body.length
});
response.end(body);
response.end(encoded.body);
return;
} catch {
// Try the next root.
@@ -207,5 +214,43 @@ function contentType(filePath) {
if (filePath.endsWith(".html")) return "text/html; charset=utf-8";
if (filePath.endsWith(".css")) return "text/css; charset=utf-8";
if (filePath.endsWith(".mjs") || filePath.endsWith(".js")) return "text/javascript; charset=utf-8";
if (filePath.endsWith(".svg")) return "image/svg+xml; charset=utf-8";
if (filePath.endsWith(".md")) return "text/markdown; charset=utf-8";
if (filePath.endsWith(".json")) return "application/json; charset=utf-8";
return "application/octet-stream";
}
async function encodeStaticBody(request, body, type) {
if (!isGzipCandidate(type, body.length) || !acceptsEncoding(request.headers["accept-encoding"], "gzip")) {
return { body, headers: isCompressibleType(type) ? { "vary": "Accept-Encoding" } : {} };
}
const compressed = await gzipAsync(body, { level: 6 });
return {
body: compressed,
headers: {
"content-encoding": "gzip",
"vary": "Accept-Encoding"
}
};
}
function isGzipCandidate(type, sizeBytes) {
return sizeBytes >= GZIP_MIN_BYTES && isCompressibleType(type);
}
function isCompressibleType(type) {
return /^(?:text\/|application\/json\b)/u.test(String(type || ""));
}
function acceptsEncoding(header, encoding) {
const value = Array.isArray(header) ? header.join(",") : String(header || "");
for (const segment of value.split(",")) {
const [name, ...params] = segment.trim().split(";");
const token = name.trim().toLowerCase();
if (token !== encoding && token !== "*") continue;
const q = params.map((param) => param.trim().toLowerCase()).find((param) => param.startsWith("q="));
if (q && Number.parseFloat(q.slice(2)) <= 0) continue;
return true;
}
return false;
}
@@ -1,9 +1,10 @@
import assert from "node:assert/strict";
import { mkdtemp, rm, writeFile } from "node:fs/promises";
import { createServer } from "node:http";
import { createServer, request as httpRequest } from "node:http";
import os from "node:os";
import path from "node:path";
import test from "node:test";
import { gunzipSync } from "node:zlib";
import { createCloudWebServer } from "./cloud-web-runtime.mjs";
@@ -219,6 +220,46 @@ test("cloud web serves access deep link through the React shell", async () => {
}
});
test("cloud web serves compressible static assets with gzip", async () => {
const root = await mkdtemp(path.join(os.tmpdir(), "hwlab-cloud-web-runtime-"));
const appJs = `console.log(${JSON.stringify("fixture-static-asset ".repeat(512))});\n`;
await writeFile(path.join(root, "app.js"), appJs, "utf8");
const cloudWeb = createCloudWebServer({
serviceId: "hwlab-cloud-web",
roots: [root],
cloudApiBaseUrl: "http://127.0.0.1:1",
healthPayload: () => ({ status: "ok" }),
sendJson(response, statusCode, body) {
const payload = JSON.stringify(body);
response.writeHead(statusCode, {
"content-type": "application/json",
"content-length": Buffer.byteLength(payload)
});
response.end(payload);
}
});
await listen(cloudWeb);
try {
const compressed = await requestRaw(`${serverUrl(cloudWeb)}/app.js`, { "accept-encoding": "gzip" });
assert.equal(compressed.statusCode, 200);
assert.equal(compressed.headers["content-type"], "text/javascript; charset=utf-8");
assert.equal(compressed.headers["content-encoding"], "gzip");
assert.equal(compressed.headers.vary, "Accept-Encoding");
assert.equal(gunzipSync(compressed.body).toString("utf8"), appJs);
assert.ok(Number(compressed.headers["content-length"]) < Buffer.byteLength(appJs), "gzip payload should be smaller than raw app.js");
const identity = await requestRaw(`${serverUrl(cloudWeb)}/app.js`, { "accept-encoding": "identity" });
assert.equal(identity.statusCode, 200);
assert.equal(identity.headers["content-encoding"], undefined);
assert.equal(identity.headers.vary, "Accept-Encoding");
assert.equal(identity.body.toString("utf8"), appJs);
} finally {
await close(cloudWeb);
await rm(root, { recursive: true, force: true });
}
});
function listen(server) {
return new Promise((resolve, reject) => {
server.once("error", reject);
@@ -236,3 +277,15 @@ function serverUrl(server) {
const address = server.address();
return `http://127.0.0.1:${address.port}`;
}
function requestRaw(url, headers = {}) {
return new Promise((resolve, reject) => {
const request = httpRequest(url, { method: "GET", headers }, (response) => {
const chunks = [];
response.on("data", (chunk) => chunks.push(chunk));
response.on("end", () => resolve({ statusCode: response.statusCode, headers: response.headers, body: Buffer.concat(chunks) }));
});
request.once("error", reject);
request.end();
});
}