From 53909d91e3ca570d4b565ba1abd00f027ca78d6b Mon Sep 17 00:00:00 2001
From: weiwei Wang <54494072+weivwang@users.noreply.github.com>
Date: Fri, 14 Aug 2026 13:08:30 +0800
Subject: [PATCH] fix(kimi-web): cache content-hashed assets (#2865)
---
.changeset/cache-web-assets.md | 5 ++
packages/kap-server/src/routes/webAssets.ts | 12 ++++-
packages/kap-server/test/webAssets.test.ts | 53 +++++++++++++++++++++
3 files changed, 69 insertions(+), 1 deletion(-)
create mode 100644 .changeset/cache-web-assets.md
create mode 100644 packages/kap-server/test/webAssets.test.ts
diff --git a/.changeset/cache-web-assets.md b/.changeset/cache-web-assets.md
new file mode 100644
index 000000000..587caf6d1
--- /dev/null
+++ b/.changeset/cache-web-assets.md
@@ -0,0 +1,5 @@
+---
+"@moonshot-ai/kimi-code": patch
+---
+
+Cache content-hashed Kimi Web assets across reloads while keeping the app entry point revalidated.
diff --git a/packages/kap-server/src/routes/webAssets.ts b/packages/kap-server/src/routes/webAssets.ts
index 7b7ad0176..e45359317 100644
--- a/packages/kap-server/src/routes/webAssets.ts
+++ b/packages/kap-server/src/routes/webAssets.ts
@@ -1,6 +1,6 @@
import { createReadStream } from 'node:fs';
import { stat } from 'node:fs/promises';
-import { extname, join, normalize, resolve, sep } from 'node:path';
+import { extname, join, normalize, relative, resolve, sep } from 'node:path';
import type { FastifyReply, FastifyRequest } from 'fastify';
@@ -56,10 +56,20 @@ async function serveWebAsset(
return reply
.type(mimeType(filePath))
+ .header('Cache-Control', cacheControl(assetsDir, filePath))
.header('Content-Length', String(fileInfo.size))
.send(createReadStream(filePath));
}
+function cacheControl(assetsDir: string, filePath: string): string {
+ const assetPath = relative(assetsDir, filePath);
+ const fileName = filePath.slice(filePath.lastIndexOf(sep) + 1);
+ if (assetPath.startsWith(`assets${sep}`) && /[-.][A-Za-z0-9_-]{8}\.[^.]+$/.test(fileName)) {
+ return 'public, max-age=31536000, immutable';
+ }
+ return 'no-cache';
+}
+
async function resolveStaticFile(
assetsDir: string,
pathname: string,
diff --git a/packages/kap-server/test/webAssets.test.ts b/packages/kap-server/test/webAssets.test.ts
new file mode 100644
index 000000000..70dbb5995
--- /dev/null
+++ b/packages/kap-server/test/webAssets.test.ts
@@ -0,0 +1,53 @@
+import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
+import { tmpdir } from 'node:os';
+import { join } from 'node:path';
+
+import Fastify, { type FastifyInstance } from 'fastify';
+import { afterEach, beforeEach, describe, expect, it } from 'vitest';
+
+import { registerWebAssetRoutes } from '../src/routes/webAssets';
+
+describe('web asset cache policy', () => {
+ let app: FastifyInstance;
+ let assetsDir: string;
+
+ beforeEach(async () => {
+ assetsDir = await mkdtemp(join(tmpdir(), 'kimi-web-assets-'));
+ await mkdir(join(assetsDir, 'assets'));
+ await Promise.all([
+ writeFile(join(assetsDir, 'index.html'), 'Kimi'),
+ writeFile(join(assetsDir, 'assets', 'index-Dy7xs5tu.js'), 'export {};'),
+ writeFile(join(assetsDir, 'assets', 'application-configuration.json'), '{}'),
+ writeFile(join(assetsDir, 'favicon.svg'), ''),
+ ]);
+ app = Fastify();
+ await registerWebAssetRoutes(app, assetsDir);
+ });
+
+ afterEach(async () => {
+ await app.close();
+ await rm(assetsDir, { recursive: true, force: true });
+ });
+
+ it('caches content-hashed assets as immutable', async () => {
+ const response = await app.inject({ method: 'GET', url: '/assets/index-Dy7xs5tu.js' });
+
+ expect(response.statusCode).toBe(200);
+ expect(response.headers['cache-control']).toBe('public, max-age=31536000, immutable');
+ });
+
+ it.each([
+ '/index.html',
+ '/sessions/active',
+ '/favicon.svg',
+ '/assets/application-configuration.json',
+ ])(
+ 'requires revalidation for %s',
+ async (url) => {
+ const response = await app.inject({ method: 'GET', url });
+
+ expect(response.statusCode).toBe(200);
+ expect(response.headers['cache-control']).toBe('no-cache');
+ },
+ );
+});