[HTTP] Update static asset headers to include public and immutable (#180378)

## Summary

Adds the `public` and `immutable` ~header~ directives to static assets
registered in via the `core_app.ts` logic (se [mdn
docs](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control)
for more info). This will ensure that assets are stored in a share-safe
cache and that for the "fresh" duration (1 year) the browser won't reach
out to Kibana server for the asset again.

## Notes

* This does not include all static assets registered via
`registerStaticDir`
* We could consider updating [this
logic](29e3ccd626/packages/core/apps/core-apps-server-internal/src/core_app.ts (L274)),
but those assets are not cache bustable, so we'd need to selectively add
the header to the path prefixed with static asset prefix, can do in this
PR or follow up

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Jean-Louis Leysens 2024-04-10 17:15:07 +02:00 committed by GitHub
parent e537deb9b5
commit f26b368180
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 92 additions and 2 deletions

View file

@ -0,0 +1,87 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import mockFs from 'mock-fs';
import { kibanaResponseFactory } from '@kbn/core-http-router-server-internal';
import { createDynamicAssetHandler } from './dynamic_asset_response';
function getHandler(args?: Partial<Parameters<typeof createDynamicAssetHandler>[0]>) {
return createDynamicAssetHandler({
bundlesPath: '/test',
publicPath: '/public',
fileHashCache: {
get: jest.fn(),
set: jest.fn(),
del: jest.fn(),
},
isDist: true,
...args,
});
}
afterEach(() => {
mockFs.restore();
});
it('returns 403 if the path requested does not match bundle path', async () => {
const handler = getHandler();
const result = await handler(
{} as any,
{ params: { path: '/non-existent/abc.js' }, headers: { 'accept-encoding': '*' } } as any,
kibanaResponseFactory
);
expect(result.status).toBe(403);
});
it('returns 404 if the file does not exist', async () => {
const handler = getHandler();
mockFs({}); // no files
const filePath = '/test/abc.js';
const result = await handler(
{} as any,
{ params: { path: filePath }, headers: { 'accept-encoding': '*' } } as any,
kibanaResponseFactory
);
expect(result.status).toBe(404);
});
describe('headers', () => {
it('returns the expected headers', async () => {
const handler = getHandler();
const filePath = '/test/abc.js';
mockFs({
[filePath]: Buffer.from('test'),
});
const result = await handler(
{} as any,
{ params: { path: filePath }, headers: { 'accept-encoding': 'br' } } as any,
kibanaResponseFactory
);
expect(result.options.headers).toEqual({
'cache-control': 'public, max-age=31536000, immutable',
'content-type': 'application/javascript; charset=utf-8',
});
});
it('returns the expected headers when not in dist mode', async () => {
const handler = getHandler({ isDist: false });
const filePath = '/test/abc.js';
mockFs({
[filePath]: Buffer.from('test'),
});
const result = await handler(
{} as any,
{ params: { path: filePath }, headers: { 'accept-encoding': '*' } } as any,
kibanaResponseFactory
);
expect(result.options.headers).toEqual({
'cache-control': 'must-revalidate',
'content-type': 'application/javascript; charset=utf-8',
etag: expect.stringMatching(/^[a-f0-9]{40}-\/public/i),
});
});
});

View file

@ -75,7 +75,9 @@ export const createDynamicAssetHandler = ({
let headers: Record<string, string>;
if (isDist) {
headers = { 'cache-control': `max-age=${365 * DAY}` };
headers = {
'cache-control': `public, max-age=${365 * DAY}, immutable`,
};
} else {
const stat = await fstat(fd);
const hash = await getFileHash(fileHashCache, path, stat, fd);

View file

@ -33,6 +33,7 @@
"@kbn/core-ui-settings-server",
"@kbn/monaco",
"@kbn/core-http-server-internal",
"@kbn/core-http-router-server-internal",
],
"exclude": [
"target/**/*",

View file

@ -154,7 +154,7 @@ describe('bundle routes', () => {
.get(`/${buildHash}/bundles/plugin/foo/gzip_chunk.js`)
.expect(200);
expect(response.get('cache-control')).toEqual('max-age=31536000');
expect(response.get('cache-control')).toEqual('public, max-age=31536000, immutable');
expect(response.get('etag')).toBeUndefined();
});
});