Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 15a9eba562 |
@@ -1,3 +1,8 @@
|
|||||||
|
import * as fs from 'node:fs';
|
||||||
|
import * as os from 'node:os';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
import Fastify, { FastifyInstance } from 'fastify';
|
||||||
|
import fastifyStatic from '@fastify/static';
|
||||||
import { resolveStaticAssetHeaders } from './static.module';
|
import { resolveStaticAssetHeaders } from './static.module';
|
||||||
|
|
||||||
// Unit tests for the static-asset cache classifier extracted from the
|
// Unit tests for the static-asset cache classifier extracted from the
|
||||||
@@ -33,3 +38,69 @@ describe('resolveStaticAssetHeaders', () => {
|
|||||||
expect(headers['vary']).toBe('Accept-Encoding');
|
expect(headers['vary']).toBe('Accept-Encoding');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Integration test proving the ACTUAL response header emitted by @fastify/static
|
||||||
|
// with the exact registration options StaticModule uses. This is the regression
|
||||||
|
// guard for #452: without `cacheControl: false`, @fastify/static writes its own
|
||||||
|
// `Cache-Control: public, max-age=0` AFTER the setHeaders callback, overwriting
|
||||||
|
// the immutable header — the /assets/ assertion below would then fail.
|
||||||
|
describe('static.module @fastify/static registration (integration)', () => {
|
||||||
|
let app: FastifyInstance;
|
||||||
|
let tmpDir: string;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
tmpDir = fs.mkdtempSync(join(os.tmpdir(), 'static-module-spec-'));
|
||||||
|
fs.mkdirSync(join(tmpDir, 'assets'), { recursive: true });
|
||||||
|
fs.mkdirSync(join(tmpDir, 'locales'), { recursive: true });
|
||||||
|
fs.writeFileSync(
|
||||||
|
join(tmpDir, 'assets', 'index-a1b2c3.js'),
|
||||||
|
'console.log(1);',
|
||||||
|
);
|
||||||
|
fs.writeFileSync(join(tmpDir, 'locales', 'en.json'), '{"hello":"world"}');
|
||||||
|
|
||||||
|
app = Fastify();
|
||||||
|
// Mirror StaticModule.onModuleInit's registration options exactly.
|
||||||
|
await app.register(fastifyStatic, {
|
||||||
|
root: tmpDir,
|
||||||
|
wildcard: false,
|
||||||
|
preCompressed: true,
|
||||||
|
cacheControl: false,
|
||||||
|
setHeaders: (res, filePath) => {
|
||||||
|
for (const [name, value] of Object.entries(
|
||||||
|
resolveStaticAssetHeaders(filePath),
|
||||||
|
)) {
|
||||||
|
res.setHeader(name, value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await app.ready();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await app.close();
|
||||||
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('serves a hashed /assets/ file with an immutable, 1-year cache-control', async () => {
|
||||||
|
const res = await app.inject({
|
||||||
|
method: 'GET',
|
||||||
|
url: '/assets/index-a1b2c3.js',
|
||||||
|
});
|
||||||
|
expect(res.statusCode).toBe(200);
|
||||||
|
const cacheControl = res.headers['cache-control'];
|
||||||
|
expect(cacheControl).toContain('immutable');
|
||||||
|
expect(cacheControl).toContain('max-age=31536000');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('serves a non-hashed /locales/ file WITHOUT an immutable cache-control', async () => {
|
||||||
|
const res = await app.inject({ method: 'GET', url: '/locales/en.json' });
|
||||||
|
expect(res.statusCode).toBe(200);
|
||||||
|
// resolveStaticAssetHeaders sets no cache-control here and cacheControl:false
|
||||||
|
// stops @fastify/static from adding one, so the browser revalidates by
|
||||||
|
// etag/last-modified — either an absent header or one without `immutable`.
|
||||||
|
const cacheControl = res.headers['cache-control'];
|
||||||
|
if (cacheControl !== undefined) {
|
||||||
|
expect(cacheControl).not.toContain('immutable');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -115,6 +115,11 @@ export class StaticModule implements OnModuleInit {
|
|||||||
// Serve the build-time .br/.gz neighbour when the client accepts it
|
// Serve the build-time .br/.gz neighbour when the client accepts it
|
||||||
// (see vite-plugin-compression2 in apps/client/vite.config.ts).
|
// (see vite-plugin-compression2 in apps/client/vite.config.ts).
|
||||||
preCompressed: true,
|
preCompressed: true,
|
||||||
|
// @fastify/static's default cacheControl:true writes its own
|
||||||
|
// Cache-Control (from maxAge, default 0) AFTER the setHeaders callback,
|
||||||
|
// silently overwriting the immutable header that resolveStaticAssetHeaders
|
||||||
|
// sets — disable it so setHeaders/resolveStaticAssetHeaders own the header.
|
||||||
|
cacheControl: false,
|
||||||
setHeaders: (res, filePath) => {
|
setHeaders: (res, filePath) => {
|
||||||
for (const [name, value] of Object.entries(
|
for (const [name, value] of Object.entries(
|
||||||
resolveStaticAssetHeaders(filePath),
|
resolveStaticAssetHeaders(filePath),
|
||||||
|
|||||||
Reference in New Issue
Block a user