diff --git a/packages/sirv/index.mjs b/packages/sirv/index.mjs index 3ad14d4..3de8a40 100644 --- a/packages/sirv/index.mjs +++ b/packages/sirv/index.mjs @@ -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].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,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]]) return data; + if (data = cache[arr[i].name]) { + return { ...data, ...arr[i] }; + } } } @@ -39,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])); + 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(name, stats, isEtag); + headers = toHeaders(stats, isEtag); headers['Cache-Control'] = isEtag ? 'no-cache' : 'no-store'; - return { abs, stats, headers }; + return { abs, stats, headers, ...arr[i] }; } } } @@ -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) { - let enc = ENCODING[name.slice(-3)]; - - 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 }; @@ -164,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 = ['']; + let extns = [{ ext: '', encoded: 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, 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) } @@ -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/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.mjs b/tests/sirv.mjs index d2e23e2..e5aa3b3 100644 --- a/tests/sirv.mjs +++ b/tests/sirv.mjs @@ -859,6 +859,76 @@ gzip('should defer to brotli when "Accept-Encoding" allows both', async () => { } }); +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 { + await testGzipDirectRequest(server); + } finally { + server.close(); + } +}); + +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); + } 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(); // ---