@@ -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+
4555function 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 }
0 commit comments