Skip to content

Commit

Permalink
Merge pull request #231 from jfhbrook/revert-230-revert-228-brotli-en…
Browse files Browse the repository at this point in the history
…coding

Fix and pull in brotli encoding (aka `Revert "Revert "Add support for brotli encoding""`)
  • Loading branch information
jfhbrook authored Sep 2, 2018
2 parents fe91caf + c09621f commit 871a3c4
Show file tree
Hide file tree
Showing 14 changed files with 264 additions and 13 deletions.
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ coverage
*.dat
*.out
*.pid
*.gz

pids
logs
Expand Down
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ const opts = {
cache: 'max-age=3600',
cors: false,
gzip: true,
brotli: false,
defaultExt: 'html',
handleError: true,
serverHeader: true,
Expand Down Expand Up @@ -209,6 +210,16 @@ that the behavior is appropriate. If `./public/some-file.js.gz` is not valid
gzip, this will fall back to `./public/some-file.js`. You can turn this off
with `opts.gzip === false`.

### `opts.brotli`
### `--brotli`

Serve `./public/some-file.js.br` in place of `./public/some-file.js` when the
[brotli encoded](https://github.com/google/brotli) version exists and ecstatic
determines that the behavior is appropriate. If the request does not contain
`br` in the HTTP `accept-encoding` header, ecstatic will instead attempt to
serve a gzipped version (if `opts.gzip` is `true`), or fall back to
`./public.some-file.js`. Defaults to **false**.

### `opts.serverHeader`
### `--no-server-header`

Expand Down
62 changes: 50 additions & 12 deletions lib/ecstatic.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ function decodePathname(pathname) {


// Check to see if we should try to compress a file with gzip.
function shouldCompress(req) {
function shouldCompressGzip(req) {
const headers = req.headers;

return headers && headers['accept-encoding'] &&
Expand All @@ -42,6 +42,16 @@ function shouldCompress(req) {
;
}

function shouldCompressBrotli(req) {
const headers = req.headers;

return headers && headers['accept-encoding'] &&
headers['accept-encoding']
.split(',')
.some(el => ['*', 'br'].indexOf(el.trim()) !== -1)
;
}

function hasGzipId12(gzipped, cb) {
const stream = fs.createReadStream(gzipped, { start: 0, end: 1 });
let buffer = Buffer('');
Expand Down Expand Up @@ -166,7 +176,8 @@ module.exports = function createMiddleware(_dir, _options) {
const parsed = url.parse(req.url);
let pathname = null;
let file = null;
let gzipped = null;
let gzippedFile = null;
let brotliFile = null;

// Strip any null bytes from the url
// This was at one point necessary because of an old bug in url.parse
Expand Down Expand Up @@ -198,7 +209,9 @@ module.exports = function createMiddleware(_dir, _options) {
path.relative(path.join('/', baseDir), pathname)
)
);
gzipped = `${file}.gz`;
// determine compressed forms if they were to exist
gzippedFile = `${file}.gz`;
brotliFile = `${file}.br`;

if (serverHeader !== false) {
// Set common headers.
Expand Down Expand Up @@ -229,7 +242,7 @@ module.exports = function createMiddleware(_dir, _options) {

function serve(stat) {
// Do a MIME lookup, fall back to octet-stream and handle gzip
// special case.
// and brotli special case.
const defaultType = opts.contentType || 'application/octet-stream';
let contentType = mime.lookup(file, defaultType);
let charSet;
Expand All @@ -238,19 +251,21 @@ module.exports = function createMiddleware(_dir, _options) {
const etag = generateEtag(stat, weakEtags);
let cacheControl = cache;
let stream = null;

if (contentType) {
charSet = mime.charsets.lookup(contentType, 'utf-8');
if (charSet) {
contentType += `; charset=${charSet}`;
}
}

if (file === gzipped) { // is .gz picked up
if (file === gzippedFile) { // is .gz picked up
res.setHeader('Content-Encoding', 'gzip');

// strip gz ending and lookup mime type
contentType = mime.lookup(path.basename(file, '.gz'), defaultType);
} else if (file === brotliFile) { // is .br picked up
res.setHeader('Content-Encoding', 'br');
// strip br ending and lookup mime type
contentType = mime.lookup(path.basename(file, '.br'), defaultType);
}

if (typeof cacheControl === 'function') {
Expand Down Expand Up @@ -401,13 +416,13 @@ module.exports = function createMiddleware(_dir, _options) {
});
}

// Look for a gzipped file if this is turned on
if (opts.gzip && shouldCompress(req)) {
fs.stat(gzipped, (err, stat) => {
// serve gzip file if exists and is valid
function tryServeWithGzip() {
fs.stat(gzippedFile, (err, stat) => {
if (!err && stat.isFile()) {
hasGzipId12(gzipped, (gzipErr, isGzip) => {
hasGzipId12(gzippedFile, (gzipErr, isGzip) => {
if (!gzipErr && isGzip) {
file = gzipped;
file = gzippedFile;
serve(stat);
} else {
statFile();
Expand All @@ -417,6 +432,29 @@ module.exports = function createMiddleware(_dir, _options) {
statFile();
}
});
}

// serve brotli file if exists, otherwise try gzip
function tryServeWithBrotli(shouldTryGzip) {
fs.stat(brotliFile, (err, stat) => {
if (!err && stat.isFile()) {
file = brotliFile;
serve(stat);
} else if (shouldTryGzip) {
tryServeWithGzip();
} else {
statFile();
}
});
}

const shouldTryBrotli = opts.brotli && shouldCompressBrotli(req);
const shouldTryGzip = opts.gzip && shouldCompressGzip(req);
// always try brotli first, next try gzip, finally serve without compression
if (shouldTryBrotli) {
tryServeWithBrotli(shouldTryGzip);
} else if (shouldTryGzip) {
tryServeWithGzip();
} else {
statFile();
}
Expand Down
1 change: 1 addition & 0 deletions lib/ecstatic/defaults.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"cache": "max-age=3600",
"cors": false,
"gzip": true,
"brotli": false,
"defaultExt": ".html",
"handleError": true,
"serverHeader": true,
Expand Down
6 changes: 6 additions & 0 deletions lib/ecstatic/opts.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ module.exports = (opts) => {
let si = defaults.si;
let cache = defaults.cache;
let gzip = defaults.gzip;
let brotli = defaults.brotli;
let defaultExt = defaults.defaultExt;
let handleError = defaults.handleError;
const headers = {};
Expand Down Expand Up @@ -105,6 +106,10 @@ module.exports = (opts) => {
gzip = opts.gzip;
}

if (typeof opts.brotli !== 'undefined' && opts.brotli !== null) {
brotli = opts.brotli;
}

aliases.handleError.some((k) => {
if (isDeclared(k)) {
handleError = opts[k];
Expand Down Expand Up @@ -195,6 +200,7 @@ module.exports = (opts) => {
defaultExt,
baseDir: (opts && opts.baseDir) || '/',
gzip,
brotli,
handleError,
headers,
serverHeader,
Expand Down
187 changes: 187 additions & 0 deletions test/compression.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
'use strict';

const test = require('tap').test;
const ecstatic = require('../lib/ecstatic');
const http = require('http');
const request = require('request');

const root = `${__dirname}/public`;

test('serves brotli-encoded file when available', (t) => {
t.plan(3);

const server = http.createServer(ecstatic({
root,
brotli: true,
autoIndex: true
}));

server.listen(() => {
const port = server.address().port;
const options = {
uri: `http://localhost:${port}/brotli`,
headers: {
'accept-encoding': 'gzip, deflate, br'
}
};

request.get(options, (err, res) => {
t.ifError(err);
t.equal(res.statusCode, 200);
t.equal(res.headers['content-encoding'], 'br');
});
});
t.once('end', () => {
server.close();
});
});

test('serves gzip-encoded file when brotli not available', (t) => {
t.plan(3);

const server = http.createServer(ecstatic({
root,
brotli: true,
gzip: true,
autoIndex: true
}));

server.listen(() => {
const port = server.address().port;
const options = {
uri: `http://localhost:${port}/gzip`,
headers: {
'accept-encoding': 'gzip, deflate, br'
}
};

request.get(options, (err, res) => {
t.ifError(err);
t.equal(res.statusCode, 200);
t.equal(res.headers['content-encoding'], 'gzip');
});
});
t.once('end', () => {
server.close();
});
});

test('serves gzip-encoded file when brotli not accepted', (t) => {
t.plan(3);

const server = http.createServer(ecstatic({
root,
brotli: true,
gzip: true,
autoIndex: true
}));

server.listen(() => {
const port = server.address().port;
const options = {
uri: `http://localhost:${port}/brotli`,
headers: {
'accept-encoding': 'gzip, deflate'
}
};

request.get(options, (err, res) => {
t.ifError(err);
t.equal(res.statusCode, 200);
t.equal(res.headers['content-encoding'], 'gzip');
});
});
t.once('end', () => {
server.close();
});
});

test('serves gzip-encoded file when brotli not enabled', (t) => {
t.plan(3);

const server = http.createServer(ecstatic({
root,
brotli: false,
gzip: true,
autoIndex: true
}));

server.listen(() => {
const port = server.address().port;
const options = {
uri: `http://localhost:${port}/brotli`,
headers: {
'accept-encoding': 'gzip, deflate, br'
}
};

request.get(options, (err, res) => {
t.ifError(err);
t.equal(res.statusCode, 200);
t.equal(res.headers['content-encoding'], 'gzip');
});
});
t.once('end', () => {
server.close();
});
});

test('serves unencoded file when compression not accepted', (t) => {
t.plan(3);

const server = http.createServer(ecstatic({
root,
brotli: true,
gzip: true,
autoIndex: true
}));

server.listen(() => {
const port = server.address().port;
const options = {
uri: `http://localhost:${port}/brotli`,
headers: {
'accept-encoding': ''
}
};

request.get(options, (err, res) => {
t.ifError(err);
t.equal(res.statusCode, 200);
t.equal(res.headers['content-encoding'], undefined);
});
});
t.once('end', () => {
server.close();
});
});

test('serves unencoded file when compression not enabled', (t) => {
t.plan(3);

const server = http.createServer(ecstatic({
root,
brotli: false,
gzip: false,
autoIndex: true
}));

server.listen(() => {
const port = server.address().port;
const options = {
uri: `http://localhost:${port}/brotli`,
headers: {
'accept-encoding': 'gzip, deflate, br'
}
};

request.get(options, (err, res) => {
t.ifError(err);
t.equal(res.statusCode, 200);
t.equal(res.headers['content-encoding'], undefined);
});
});
t.once('end', () => {
server.close();
});
});
1 change: 1 addition & 0 deletions test/public/brotli/fake_ecstatic
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ecstatic
Binary file added test/public/brotli/fake_ecstatic.br
Binary file not shown.
1 change: 1 addition & 0 deletions test/public/brotli/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
brotli, but I'm not compressed!!!
3 changes: 3 additions & 0 deletions test/public/brotli/index.html.br
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@

�brotli, compressed!!

Binary file added test/public/brotli/index.html.gz
Binary file not shown.
1 change: 1 addition & 0 deletions test/public/brotli/not_actually_brotli.br
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
You've been duped! This is not compressed!
1 change: 1 addition & 0 deletions test/public/brotli/real_ecstatic
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ecstatic
Loading

0 comments on commit 871a3c4

Please sign in to comment.