├── .eslintrc ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── lib ├── index.js └── push.js ├── package.json └── test ├── .eslintrc ├── fontawesome-webfont.svg └── test.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "standard" 4 | ], 5 | "rules": { 6 | 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .DS_Store* 3 | *.log 4 | *.gz 5 | 6 | node_modules 7 | coverage 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | node_js: 2 | - "0.10" 3 | - "0.12" 4 | - 1 5 | - 2 6 | - 3 7 | - 4 8 | - 5 9 | - 6 10 | - 7 11 | sudo: false 12 | language: node_js 13 | script: 14 | - npm run lint 15 | - npm run test-travis 16 | after_script: "npm install coveralls@2 && cat ./coverage/lcov.info | coveralls" 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2014 Jonathan Ong me@jongleberry.com 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > [!CAUTION] 2 | > **This repository is archived and no longer actively maintained.** 3 | > 4 | > We are no longer accepting issues, feature requests, or pull requests. 5 | > For additional support or questions, please visit the [Express.js Discussions page](https://github.com/expressjs/express/discussions). 6 | 7 | 8 | # spdy-push 9 | 10 | [![NPM version][npm-image]][npm-url] 11 | [![Build status][travis-image]][travis-url] 12 | [![Test coverage][coveralls-image]][coveralls-url] 13 | [![Dependency Status][david-image]][david-url] 14 | [![License][license-image]][license-url] 15 | [![Downloads][downloads-image]][downloads-url] 16 | 17 | A SPDY Push helper to be used with [spdy](https://github.com/indutny/node-spdy). 18 | 19 | - Handles `close` events and file descriptor leaks 20 | - Automatically gzips 21 | - Automatically sets the `content-length` and `content-type` headers if it can 22 | - Supports pushing strings, buffers, streams, and files 23 | 24 | ## Example 25 | 26 | ```js 27 | var spdy = require('spdy-push'); 28 | 29 | require('spdy').createServer(require('spdy-keys'), function (req, res) { 30 | if (res.isSpdy) { 31 | spdy(res).push('/script.js', { 32 | filename: 'public/script.js', // resolves against CWD 33 | }).catch(function (err) { 34 | console.error(err.stack); // log any critical errors 35 | }) 36 | } 37 | 38 | res.statusCode = 204; 39 | res.end(); 40 | }) 41 | ``` 42 | 43 | ## API 44 | 45 | ### spdy(res).push([path], [options], [priority]).then( => ) 46 | 47 | - `path` is the path of the object being pushed. 48 | Can also be set as `options.path`. 49 | - `priority` is the priority between `0-7` of the push stream 50 | with `7`, the default, being the lowest priority. 51 | Can also be set as `options.priority`. 52 | - `options` are: 53 | - `headers` 54 | - `body` - a `String`, `Buffer`, or `Stream.Readable` body 55 | - `filename` - a path to a file. Resolves against CWD. 56 | 57 | Either `options.body` or `options.filename` must be set. 58 | 59 | You do not need to set the following headers: 60 | 61 | - `content-encoding` 62 | - `content-length` 63 | - `content-type` 64 | 65 | ### .then( => ) 66 | 67 | Waits until the acknowledge event. 68 | 69 | ### .send().then( => ) 70 | 71 | Waits until the entire stream has been flushed. 72 | 73 | [npm-image]: https://img.shields.io/npm/v/spdy-push.svg?style=flat-square 74 | [npm-url]: https://npmjs.org/package/spdy-push 75 | [github-tag]: http://img.shields.io/github/tag/jshttp/spdy-push.svg?style=flat-square 76 | [github-url]: https://github.com/jshttp/spdy-push/tags 77 | [travis-image]: https://img.shields.io/travis/jshttp/spdy-push.svg?style=flat-square 78 | [travis-url]: https://travis-ci.org/jshttp/spdy-push 79 | [coveralls-image]: https://img.shields.io/coveralls/jshttp/spdy-push.svg?style=flat-square 80 | [coveralls-url]: https://coveralls.io/r/jshttp/spdy-push?branch=master 81 | [david-image]: http://img.shields.io/david/jshttp/spdy-push.svg?style=flat-square 82 | [david-url]: https://david-dm.org/jshttp/spdy-push 83 | [license-image]: http://img.shields.io/npm/l/spdy-push.svg?style=flat-square 84 | [license-url]: LICENSE 85 | [downloads-image]: http://img.shields.io/npm/dm/spdy-push.svg?style=flat-square 86 | [downloads-url]: https://npmjs.org/package/spdy-push 87 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 2 | var Push = require('./push') 3 | 4 | module.exports = SPDY 5 | 6 | function SPDY (res) { 7 | if (!(this instanceof SPDY)) return new SPDY(res) 8 | 9 | this.res = res 10 | } 11 | 12 | SPDY.prototype.push = function (a, b, c) { 13 | return new Push(this.res, a, b, c) 14 | } 15 | -------------------------------------------------------------------------------- /lib/push.js: -------------------------------------------------------------------------------- 1 | 2 | var compressible = require('compressible') 3 | var debug = require('debug')('spdy-push') 4 | var basename = require('path').basename 5 | var resolve = require('path').resolve 6 | var Promise = require('any-promise') 7 | var mime = require('mime-types') 8 | var dethroy = require('destroy') 9 | var assert = require('assert') 10 | var zlib = require('mz/zlib') 11 | var bytes = require('bytes') 12 | var fs = require('fs') 13 | 14 | module.exports = Push 15 | 16 | function Push (res, path, options, priority) { 17 | for (var i = 1; i < arguments.length; i++) { 18 | var arg = arguments[i] 19 | switch (typeof arg) { 20 | case 'number': 21 | priority = arg 22 | break 23 | case 'string': 24 | path = arg 25 | break 26 | case 'object': 27 | options = arg 28 | if ('priority' in options) priority = options.priority 29 | if ('path' in options) path = options.path 30 | break 31 | } 32 | } 33 | 34 | this.res = res 35 | this.options = options = options || {} 36 | var headers = this.headers = options.headers || {} 37 | 38 | this.path = path 39 | assert(this.path, 'path must be defined') 40 | this.priority = priority == null 41 | ? 7 42 | : priority 43 | assert(this.priority >= 0 && this.priority <= 7, 'Priority must be between 0-7') 44 | 45 | if (typeof options.filter === 'function') { 46 | this.filter = options.filter 47 | } 48 | if (typeof options.threshold === 'string') { 49 | this.threshold = bytes(options.threshold) 50 | } else if (typeof options.threshold === 'number') { 51 | this.threshold = options.threshold 52 | } 53 | 54 | // set the body and content-length if possible 55 | if (Buffer.isBuffer(options.body)) { 56 | this.body = options.body 57 | this.bodyType = 'buffer' 58 | this.length = headers['content-length'] = options.body.length 59 | } else if (typeof options.body === 'string') { 60 | this.body = options.body 61 | this.bodyType = 'string' 62 | this.length = headers['content-length'] = Buffer.byteLength(options.body) 63 | } else if (options.body && typeof options.body.pipe === 'function') { 64 | this.body = options.body 65 | this.bodyType = 'stream' 66 | if (headers['content-length']) this.length = parseInt(headers['content-length'], 10) 67 | } else if (typeof options.filename === 'string') { 68 | this.filename = resolve(options.filename) 69 | if (headers['content-length']) this.length = parseInt(headers['content-length'], 10) 70 | } else { 71 | throw new Error('You must either set .body or .filename') 72 | } 73 | 74 | // set the content type 75 | this.type = headers['content-type'] 76 | if (!this.type) { 77 | var type = mime.contentType(basename(path)) 78 | if (type) this.type = headers['content-type'] = type 79 | } 80 | 81 | this._handleCompression() 82 | 83 | this._acknowledgeDeferred = this._acknowledge().catch(filterError) 84 | this._sendDeferred = this._send().catch(filterError) 85 | } 86 | 87 | // compression options 88 | Push.prototype.filter = compressible 89 | Push.prototype.threshold = 1024 90 | 91 | // other default options 92 | Push.prototype.priority = 7 // lowest priority be default 93 | 94 | /** 95 | * Make `push()` into a promise. 96 | */ 97 | 98 | Push.prototype.then = function (resolve, reject) { 99 | return this._acknowledgeDeferred.then(resolve, reject) 100 | } 101 | 102 | Push.prototype.catch = function (reject) { 103 | return this._acknowledgeDeferred.catch(reject) 104 | } 105 | 106 | Push.prototype.acknowledge = function () { 107 | return this._acknowledgeDeferred 108 | } 109 | 110 | Push.prototype.send = function () { 111 | return this._sendDeferred 112 | } 113 | 114 | Push.prototype._acknowledge = function () { 115 | var self = this 116 | var stream = 117 | this.stream = this.res.push(this.path, this.headers, this.priority) 118 | return new Promise(function (resolve, reject) { 119 | stream.on('acknowledge', acknowledge) 120 | stream.on('error', cleanup) 121 | stream.on('close', cleanup) 122 | 123 | function acknowledge () { 124 | cleanup() 125 | resolve() 126 | } 127 | 128 | function cleanup (err) { 129 | stream.removeListener('acknowledge', acknowledge) 130 | stream.removeListener('error', cleanup) 131 | stream.removeListener('close', cleanup) 132 | if (err) { 133 | if (self.bodyType === 'stream') dethroy(self.body) 134 | reject(err) 135 | } 136 | } 137 | }) 138 | } 139 | 140 | Push.prototype._send = function () { 141 | var self = this 142 | return this._acknowledgeDeferred.then(function () { 143 | var stream = self.stream 144 | // empty body and no filename 145 | if (!self.body && !self.filename) return stream.end() 146 | 147 | // send the string or buffer 148 | if (self.bodyType === 'string' || self.bodyType === 'buffer') { 149 | if (!self.compress) return stream.end(self.body) 150 | return zlib.gzip(self.body).then(function (body) { 151 | stream.end(body) 152 | }, function (err) { 153 | dethroy(stream) 154 | throw err 155 | }) 156 | } 157 | 158 | return new Promise(function (resolve, reject) { 159 | // send a stream 160 | var body = self.filename ? fs.createReadStream(self.filename) : self.body 161 | body.on('error', destroy) 162 | if (self.compress) { 163 | body.pipe(zlib.Gzip(self.compress)) 164 | .on('error', destroy) 165 | .pipe(stream) 166 | } else { 167 | body.pipe(stream) 168 | } 169 | 170 | stream.on('error', destroy) 171 | stream.on('close', destroy) 172 | stream.on('finish', destroy) 173 | 174 | function destroy (err) { 175 | dethroy(body) 176 | 177 | stream.removeListener('error', destroy) 178 | stream.removeListener('close', destroy) 179 | stream.removeListener('finish', destroy) 180 | 181 | if (err) reject(err) 182 | else resolve() 183 | 184 | stream.on('error', postDestroyError) 185 | } 186 | }) 187 | }) 188 | } 189 | 190 | Push.prototype._handleCompression = function () { 191 | var options = this.options 192 | // manually disabled compression 193 | if (options.compress === false) { 194 | this.compress = false 195 | return 196 | } 197 | var headers = this.headers 198 | // already compressed or something 199 | if (headers['content-encoding']) { 200 | this.compress = false 201 | return 202 | } 203 | // below threshold 204 | if (typeof this.length === 'number' && this.length < this.threshold) { 205 | this.compress = false 206 | return 207 | } 208 | this.compress = this.filter(this.type) 209 | if (!this.compress) return 210 | this.compress = typeof options.compress === 'object' 211 | ? options.compress 212 | : {} 213 | headers['content-encoding'] = 'gzip' 214 | delete headers['content-length'] 215 | } 216 | 217 | // we don't care about these errors 218 | // and we don't want to clog up `this.onerror` 219 | function filterError (err) { 220 | if (!err || !(err instanceof Error)) return 221 | if (err.code === 'RST_STREAM') { 222 | debug('got RST_STREAM %s', err.status) 223 | return 224 | } 225 | // WHY AM I GETTING THESE ERRORS? 226 | if (err.message === 'Write after end!') return 227 | throw err 228 | } 229 | 230 | // sometimes this happens and crashes the entire server 231 | function postDestroyError (err) { 232 | /* eslint no-console: 0 */ 233 | console.error(err.stack) 234 | } 235 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spdy-push", 3 | "description": "SPDY Push helper", 4 | "version": "1.0.2", 5 | "author": { 6 | "name": "Jonathan Ong", 7 | "email": "me@jongleberry.com", 8 | "url": "http://jongleberry.com", 9 | "twitter": "https://twitter.com/jongleberry" 10 | }, 11 | "license": "MIT", 12 | "repository": "jshttp/spdy-push", 13 | "dependencies": { 14 | "any-promise": "^1.1.0", 15 | "bytes": "^2.1.0", 16 | "compressible": "^2.0.0", 17 | "debug": "*", 18 | "destroy": "^1.0.3", 19 | "mime-types": "^2.0.0", 20 | "mz": "^2.0.0", 21 | "raw-body": "^2.1.3", 22 | "spdy": "^1.32.4" 23 | }, 24 | "devDependencies": { 25 | "babel-eslint": "^7.1.1", 26 | "bluebird": "^3.3.1", 27 | "eslint": "^3.12.2", 28 | "eslint-config-standard": "^6.2.1", 29 | "eslint-plugin-promise": "^3.4.0", 30 | "eslint-plugin-standard": "^2.0.1", 31 | "istanbul": "^0.4.2", 32 | "mocha": "^3.2.0", 33 | "raw-body": "^1.0.0", 34 | "spdy": "^1.0.0", 35 | "spdy-keys": "^0.0.0" 36 | }, 37 | "scripts": { 38 | "lint": "eslint . --fix", 39 | "test": "mocha", 40 | "test-cov": "istanbul cover node_modules/mocha/bin/_mocha -- --reporter dot", 41 | "test-travis": "istanbul cover node_modules/mocha/bin/_mocha --report lcovonly -- --reporter dot" 42 | }, 43 | "keywords": [ 44 | "http2", 45 | "http", 46 | "spdy", 47 | "push" 48 | ], 49 | "files": [ 50 | "lib" 51 | ], 52 | "main": "lib" 53 | } 54 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | }, 5 | "rules": { 6 | "max-nested-callbacks": 0, 7 | "no-console": 0 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | 2 | var spdy = require('spdy') 3 | var https = require('https') 4 | var zlib = require('mz/zlib') 5 | var assert = require('assert') 6 | var keys = require('spdy-keys') 7 | var join = require('path').join 8 | var Readable = require('stream').Readable 9 | var Promise = require('any-promise') 10 | 11 | var SPDY = require('..') 12 | 13 | var port 14 | var server 15 | var agent 16 | 17 | afterEach(function (done) { 18 | agent.close() 19 | server.close(done) 20 | }) 21 | 22 | describe('Streams', function () { 23 | describe('when empty', function () { 24 | it('should push', function () { 25 | return listen(function (req, res) { 26 | var stream = new Readable() 27 | stream._read = noop 28 | stream.push(null) 29 | 30 | return SPDY(res).push('/', { 31 | body: stream 32 | }) 33 | }).then(pull).then(function (res) { 34 | res.resume() 35 | return new Promise(function (resolve, reject) { 36 | res.on('end', resolve) 37 | res.on('error', reject) 38 | }) 39 | }) 40 | }) 41 | }) 42 | 43 | describe('when text', function () { 44 | it('should gzip', function () { 45 | return listen(function (req, res) { 46 | var stream = new Readable() 47 | stream._read = noop 48 | stream.push('klajsdlfjalsdkfjalsdjkfsjdf') 49 | stream.push(null) 50 | 51 | return SPDY(res).push('/', { 52 | headers: { 53 | 'content-type': 'text/plain' 54 | }, 55 | body: stream 56 | }) 57 | }).then(pull).then(function (res) { 58 | res.resume() 59 | assert.equal(res.headers['content-encoding'], 'gzip') 60 | assert(~res.headers['content-type'].indexOf('text/plain')) 61 | assert.equal(res.url, '/') 62 | }) 63 | }) 64 | }) 65 | 66 | describe('when image', function () { 67 | it('should not gzip', function () { 68 | return listen(function (req, res) { 69 | var stream = new Readable() 70 | stream._read = noop 71 | stream.push(null) 72 | 73 | return SPDY(res).push('/', { 74 | headers: { 75 | 'content-type': 'image/png' 76 | }, 77 | body: stream 78 | }) 79 | }).then(pull).then(function (res) { 80 | res.resume() 81 | assert(res.headers['content-encoding'] == null) 82 | assert.equal(res.headers['content-type'], 'image/png') 83 | }) 84 | }) 85 | }) 86 | 87 | describe('when svg', function () { 88 | it('should push', function () { 89 | return listen(function (req, res) { 90 | return SPDY(res).push('/fontawesome-webfont.svg', { 91 | filename: join(__dirname, 'fontawesome-webfont.svg') 92 | }) 93 | }).then(pull).then(function (res) { 94 | res.resume() 95 | assert.equal(res.headers['content-encoding'], 'gzip') 96 | assert(~res.headers['content-type'].indexOf('image/svg+xml')) 97 | }) 98 | }) 99 | }) 100 | }) 101 | 102 | describe('Strings', function () { 103 | describe('when no content-type is set', function () { 104 | it('should set the content-type if possible', function () { 105 | return listen(function (req, res) { 106 | return SPDY(res).push('/some.txt', { 107 | body: 'lol' 108 | }) 109 | }).then(pull).then(function (res) { 110 | assert(~res.headers['content-type'].indexOf('text/plain')) 111 | }) 112 | }) 113 | }) 114 | 115 | describe('when content-encoding is already set', function () { 116 | it('should not compress', function () { 117 | return listen(function (req, res) { 118 | return SPDY(res).push('/something.txt', { 119 | body: 'klajsldkfjaklsdjflkajdsflkajsdlkfjaklsdjf', 120 | threshold: 1, 121 | headers: { 122 | 'content-encoding': 'identity' 123 | } 124 | }) 125 | }).then(pull).then(function (res) { 126 | assert.equal(res.headers['content-encoding'], 'identity') 127 | assert(~res.headers['content-type'].indexOf('text/plain')) 128 | }) 129 | }) 130 | }) 131 | 132 | describe('when empty', function () { 133 | it('should push', function () { 134 | return listen(function (req, res) { 135 | return SPDY(res).push('/some.txt', { 136 | body: '' 137 | }) 138 | }).then(pull).then(function (res) { 139 | assert(~res.headers['content-type'].indexOf('text/plain')) 140 | }) 141 | }) 142 | }) 143 | }) 144 | 145 | describe('Buffers', function () { 146 | describe('when already compressed', function () { 147 | it('should not compress', function () { 148 | return zlib.gzip('lol').then(function (body) { 149 | listen(function (req, res) { 150 | return SPDY(res).push({ 151 | path: '/', 152 | threshold: 1, 153 | headers: { 154 | 'content-encoding': 'gzip', 155 | 'content-type': 'text/plain' 156 | }, 157 | body: body 158 | }) 159 | }) 160 | }).then(pull).then(function (res) { 161 | res.resume() 162 | assert.equal(res.headers['content-encoding'], 'gzip') 163 | assert(~res.headers['content-type'].indexOf('text/plain')) 164 | }) 165 | }) 166 | }) 167 | 168 | describe('when compressing', function () { 169 | it('should remove the content-length', function () { 170 | return listen(function (req, res) { 171 | return SPDY(res).push({ 172 | path: '/something.txt', 173 | body: new Buffer(2048), 174 | headers: { 175 | 'content-length': '2048' 176 | } 177 | }) 178 | }).then(pull).then(function (res) { 179 | assert.equal(res.headers['content-encoding'], 'gzip') 180 | assert(~res.headers['content-type'].indexOf('text/plain')) 181 | assert(res.headers['content-length'] == null) 182 | }) 183 | }) 184 | }) 185 | }) 186 | 187 | describe('Compression', function () { 188 | describe('Thresholds', function () { 189 | it('should not compress when below the threshold', function () { 190 | return listen(function (req, res) { 191 | return SPDY(res).push({ 192 | path: '/something.txt', 193 | body: 'lol' 194 | }) 195 | }).then(pull).then(function (res) { 196 | assert(!res.headers['content-encoding']) 197 | }) 198 | }) 199 | 200 | it('should compress when above the threshold', function () { 201 | return listen(function (req, res) { 202 | return SPDY(res).push({ 203 | path: '/something.txt', 204 | body: 'lol', 205 | threshold: 1 206 | }) 207 | }).then(pull).then(function (res) { 208 | assert.equal(res.headers['content-encoding'], 'gzip') 209 | }) 210 | }) 211 | }) 212 | 213 | describe('.compress', function () { 214 | it('should not compress when false', function () { 215 | return listen(function (req, res) { 216 | return SPDY(res).push({ 217 | path: '/something.txt', 218 | body: 'lol', 219 | threshold: 1, 220 | compress: false 221 | }) 222 | }).then(pull).then(function (res) { 223 | assert(!res.headers['content-encoding']) 224 | }) 225 | }) 226 | }) 227 | }) 228 | 229 | describe('Disconnections', function () { 230 | it('should not leak file descriptors', function (done) { 231 | var stream = new Readable() 232 | stream._read = noop 233 | stream.destroy = done 234 | 235 | return listen(function (req, res) { 236 | return SPDY(res).push('/', { 237 | body: stream 238 | }) 239 | }).then(pull).then(function (res) { 240 | res.destroy() 241 | }).catch(done) 242 | }) 243 | }) 244 | 245 | function listen (fn) { 246 | return new Promise(function (resolve, reject) { 247 | server = spdy.createServer(keys, function (req, res) { 248 | var defer 249 | try { 250 | defer = fn(req, res) 251 | } catch (err) { 252 | console.error(err.stack) 253 | res.statusCode = 500 254 | res.end() 255 | return 256 | } 257 | 258 | defer.then(function () { 259 | res.statusCode = 204 260 | res.end() 261 | }).catch(function (err) { 262 | console.error(err.stack) 263 | res.statusCode = 500 264 | res.end() 265 | }) 266 | }).listen(port, function (err) { 267 | if (err) return reject(err) 268 | port = this.address().port 269 | resolve() 270 | }) 271 | }) 272 | } 273 | 274 | function pull () { 275 | return new Promise(function (resolve, reject) { 276 | agent = spdy.createAgent({ 277 | port: port, 278 | rejectUnauthorized: false 279 | }) 280 | 281 | agent.once('error', reject) 282 | agent.once('push', resolve) 283 | 284 | https.request({ 285 | agent: agent, 286 | path: '/' 287 | }) 288 | .once('error', reject) 289 | .once('response', function (res) { 290 | if (res.statusCode !== 204) reject(new Error('got status code: ' + res.statusCode)) 291 | res.resume() 292 | }) 293 | .end() 294 | }) 295 | } 296 | 297 | function noop () {} 298 | --------------------------------------------------------------------------------