Skip to content

Commit 871a3c4

Browse files
authored
Merge pull request #231 from jfhbrook/revert-230-revert-228-brotli-encoding
Fix and pull in brotli encoding (aka `Revert "Revert "Add support for brotli encoding""`)
2 parents fe91caf + c09621f commit 871a3c4

File tree

14 files changed

+264
-13
lines changed

14 files changed

+264
-13
lines changed

.gitignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ coverage
1111
*.dat
1212
*.out
1313
*.pid
14-
*.gz
1514

1615
pids
1716
logs

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ const opts = {
9797
cache: 'max-age=3600',
9898
cors: false,
9999
gzip: true,
100+
brotli: false,
100101
defaultExt: 'html',
101102
handleError: true,
102103
serverHeader: true,
@@ -209,6 +210,16 @@ that the behavior is appropriate. If `./public/some-file.js.gz` is not valid
209210
gzip, this will fall back to `./public/some-file.js`. You can turn this off
210211
with `opts.gzip === false`.
211212

213+
### `opts.brotli`
214+
### `--brotli`
215+
216+
Serve `./public/some-file.js.br` in place of `./public/some-file.js` when the
217+
[brotli encoded](https://github.com/google/brotli) version exists and ecstatic
218+
determines that the behavior is appropriate. If the request does not contain
219+
`br` in the HTTP `accept-encoding` header, ecstatic will instead attempt to
220+
serve a gzipped version (if `opts.gzip` is `true`), or fall back to
221+
`./public.some-file.js`. Defaults to **false**.
222+
212223
### `opts.serverHeader`
213224
### `--no-server-header`
214225

lib/ecstatic.js

Lines changed: 50 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ function decodePathname(pathname) {
3232

3333

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

3838
return headers && headers['accept-encoding'] &&
@@ -42,6 +42,16 @@ function shouldCompress(req) {
4242
;
4343
}
4444

45+
function shouldCompressBrotli(req) {
46+
const headers = req.headers;
47+
48+
return headers && headers['accept-encoding'] &&
49+
headers['accept-encoding']
50+
.split(',')
51+
.some(el => ['*', 'br'].indexOf(el.trim()) !== -1)
52+
;
53+
}
54+
4555
function hasGzipId12(gzipped, cb) {
4656
const stream = fs.createReadStream(gzipped, { start: 0, end: 1 });
4757
let buffer = Buffer('');
@@ -166,7 +176,8 @@ module.exports = function createMiddleware(_dir, _options) {
166176
const parsed = url.parse(req.url);
167177
let pathname = null;
168178
let file = null;
169-
let gzipped = null;
179+
let gzippedFile = null;
180+
let brotliFile = null;
170181

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

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

230243
function serve(stat) {
231244
// Do a MIME lookup, fall back to octet-stream and handle gzip
232-
// special case.
245+
// and brotli special case.
233246
const defaultType = opts.contentType || 'application/octet-stream';
234247
let contentType = mime.lookup(file, defaultType);
235248
let charSet;
@@ -238,19 +251,21 @@ module.exports = function createMiddleware(_dir, _options) {
238251
const etag = generateEtag(stat, weakEtags);
239252
let cacheControl = cache;
240253
let stream = null;
241-
242254
if (contentType) {
243255
charSet = mime.charsets.lookup(contentType, 'utf-8');
244256
if (charSet) {
245257
contentType += `; charset=${charSet}`;
246258
}
247259
}
248260

249-
if (file === gzipped) { // is .gz picked up
261+
if (file === gzippedFile) { // is .gz picked up
250262
res.setHeader('Content-Encoding', 'gzip');
251-
252263
// strip gz ending and lookup mime type
253264
contentType = mime.lookup(path.basename(file, '.gz'), defaultType);
265+
} else if (file === brotliFile) { // is .br picked up
266+
res.setHeader('Content-Encoding', 'br');
267+
// strip br ending and lookup mime type
268+
contentType = mime.lookup(path.basename(file, '.br'), defaultType);
254269
}
255270

256271
if (typeof cacheControl === 'function') {
@@ -401,13 +416,13 @@ module.exports = function createMiddleware(_dir, _options) {
401416
});
402417
}
403418

404-
// Look for a gzipped file if this is turned on
405-
if (opts.gzip && shouldCompress(req)) {
406-
fs.stat(gzipped, (err, stat) => {
419+
// serve gzip file if exists and is valid
420+
function tryServeWithGzip() {
421+
fs.stat(gzippedFile, (err, stat) => {
407422
if (!err && stat.isFile()) {
408-
hasGzipId12(gzipped, (gzipErr, isGzip) => {
423+
hasGzipId12(gzippedFile, (gzipErr, isGzip) => {
409424
if (!gzipErr && isGzip) {
410-
file = gzipped;
425+
file = gzippedFile;
411426
serve(stat);
412427
} else {
413428
statFile();
@@ -417,6 +432,29 @@ module.exports = function createMiddleware(_dir, _options) {
417432
statFile();
418433
}
419434
});
435+
}
436+
437+
// serve brotli file if exists, otherwise try gzip
438+
function tryServeWithBrotli(shouldTryGzip) {
439+
fs.stat(brotliFile, (err, stat) => {
440+
if (!err && stat.isFile()) {
441+
file = brotliFile;
442+
serve(stat);
443+
} else if (shouldTryGzip) {
444+
tryServeWithGzip();
445+
} else {
446+
statFile();
447+
}
448+
});
449+
}
450+
451+
const shouldTryBrotli = opts.brotli && shouldCompressBrotli(req);
452+
const shouldTryGzip = opts.gzip && shouldCompressGzip(req);
453+
// always try brotli first, next try gzip, finally serve without compression
454+
if (shouldTryBrotli) {
455+
tryServeWithBrotli(shouldTryGzip);
456+
} else if (shouldTryGzip) {
457+
tryServeWithGzip();
420458
} else {
421459
statFile();
422460
}

lib/ecstatic/defaults.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"cache": "max-age=3600",
99
"cors": false,
1010
"gzip": true,
11+
"brotli": false,
1112
"defaultExt": ".html",
1213
"handleError": true,
1314
"serverHeader": true,

lib/ecstatic/opts.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ module.exports = (opts) => {
1414
let si = defaults.si;
1515
let cache = defaults.cache;
1616
let gzip = defaults.gzip;
17+
let brotli = defaults.brotli;
1718
let defaultExt = defaults.defaultExt;
1819
let handleError = defaults.handleError;
1920
const headers = {};
@@ -105,6 +106,10 @@ module.exports = (opts) => {
105106
gzip = opts.gzip;
106107
}
107108

109+
if (typeof opts.brotli !== 'undefined' && opts.brotli !== null) {
110+
brotli = opts.brotli;
111+
}
112+
108113
aliases.handleError.some((k) => {
109114
if (isDeclared(k)) {
110115
handleError = opts[k];
@@ -195,6 +200,7 @@ module.exports = (opts) => {
195200
defaultExt,
196201
baseDir: (opts && opts.baseDir) || '/',
197202
gzip,
203+
brotli,
198204
handleError,
199205
headers,
200206
serverHeader,

test/compression.js

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
'use strict';
2+
3+
const test = require('tap').test;
4+
const ecstatic = require('../lib/ecstatic');
5+
const http = require('http');
6+
const request = require('request');
7+
8+
const root = `${__dirname}/public`;
9+
10+
test('serves brotli-encoded file when available', (t) => {
11+
t.plan(3);
12+
13+
const server = http.createServer(ecstatic({
14+
root,
15+
brotli: true,
16+
autoIndex: true
17+
}));
18+
19+
server.listen(() => {
20+
const port = server.address().port;
21+
const options = {
22+
uri: `http://localhost:${port}/brotli`,
23+
headers: {
24+
'accept-encoding': 'gzip, deflate, br'
25+
}
26+
};
27+
28+
request.get(options, (err, res) => {
29+
t.ifError(err);
30+
t.equal(res.statusCode, 200);
31+
t.equal(res.headers['content-encoding'], 'br');
32+
});
33+
});
34+
t.once('end', () => {
35+
server.close();
36+
});
37+
});
38+
39+
test('serves gzip-encoded file when brotli not available', (t) => {
40+
t.plan(3);
41+
42+
const server = http.createServer(ecstatic({
43+
root,
44+
brotli: true,
45+
gzip: true,
46+
autoIndex: true
47+
}));
48+
49+
server.listen(() => {
50+
const port = server.address().port;
51+
const options = {
52+
uri: `http://localhost:${port}/gzip`,
53+
headers: {
54+
'accept-encoding': 'gzip, deflate, br'
55+
}
56+
};
57+
58+
request.get(options, (err, res) => {
59+
t.ifError(err);
60+
t.equal(res.statusCode, 200);
61+
t.equal(res.headers['content-encoding'], 'gzip');
62+
});
63+
});
64+
t.once('end', () => {
65+
server.close();
66+
});
67+
});
68+
69+
test('serves gzip-encoded file when brotli not accepted', (t) => {
70+
t.plan(3);
71+
72+
const server = http.createServer(ecstatic({
73+
root,
74+
brotli: true,
75+
gzip: true,
76+
autoIndex: true
77+
}));
78+
79+
server.listen(() => {
80+
const port = server.address().port;
81+
const options = {
82+
uri: `http://localhost:${port}/brotli`,
83+
headers: {
84+
'accept-encoding': 'gzip, deflate'
85+
}
86+
};
87+
88+
request.get(options, (err, res) => {
89+
t.ifError(err);
90+
t.equal(res.statusCode, 200);
91+
t.equal(res.headers['content-encoding'], 'gzip');
92+
});
93+
});
94+
t.once('end', () => {
95+
server.close();
96+
});
97+
});
98+
99+
test('serves gzip-encoded file when brotli not enabled', (t) => {
100+
t.plan(3);
101+
102+
const server = http.createServer(ecstatic({
103+
root,
104+
brotli: false,
105+
gzip: true,
106+
autoIndex: true
107+
}));
108+
109+
server.listen(() => {
110+
const port = server.address().port;
111+
const options = {
112+
uri: `http://localhost:${port}/brotli`,
113+
headers: {
114+
'accept-encoding': 'gzip, deflate, br'
115+
}
116+
};
117+
118+
request.get(options, (err, res) => {
119+
t.ifError(err);
120+
t.equal(res.statusCode, 200);
121+
t.equal(res.headers['content-encoding'], 'gzip');
122+
});
123+
});
124+
t.once('end', () => {
125+
server.close();
126+
});
127+
});
128+
129+
test('serves unencoded file when compression not accepted', (t) => {
130+
t.plan(3);
131+
132+
const server = http.createServer(ecstatic({
133+
root,
134+
brotli: true,
135+
gzip: true,
136+
autoIndex: true
137+
}));
138+
139+
server.listen(() => {
140+
const port = server.address().port;
141+
const options = {
142+
uri: `http://localhost:${port}/brotli`,
143+
headers: {
144+
'accept-encoding': ''
145+
}
146+
};
147+
148+
request.get(options, (err, res) => {
149+
t.ifError(err);
150+
t.equal(res.statusCode, 200);
151+
t.equal(res.headers['content-encoding'], undefined);
152+
});
153+
});
154+
t.once('end', () => {
155+
server.close();
156+
});
157+
});
158+
159+
test('serves unencoded file when compression not enabled', (t) => {
160+
t.plan(3);
161+
162+
const server = http.createServer(ecstatic({
163+
root,
164+
brotli: false,
165+
gzip: false,
166+
autoIndex: true
167+
}));
168+
169+
server.listen(() => {
170+
const port = server.address().port;
171+
const options = {
172+
uri: `http://localhost:${port}/brotli`,
173+
headers: {
174+
'accept-encoding': 'gzip, deflate, br'
175+
}
176+
};
177+
178+
request.get(options, (err, res) => {
179+
t.ifError(err);
180+
t.equal(res.statusCode, 200);
181+
t.equal(res.headers['content-encoding'], undefined);
182+
});
183+
});
184+
t.once('end', () => {
185+
server.close();
186+
});
187+
});

test/public/brotli/fake_ecstatic

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ecstatic

test/public/brotli/fake_ecstatic.br

11 Bytes
Binary file not shown.

test/public/brotli/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
brotli, but I'm not compressed!!!

test/public/brotli/index.html.br

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
2+
�brotli, compressed!!
3+


0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy