0.2.6-dev: brotli (quality 5) on the fly with the existing memory cache for clients that name br, gzip for the rest; separate ETags per encoding (measured against gzip-6: .skel −27 %, data JSON −26 %, vendor −14 %, scripts −5 %) — host bandwidth layer 1.4; golden: unchanged
This commit is contained in:
1 parent
35b09bb63b
commit
0bb16fd616
8 files changed
+173
-59
No files matched your search
+53
-11
@@ -11,7 +11,7 @@ import path from 'node:path';
|
||||
import { randomBytes } from 'node:crypto';
|
||||
import { setTimeout as delay } from 'node:timers/promises';
|
||||
|
||||
import { startServer, parseRange, acceptsGzip, parseTrustProxy } from '../server/index.js';
|
||||
import { startServer, parseRange, acceptsGzip, pickEncoding, parseTrustProxy } from '../server/index.js';
|
||||
import { loadData, lookup, getChess, getBond, getBand, getMode, getConfig, INDEXED_FILES } from '../server/data.js';
|
||||
import * as dataModule from '../server/data.js';
|
||||
import { CODE_ALPHABET, BOT_NAMES } from '../server/lobby.js';
|
||||
@@ -109,6 +109,8 @@ describe('static http server', () => {
|
||||
const js = `// app\n${'export const x = 1;\n'.repeat(200)}`;
|
||||
const mp3 = randomBytes(5000);
|
||||
const skel = Buffer.from('skel'.repeat(400));
|
||||
// over the 8 MB compression cache limit: compressed as a stream
|
||||
const big = `// big\n${Array.from({ length: 360000 }, (_, i) => `export const v${i} = ${i % 97};`).join('\n')}\n`;
|
||||
|
||||
before(async () => {
|
||||
tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'sp-static-'));
|
||||
@@ -127,6 +129,7 @@ describe('static http server', () => {
|
||||
w('public/assets/spine/x.skel', skel);
|
||||
w('public/assets/spine/x.atlas', 'x.png\nsize: 64,64\n'.repeat(40));
|
||||
w('public/assets/img/a.png', randomBytes(32));
|
||||
w('public/js/huge.js', big);
|
||||
w('public/assets/img/a.webp', randomBytes(32));
|
||||
w('public/fonts/f.woff2', randomBytes(32));
|
||||
w('public/fonts/f.ttf', randomBytes(32));
|
||||
@@ -164,17 +167,20 @@ describe('static http server', () => {
|
||||
assert.equal(redirect.headers.location, '/sub/?x=1');
|
||||
});
|
||||
|
||||
test('javascript: correct MIME, gzip when accepted, identity otherwise', async () => {
|
||||
const gz = await httpReq(srv.port, '/js/app.js', { headers: { 'accept-encoding': 'gzip, deflate, br' } });
|
||||
assert.equal(gz.status, 200);
|
||||
assert.equal(gz.headers['content-type'], 'text/javascript; charset=utf-8');
|
||||
assert.equal(gz.headers['content-encoding'], 'gzip');
|
||||
assert.equal(gz.headers.vary, 'Accept-Encoding');
|
||||
assert.equal(Number(gz.headers['content-length']), gz.body.length);
|
||||
test('javascript: correct MIME, brotli when named, gzip when accepted, identity otherwise', async () => {
|
||||
const br = await httpReq(srv.port, '/js/app.js', { headers: { 'accept-encoding': 'gzip, deflate, br' } });
|
||||
assert.equal(br.status, 200);
|
||||
assert.equal(br.headers['content-type'], 'text/javascript; charset=utf-8');
|
||||
assert.equal(br.headers['content-encoding'], 'br', 'a browser on an HTTPS page (or localhost) names br');
|
||||
assert.equal(br.headers.vary, 'Accept-Encoding');
|
||||
assert.equal(Number(br.headers['content-length']), br.body.length);
|
||||
assert.equal(zlib.brotliDecompressSync(br.body).toString(), js);
|
||||
const gz = await httpReq(srv.port, '/js/app.js', { headers: { 'accept-encoding': 'gzip, deflate' } });
|
||||
assert.equal(gz.headers['content-encoding'], 'gzip', 'a LAN http:// page: gzip');
|
||||
assert.equal(zlib.gunzipSync(gz.body).toString(), js);
|
||||
// cached second response is identical
|
||||
const gz2 = await httpReq(srv.port, '/js/app.js', { headers: { 'accept-encoding': 'gzip' } });
|
||||
assert.deepEqual(gz2.body, gz.body);
|
||||
// cached second responses are identical
|
||||
assert.deepEqual((await httpReq(srv.port, '/js/app.js', { headers: { 'accept-encoding': 'gzip' } })).body, gz.body);
|
||||
assert.deepEqual((await httpReq(srv.port, '/js/app.js', { headers: { 'accept-encoding': 'br' } })).body, br.body);
|
||||
|
||||
const plain = await httpReq(srv.port, '/js/app.js');
|
||||
assert.equal(plain.headers['content-encoding'], undefined);
|
||||
@@ -209,6 +215,27 @@ describe('static http server', () => {
|
||||
}
|
||||
const skelGz = await httpReq(srv.port, '/assets/spine/x.skel', { headers: { 'accept-encoding': 'gzip' } });
|
||||
assert.deepEqual(zlib.gunzipSync(skelGz.body), skel);
|
||||
const skelBr = await httpReq(srv.port, '/assets/spine/x.skel', { headers: { 'accept-encoding': 'gzip, br' } });
|
||||
assert.equal(skelBr.headers['content-encoding'], 'br');
|
||||
assert.deepEqual(zlib.brotliDecompressSync(skelBr.body), skel);
|
||||
// media is never compressed, whatever the client names
|
||||
assert.equal((await httpReq(srv.port, '/assets/audio/bgm.mp3', { headers: { 'accept-encoding': 'br, gzip' } })).headers['content-encoding'], undefined);
|
||||
assert.equal((await httpReq(srv.port, '/assets/img/a.png', { headers: { 'accept-encoding': 'br' } })).headers['content-encoding'], undefined);
|
||||
});
|
||||
|
||||
test('a file over the compression cache limit is compressed as a stream (brotli and gzip); HEAD sends no body', async () => {
|
||||
const br = await httpReq(srv.port, '/js/huge.js', { headers: { 'accept-encoding': 'br' } });
|
||||
assert.equal(br.headers['content-encoding'], 'br');
|
||||
assert.equal(br.headers['content-length'], undefined, 'streamed: no length up front');
|
||||
assert.equal(zlib.brotliDecompressSync(br.body).toString(), big);
|
||||
assert.ok(br.body.length < big.length / 4);
|
||||
const gz = await httpReq(srv.port, '/js/huge.js', { headers: { 'accept-encoding': 'gzip' } });
|
||||
assert.equal(gz.headers['content-encoding'], 'gzip');
|
||||
assert.equal(zlib.gunzipSync(gz.body).toString(), big);
|
||||
const head = await httpReq(srv.port, '/js/huge.js', { method: 'HEAD', headers: { 'accept-encoding': 'br' } });
|
||||
assert.equal(head.status, 200);
|
||||
assert.equal(head.body.length, 0);
|
||||
assert.equal(head.headers['content-encoding'], 'br');
|
||||
});
|
||||
|
||||
test('ETag / Last-Modified revalidation and Cache-Control policy', async () => {
|
||||
@@ -228,6 +255,10 @@ describe('static http server', () => {
|
||||
assert.equal(stale.status, 200);
|
||||
const gzEtag = (await httpReq(srv.port, '/js/app.js', { headers: { 'accept-encoding': 'gzip' } })).headers.etag;
|
||||
assert.notEqual(gzEtag, etag, 'gzip variant has its own ETag');
|
||||
const brEtag = (await httpReq(srv.port, '/js/app.js', { headers: { 'accept-encoding': 'br' } })).headers.etag;
|
||||
assert.equal(brEtag, etag.replace(/"$/, '-br"'), 'so has the brotli variant');
|
||||
assert.equal((await httpReq(srv.port, '/js/app.js', { headers: { 'accept-encoding': 'br', 'if-none-match': brEtag } })).status, 304);
|
||||
assert.equal((await httpReq(srv.port, '/js/app.js', { headers: { 'accept-encoding': 'gzip', 'if-none-match': brEtag } })).status, 200, 'not the gzip variant');
|
||||
|
||||
// unversioned art/fonts/vendor: cached for a day (files can be rewritten in place by the asset tools); the art and
|
||||
// fonts carry their content hash as ETag (server/http/assetIndex.js), so a rewrite with the same bytes is a 304
|
||||
@@ -396,6 +427,17 @@ describe('static http server', () => {
|
||||
assert.equal(acceptsGzip('*;q=0'), false);
|
||||
assert.equal(acceptsGzip('br'), false);
|
||||
assert.equal(acceptsGzip(undefined), false);
|
||||
// brotli when named at least as high as gzip; `*` alone never picks it
|
||||
assert.equal(pickEncoding('gzip, deflate, br, zstd'), 'br');
|
||||
assert.equal(pickEncoding('gzip, deflate'), 'gzip');
|
||||
assert.equal(pickEncoding('br;q=0, gzip'), 'gzip');
|
||||
assert.equal(pickEncoding('br;q=0.5, gzip'), 'gzip');
|
||||
assert.equal(pickEncoding('gzip;q=0.5, br'), 'br');
|
||||
assert.equal(pickEncoding('br'), 'br');
|
||||
assert.equal(pickEncoding('*'), 'gzip');
|
||||
assert.equal(pickEncoding('*;q=0'), null);
|
||||
assert.equal(pickEncoding('identity'), null);
|
||||
assert.equal(pickEncoding(undefined), null);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user