Coveralls logob
Coveralls logo
  • Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

expressjs / compression / 2809352689

6 Aug 2022 - 17:45 coverage: 93.617% (-6.4%) from 100.0%
2809352689

Pull #183

github

GitHub
Merge 8d9fb6c16 into ad5113b98
Pull Request #183: fix: call underlying implementation properly

103 of 114 branches covered (90.35%)

35 of 44 new or added lines in 1 file covered. (79.55%)

132 of 141 relevant lines covered (93.62%)

18.11 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

92.16
/index.js
1
/*!
2
 * compression
3
 * Copyright(c) 2010 Sencha Inc.
4
 * Copyright(c) 2011 TJ Holowaychuk
5
 * Copyright(c) 2014 Jonathan Ong
6
 * Copyright(c) 2014-2015 Douglas Christopher Wilson
7
 * MIT Licensed
8
 */
9

10
'use strict'
11

12
/**
13
 * Module dependencies.
14
 * @private
15
 */
16

17
var accepts = require('accepts')
13×
18
var Buffer = require('safe-buffer').Buffer
13×
19
var bytes = require('bytes')
13×
20
var compressible = require('compressible')
13×
21
var debug = require('debug')('compression')
13×
22
var onHeaders = require('on-headers')
13×
23
var vary = require('vary')
13×
24
var zlib = require('zlib')
13×
25
var ServerResponse = require('http').ServerResponse
13×
26

27
/**
28
 * Module exports.
29
 */
30

31
module.exports = compression
13×
32
module.exports.filter = shouldCompress
13×
33

34
/**
35
 * Module variables.
36
 * @private
37
 */
38

39
var cacheControlNoTransformRegExp = /(?:^|,)\s*?no-transform\s*?(?:,|$)/
13×
40
var hasUint8Array = (typeof Uint8Array === 'function')
13×
41
function isUint8Array (arg) {
42
  return hasUint8Array && arg && (arg instanceof Uint8Array || arg.toString() === '[object Uint8Array]')
26×
43
}
44

45
/**
46
 * Compress response data with gzip / deflate.
47
 *
48
 * @param {Object} [options]
49
 * @return {Function} middleware
50
 * @public
51
 */
52

53
function compression (options) {
54
  var opts = options || {}
523×
55

56
  // options
57
  var filter = opts.filter || shouldCompress
523×
58
  var threshold = bytes.parse(opts.threshold)
523×
59

60
  if (threshold == null) {
523×
61
    threshold = 1024
39×
62
  }
63

64
  function noop () { }
65

66
  return function compression (req, res, next) {
523×
67
    var ended = false
523×
68
    var length
69
    var listeners = []
523×
70
    var stream
71

72
    var _end = res.end
523×
73
    var _on = res.on
523×
74
    var _write = res.write
523×
75

76
    // flush
77
    res.flush = function flush () {
523×
78
      if (stream) {
91×
79
        stream.flush()
78×
80
      }
81
    }
82

83
    // proxy
84

85
    res.write = function write (chunk, encoding, callback) {
523×
86
      if (chunk === null) {
344×
87
        // throw ERR_STREAM_NULL_VALUES
88
        return _write.call(this, chunk, encoding, callback)
13×
89
      } else if (typeof chunk === 'string' || typeof chunk.fill === 'function' || isUint8Array(chunk)) {
331×
90
        // noop
91
      } else {
92
        // throw ERR_INVALID_ARG_TYPE
93
        return _write.call(this, chunk, encoding, callback)
26×
94
      }
95

96
      if (!callback && typeof encoding === 'function') {
305×
97
        callback = encoding
28×
98
        encoding = undefined
28×
99
      }
100

101
      if (typeof callback !== 'function') {
305×
102
        callback = noop
277×
103
      }
104

105
      if (res.destroyed || res.finished || ended) {
305×
106
        // HACK: node doesn't expose internal errors,
107
        // we need to fake response to throw underlying errors type
108
        var fakeRes = new ServerResponse({})
28×
109
        fakeRes.on('error', function (err) {
28×
110
          res.emit('error', err)
27×
111
        })
112
        fakeRes.destroyed = res.destroyed
28×
113
        fakeRes.finished = res.finished || ended
28×
114
        // throw ERR_STREAM_DESTROYED or ERR_STREAM_WRITE_AFTER_END
115
        _write.call(fakeRes, chunk, encoding, callback)
28×
116
        return false
28×
117
      }
118

119
      if (!this._header) {
277×
120
        this._implicitHeader()
143×
121
      }
122

123
      if (chunk) {
Branches [[15, 1]] missed. 277×
124
        chunk = toBuffer(chunk, encoding)
277×
125
        if (/^v0\.8\./.test(process.version) && stream) {
Branches [[16, 0], [17, 1]] missed. 277×
NEW
126
          encoding = callback
!
127
        }
128
      }
129

130
      return stream
277×
131
        ? stream.write(chunk, encoding, callback)
132
        : _write.call(this, chunk, encoding, callback)
133
    }
134

135
    res.end = function end (chunk, encoding, callback) {
523×
136
      if (!callback) {
Branches [[19, 1]] missed. 523×
137
        if (typeof chunk === 'function') {
Branches [[20, 0]] missed. 523×
NEW
138
          callback = chunk
!
NEW
139
          chunk = encoding = undefined
!
140
        } else if (typeof encoding === 'function') {
Branches [[21, 0]] missed. 523×
NEW
141
          callback = encoding
!
NEW
142
          encoding = undefined
!
143
        }
144
      }
145

146
      if (typeof callback !== 'function') {
Branches [[22, 1]] missed. 523×
147
        callback = noop
523×
148
      }
149

150
      if (this.destroyed || this.finished || ended) {
523×
NEW
151
        this.finished = ended
1×
152
        // throw ERR_STREAM_WRITE_AFTER_END or ERR_STREAM_ALREADY_FINISHED
NEW
153
        return _end.call(this, chunk, encoding, callback)
1×
154
      }
155

156
      if (!this._header) {
522×
157
        // estimate the length
158
        if (!this.getHeader('Content-Length')) {
340×
159
          length = chunkLength(chunk, encoding)
314×
160
        }
161

162
        this._implicitHeader()
340×
163
      }
164

165
      if (!stream) {
522×
166
        return _end.call(this, chunk, encoding, callback)
195×
167
      }
168

169
      // mark ended
170
      ended = true
327×
171

172
      if (chunk) {
327×
173
        chunk = toBuffer(chunk, encoding)
274×
174
        if (/^v0\.8\./.test(process.version) && stream && chunk) {
Branches [[29, 0], [30, 1], [30, 2]] missed. 274×
NEW
175
          encoding = callback
!
176
        }
177
      }
178

179
      // write Buffer for Node.js 0.8
180
      return chunk
327×
181
        ? stream.end(chunk, encoding, callback)
182
        : stream.end(chunk, callback)
183
    }
184

185
    res.on = function on (type, listener) {
523×
186
      if (!listeners || type !== 'drain') {
314×
187
        return _on.call(this, type, listener)
275×
188
      }
189

190
      if (stream) {
39×
191
        return stream.on(type, listener)
13×
192
      }
193

194
      // buffer listeners for future stream
195
      listeners.push([type, listener])
26×
196

197
      return this
26×
198
    }
199

200
    function nocompress (msg) {
201
      debug('no compression: %s', msg)
195×
202
      addListeners(res, _on, listeners)
195×
203
      listeners = null
195×
204
    }
205

206
    onHeaders(res, function onResponseHeaders () {
523×
207
      // determine if request is filtered
208
      if (!filter(req, res)) {
523×
209
        nocompress('filtered')
39×
210
        return
39×
211
      }
212

213
      // determine if the entity should be transformed
214
      if (!shouldTransform(req, res)) {
484×
215
        nocompress('no transform')
26×
216
        return
26×
217
      }
218

219
      // vary
220
      vary(res, 'Accept-Encoding')
458×
221

222
      // content-length below threshold
223
      if (Number(res.getHeader('Content-Length')) < threshold || length < threshold) {
458×
224
        nocompress('size below threshold')
78×
225
        return
78×
226
      }
227

228
      var encoding = res.getHeader('Content-Encoding') || 'identity'
380×
229

230
      // already encoded
231
      if (encoding !== 'identity') {
380×
232
        nocompress('already encoded')
13×
233
        return
13×
234
      }
235

236
      // head
237
      if (req.method === 'HEAD') {
367×
238
        nocompress('HEAD request')
26×
239
        return
26×
240
      }
241

242
      // compression method
243
      var accept = accepts(req)
341×
244
      var method = accept.encoding(['gzip', 'deflate', 'identity'])
341×
245

246
      // we really don't prefer deflate
247
      if (method === 'deflate' && accept.encoding(['gzip'])) {
341×
248
        method = accept.encoding(['gzip', 'identity'])
13×
249
      }
250

251
      // negotiation failed
252
      if (!method || method === 'identity') {
341×
253
        nocompress('not acceptable')
13×
254
        return
13×
255
      }
256

257
      // compression stream
258
      debug('%s compression', method)
328×
259
      stream = method === 'gzip'
328×
260
        ? zlib.createGzip(opts)
261
        : zlib.createDeflate(opts)
262

263
      // add buffered listeners to stream
264
      addListeners(stream, stream.on, listeners)
328×
265

266
      // header fields
267
      res.setHeader('Content-Encoding', method)
328×
268
      res.removeHeader('Content-Length')
328×
269

270
      // compression
271
      stream.on('error', function (err) {
328×
NEW
272
        res.emit('error', err)
!
273
      })
274

275
      stream.on('data', function onStreamData (chunk) {
328×
276
        if (_write.call(res, chunk) === false) {
798×
277
          stream.pause()
90×
278
        }
279
      })
280

281
      stream.on('end', function onStreamEnd () {
328×
282
        _end.call(res)
315×
283
      })
284

285
      _on.call(res, 'drain', function onResponseDrain () {
328×
286
        stream.resume()
78×
287
      })
288
    })
289

290
    next()
523×
291
  }
292
}
293

294
/**
295
 * Add bufferred listeners to stream
296
 * @private
297
 */
298

299
function addListeners (stream, on, listeners) {
300
  for (var i = 0; i < listeners.length; i++) {
523×
301
    on.apply(stream, listeners[i])
26×
302
  }
303
}
304

305
/**
306
 * Get the length of a given chunk
307
 */
308

309
function chunkLength (chunk, encoding) {
310
  if (!chunk) {
314×
311
    return 0
52×
312
  }
313

314
  return !Buffer.isBuffer(chunk)
262×
315
    ? Buffer.byteLength(chunk, encoding)
316
    : chunk.length
317
}
318

319
/**
320
 * Default filter function.
321
 * @private
322
 */
323

324
function shouldCompress (req, res) {
325
  var type = res.getHeader('Content-Type')
549×
326

327
  if (type === undefined || !compressible(type)) {
549×
328
    debug('%s not compressible', type)
52×
329
    return false
52×
330
  }
331

332
  return true
497×
333
}
334

335
/**
336
 * Determine if the entity should be transformed.
337
 * @private
338
 */
339

340
function shouldTransform (req, res) {
341
  var cacheControl = res.getHeader('Cache-Control')
484×
342

343
  // Don't compress for Cache-Control: no-transform
344
  // https://tools.ietf.org/html/rfc7234#section-5.2.2.4
345
  return !cacheControl ||
484×
346
    !cacheControlNoTransformRegExp.test(cacheControl)
347
}
348

349
/**
350
 * Coerce arguments to Buffer
351
 * @private
352
 */
353

354
function toBuffer (chunk, encoding) {
355
  return !Buffer.isBuffer(chunk)
551×
356
    ? Buffer.from(chunk, encoding)
357
    : chunk
358
}
Troubleshooting · Open an Issue · Sales · Support · ENTERPRISE · CAREERS · STATUS
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2023 Coveralls, Inc