From 88ead497637e628ee916ed90f159b930b5e60fc0 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 12 Aug 2024 18:27:04 +0900 Subject: [PATCH 1/4] fix: should not set "Content-Encoding" for a direct request of a compressed file --- packages/sirv/index.js | 24 ++++++++++++------------ tests/sirv.js | 21 +++++++++++++++++++++ 2 files changed, 33 insertions(+), 12 deletions(-) diff --git a/packages/sirv/index.js b/packages/sirv/index.js index c93bbe6..32bb492 100644 --- a/packages/sirv/index.js +++ b/packages/sirv/index.js @@ -20,9 +20,9 @@ function toAssume(uri, extns) { let arr=[], tmp=`${uri}/index`; for (; i < extns.length; i++) { - x = extns[i] ? `.${extns[i]}` : ''; - if (uri) arr.push(uri + x); - arr.push(tmp + x); + x = extns[i][0] ? `.${extns[i][0]}` : ''; + if (uri) arr.push([uri + x, extns[i][1]]); + arr.push([tmp + x, extns[i][1]]); } return arr; @@ -31,7 +31,7 @@ function toAssume(uri, extns) { function viaCache(cache, uri, extns) { let i=0, data, arr=toAssume(uri, extns); for (; i < arr.length; i++) { - if (data = cache[arr[i]]) return data; + if (data = cache[arr[i][0]]) return data; } } @@ -39,11 +39,11 @@ function viaLocal(dir, isEtag, uri, extns) { let i=0, arr=toAssume(uri, extns); let abs, stats, name, headers; for (; i < arr.length; i++) { - abs = normalize(join(dir, name=arr[i])); + abs = normalize(join(dir, name=arr[i][0])); if (abs.startsWith(dir) && fs.existsSync(abs)) { stats = fs.statSync(abs); if (stats.isDirectory()) continue; - headers = toHeaders(name, stats, isEtag); + headers = toHeaders(name, stats, isEtag, arr[i][1]); headers['Cache-Control'] = isEtag ? 'no-cache' : 'no-store'; return { abs, stats, headers }; } @@ -97,8 +97,8 @@ const ENCODING = { '.gz': 'gzip', }; -function toHeaders(name, stats, isEtag) { - let enc = ENCODING[name.slice(-3)]; +function toHeaders(name, stats, isEtag, encoded = true) { + let enc = encoded ? ENCODING[name.slice(-3)] : undefined; let ctype = lookup(name.slice(0, enc && -3)) || ''; if (ctype === 'text/html') ctype += ';charset=utf-8'; @@ -164,12 +164,12 @@ export default function (dir, opts={}) { let lookup = opts.dev ? viaLocal.bind(0, dir, isEtag) : viaCache.bind(0, FILES); return function (req, res, next) { - let extns = ['']; + let extns = [['', false]]; let pathname = parse(req).pathname; let val = req.headers['accept-encoding'] || ''; - if (gzips && val.includes('gzip')) extns.unshift(...gzips); - if (brots && /(br|brotli)/i.test(val)) extns.unshift(...brots); - extns.push(...extensions); // [...br, ...gz, orig, ...exts] + if (gzips && val.includes('gzip')) extns.unshift(...gzips.map(ext => [ext, true])); + if (brots && /(br|brotli)/i.test(val)) extns.unshift(...brots.map(ext => [ext, true])); + extns.push(...extensions.map(ext => [ext, false])); // [...br, ...gz, orig, ...exts] if (pathname.indexOf('%') !== -1) { try { pathname = decodeURI(pathname) } diff --git a/tests/sirv.js b/tests/sirv.js index 8235497..45f886c 100644 --- a/tests/sirv.js +++ b/tests/sirv.js @@ -859,6 +859,27 @@ gzip('should defer to brotli when "Accept-Encoding" allows both', async () => { } }); +// TODO: handle also { dev: false } +gzip('should not set "Content-Encoding" for a direct request of a copressed file', async () => { + let server = utils.http({ dev: true }); + + try { + let res1 = await server.send('GET', '/data.js.gz', { headers: { 'Accept-Encoding': 'gzip' } }); + assert.is(res1.headers['content-type'], 'application/gzip'); + assert.is(res1.headers['content-encoding'], undefined); + assert.is(res1.data, 'gzip js file\n'); + assert.is(res1.statusCode, 200); + + let res2 = await server.send('GET', '/data.js.gz'); + assert.is(res2.headers['content-type'], 'application/gzip'); + assert.is(res2.headers['content-encoding'], undefined); + assert.is(res2.data, 'gzip js file\n'); + assert.is(res2.statusCode, 200); + } finally { + server.close(); + } +}); + gzip.run(); // --- From 9b45c0ee7fde6c8b394c2cfd03bcd1c9968b4906 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 12 Aug 2024 18:43:13 +0900 Subject: [PATCH 2/4] test: improve tests --- tests/public/test.csv | 1 + tests/public/test.csv.gz | 1 + tests/public/test.csv.gz.gz | 1 + tests/sirv.js | 78 ++++++++++++++++++++++++++++++------- 4 files changed, 67 insertions(+), 14 deletions(-) create mode 100644 tests/public/test.csv create mode 100644 tests/public/test.csv.gz create mode 100644 tests/public/test.csv.gz.gz diff --git a/tests/public/test.csv b/tests/public/test.csv new file mode 100644 index 0000000..6a1d713 --- /dev/null +++ b/tests/public/test.csv @@ -0,0 +1 @@ +test.csv diff --git a/tests/public/test.csv.gz b/tests/public/test.csv.gz new file mode 100644 index 0000000..ad079fe --- /dev/null +++ b/tests/public/test.csv.gz @@ -0,0 +1 @@ +test.csv.gz diff --git a/tests/public/test.csv.gz.gz b/tests/public/test.csv.gz.gz new file mode 100644 index 0000000..6a9783d --- /dev/null +++ b/tests/public/test.csv.gz.gz @@ -0,0 +1 @@ +test.csv.gz.gz diff --git a/tests/sirv.js b/tests/sirv.js index 45f886c..97148b1 100644 --- a/tests/sirv.js +++ b/tests/sirv.js @@ -859,27 +859,77 @@ gzip('should defer to brotli when "Accept-Encoding" allows both', async () => { } }); -// TODO: handle also { dev: false } -gzip('should not set "Content-Encoding" for a direct request of a copressed file', async () => { - let server = utils.http({ dev: true }); - +gzip('should not set "Content-Encoding" for a direct request of a copressed file (dev: true)', async () => { + let server = utils.http({ dev: true, gzip: true }); try { - let res1 = await server.send('GET', '/data.js.gz', { headers: { 'Accept-Encoding': 'gzip' } }); - assert.is(res1.headers['content-type'], 'application/gzip'); - assert.is(res1.headers['content-encoding'], undefined); - assert.is(res1.data, 'gzip js file\n'); - assert.is(res1.statusCode, 200); + await testGzipDirectRequest(server); + } finally { + server.close(); + } +}); - let res2 = await server.send('GET', '/data.js.gz'); - assert.is(res2.headers['content-type'], 'application/gzip'); - assert.is(res2.headers['content-encoding'], undefined); - assert.is(res2.data, 'gzip js file\n'); - assert.is(res2.statusCode, 200); +// TODO +gzip.skip('should not set "Content-Encoding" for a direct request of a copressed file (dev: false)', async () => { + let server = utils.http({ dev: false, gzip: true }); + try { + await testGzipDirectRequest(server); } finally { server.close(); } }); +async function testGzipDirectRequest(server) { + let headers = { 'Accept-Encoding': 'gzip' }; + + { + let res = await server.send('GET', '/test.csv.gz.gz', { headers }); + assert.is(res.headers['content-type'], 'application/gzip'); + assert.is(res.headers['content-encoding'], undefined); + assert.is(res.data, 'test.csv.gz.gz\n'); + assert.is(res.statusCode, 200); + } + + { + let res = await server.send('GET', '/test.csv.gz', { headers }); + assert.is(res.headers['content-type'], 'application/gzip'); + assert.is(res.headers['content-encoding'], 'gzip'); + assert.is(res.data, 'test.csv.gz.gz\n'); + assert.is(res.statusCode, 200); + } + + { + let res = await server.send('GET', '/test.csv', { headers }); + assert.is(res.headers['content-type'], 'text/csv'); + assert.is(res.headers['content-encoding'], 'gzip'); + assert.is(res.data, 'test.csv.gz\n'); + assert.is(res.statusCode, 200); + } + + { + let res = await server.send('GET', '/test.csv.gz.gz'); + assert.is(res.headers['content-type'], 'application/gzip'); + assert.is(res.headers['content-encoding'], undefined); + assert.is(res.data, 'test.csv.gz.gz\n'); + assert.is(res.statusCode, 200); + } + + { + let res = await server.send('GET', '/test.csv.gz'); + assert.is(res.headers['content-type'], 'application/gzip'); + assert.is(res.headers['content-encoding'], undefined); + assert.is(res.data, 'test.csv.gz\n'); + assert.is(res.statusCode, 200); + } + + { + let res = await server.send('GET', '/test.csv'); + assert.is(res.headers['content-type'], 'text/csv'); + assert.is(res.headers['content-encoding'], undefined); + assert.is(res.data, 'test.csv\n'); + assert.is(res.statusCode, 200); + } +} + gzip.run(); // --- From d8c088665a41dac7b112d30003c089710d7a3cdc Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 12 Aug 2024 18:57:17 +0900 Subject: [PATCH 3/4] fix: also fix `dev: false` --- packages/sirv/index.js | 30 ++++++++++++++++-------------- tests/sirv.js | 3 +-- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/packages/sirv/index.js b/packages/sirv/index.js index 32bb492..e30fce7 100644 --- a/packages/sirv/index.js +++ b/packages/sirv/index.js @@ -31,7 +31,9 @@ function toAssume(uri, extns) { function viaCache(cache, uri, extns) { let i=0, data, arr=toAssume(uri, extns); for (; i < arr.length; i++) { - if (data = cache[arr[i][0]]) return data; + if (data = cache[arr[i][0]]) { + return { ...data, name: arr[i][0], encoded: arr[i][1] }; + } } } @@ -43,9 +45,9 @@ function viaLocal(dir, isEtag, uri, extns) { if (abs.startsWith(dir) && fs.existsSync(abs)) { stats = fs.statSync(abs); if (stats.isDirectory()) continue; - headers = toHeaders(name, stats, isEtag, arr[i][1]); + headers = toHeaders(stats, isEtag); headers['Cache-Control'] = isEtag ? 'no-cache' : 'no-store'; - return { abs, stats, headers }; + return { abs, stats, headers, name, encoded: arr[i][1] }; } } } @@ -54,10 +56,17 @@ function is404(req, res) { return (res.statusCode=404,res.end()); } -function send(req, res, file, stats, headers) { +function send(req, res, file, stats, headers, name, encoded) { let code=200, tmp, opts={}; headers = { ...headers }; + let enc = encoded ? ENCODING[name.slice(-3)] : undefined; + if (enc) headers['Content-Encoding'] = enc; + + let ctype = lookup(name.slice(0, enc && -3)) || ''; + if (ctype === 'text/html') ctype += ';charset=utf-8'; + headers['Content-Type'] = ctype; + for (let key in headers) { tmp = res.getHeader(key); if (tmp) headers[key] = tmp; @@ -97,19 +106,12 @@ const ENCODING = { '.gz': 'gzip', }; -function toHeaders(name, stats, isEtag, encoded = true) { - let enc = encoded ? ENCODING[name.slice(-3)] : undefined; - - let ctype = lookup(name.slice(0, enc && -3)) || ''; - if (ctype === 'text/html') ctype += ';charset=utf-8'; - +function toHeaders(stats, isEtag) { let headers = { 'Content-Length': stats.size, - 'Content-Type': ctype, 'Last-Modified': stats.mtime.toUTCString(), }; - if (enc) headers['Content-Encoding'] = enc; if (isEtag) headers['ETag'] = `W/"${stats.size}-${stats.mtime.getTime()}"`; return headers; @@ -154,7 +156,7 @@ export default function (dir, opts={}) { if (/\.well-known[\\+\/]/.test(name)) {} // keep else if (!opts.dotfiles && /(^\.|[\\+|\/+]\.)/.test(name)) return; - let headers = toHeaders(name, stats, isEtag); + let headers = toHeaders(stats, isEtag); if (cc) headers['Cache-Control'] = cc; FILES['/' + name.normalize().replace(/\\+/g, '/')] = { abs, stats, headers }; @@ -189,6 +191,6 @@ export default function (dir, opts={}) { } setHeaders(res, pathname, data.stats); - send(req, res, data.abs, data.stats, data.headers); + send(req, res, data.abs, data.stats, data.headers, data.name, data.encoded); }; } diff --git a/tests/sirv.js b/tests/sirv.js index 97148b1..cfd9ce7 100644 --- a/tests/sirv.js +++ b/tests/sirv.js @@ -868,8 +868,7 @@ gzip('should not set "Content-Encoding" for a direct request of a copressed file } }); -// TODO -gzip.skip('should not set "Content-Encoding" for a direct request of a copressed file (dev: false)', async () => { +gzip('should not set "Content-Encoding" for a direct request of a copressed file (dev: false)', async () => { let server = utils.http({ dev: false, gzip: true }); try { await testGzipDirectRequest(server); From b21471d26e483f36ec058821fa8fff4603c71274 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Tue, 13 Aug 2024 08:45:08 +0900 Subject: [PATCH 4/4] refactor: array to object --- packages/sirv/index.js | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/sirv/index.js b/packages/sirv/index.js index e30fce7..fcffa05 100644 --- a/packages/sirv/index.js +++ b/packages/sirv/index.js @@ -20,9 +20,9 @@ function toAssume(uri, extns) { let arr=[], tmp=`${uri}/index`; for (; i < extns.length; i++) { - x = extns[i][0] ? `.${extns[i][0]}` : ''; - if (uri) arr.push([uri + x, extns[i][1]]); - arr.push([tmp + x, extns[i][1]]); + x = extns[i].ext ? `.${extns[i].ext}` : ''; + if (uri) arr.push({ name: uri + x, encoded: extns[i].encoded }); + arr.push({ name: tmp + x, encoded: extns[i].encoded }); } return arr; @@ -31,8 +31,8 @@ function toAssume(uri, extns) { function viaCache(cache, uri, extns) { let i=0, data, arr=toAssume(uri, extns); for (; i < arr.length; i++) { - if (data = cache[arr[i][0]]) { - return { ...data, name: arr[i][0], encoded: arr[i][1] }; + if (data = cache[arr[i].name]) { + return { ...data, ...arr[i] }; } } } @@ -41,13 +41,13 @@ function viaLocal(dir, isEtag, uri, extns) { let i=0, arr=toAssume(uri, extns); let abs, stats, name, headers; for (; i < arr.length; i++) { - abs = normalize(join(dir, name=arr[i][0])); + abs = normalize(join(dir, name=arr[i].name)); if (abs.startsWith(dir) && fs.existsSync(abs)) { stats = fs.statSync(abs); if (stats.isDirectory()) continue; headers = toHeaders(stats, isEtag); headers['Cache-Control'] = isEtag ? 'no-cache' : 'no-store'; - return { abs, stats, headers, name, encoded: arr[i][1] }; + return { abs, stats, headers, ...arr[i] }; } } } @@ -166,12 +166,12 @@ export default function (dir, opts={}) { let lookup = opts.dev ? viaLocal.bind(0, dir, isEtag) : viaCache.bind(0, FILES); return function (req, res, next) { - let extns = [['', false]]; + let extns = [{ ext: '', encoded: false }]; let pathname = parse(req).pathname; let val = req.headers['accept-encoding'] || ''; - if (gzips && val.includes('gzip')) extns.unshift(...gzips.map(ext => [ext, true])); - if (brots && /(br|brotli)/i.test(val)) extns.unshift(...brots.map(ext => [ext, true])); - extns.push(...extensions.map(ext => [ext, false])); // [...br, ...gz, orig, ...exts] + if (gzips && val.includes('gzip')) extns.unshift(...gzips.map(ext => ({ ext, encoded: true }))); + if (brots && /(br|brotli)/i.test(val)) extns.unshift(...brots.map(ext => ({ ext, encoded: true }))); + extns.push(...extensions.map(ext => ({ ext, encoded: false }))); // [...br, ...gz, orig, ...exts] if (pathname.indexOf('%') !== -1) { try { pathname = decodeURI(pathname) }