├── .gitignore ├── .travis.yml ├── lib └── pull-fetch.js ├── license ├── package-lock.json ├── package.json ├── readme.md └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | logs 2 | *.log 3 | npm-debug.log* 4 | yarn-debug.log* 5 | yarn-error.log* 6 | pids 7 | *.pid 8 | *.seed 9 | *.pid.lock 10 | lib-cov 11 | coverage 12 | .nyc_output 13 | .grunt 14 | bower_components 15 | .lock-wscript 16 | build/Release 17 | node_modules/ 18 | jspm_packages/ 19 | typings/ 20 | .npm 21 | .eslintcache 22 | .node_repl_history 23 | *.tgz 24 | .yarn-integrity 25 | .env 26 | # (produces errors) 27 | # package-lock.json -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - 'node' 5 | - '6' 6 | - '5' 7 | - '4' 8 | -------------------------------------------------------------------------------- /lib/pull-fetch.js: -------------------------------------------------------------------------------- 1 | 2 | const { request: requestHttp } = require('http') 3 | const { request: requestHttps } = require('https') 4 | const { parse } = require('url') 5 | 6 | function fetch (options) { 7 | if (typeof options === 'string') options = parse(options) 8 | const protocol = options.protocol 9 | const request = protocol === 'https:' ? requestHttps : requestHttp 10 | 11 | const req = request(options) 12 | let sinkCb = null 13 | let ended = false 14 | const buffer = [] 15 | 16 | req.on('response', res => { 17 | res.on('data', chunk => { 18 | if(sinkCb){ // if we are waiting, provide the chunk immediately 19 | sinkCb(null, chunk) 20 | } else { // otherwise store it in buffer ('data' event can be emitted faster then sink reads from source) 21 | buffer.push(chunk) 22 | } 23 | }) 24 | res.on('end', () => { 25 | if(ended) return 26 | if(sinkCb){ 27 | sinkCb(ended = true) 28 | } 29 | }) 30 | res.on('error', err => { 31 | if(ended) return 32 | if(sinkCb){ 33 | sinkCb(ended = err) 34 | } 35 | }) 36 | 37 | }) 38 | 39 | return (read) => { 40 | return new Promise((resolve, reject) => { 41 | req.on('error', err => { 42 | reject(err) 43 | }) 44 | if (read) { 45 | (function next () { 46 | read(null, (end, chunk) => { 47 | if (end === true) return req.end(response) 48 | if (end) return reject(end) 49 | req.write(chunk, next) 50 | }) 51 | })() 52 | } else { 53 | req.end(response) 54 | } 55 | 56 | function response () { 57 | const source = (end, cb) => { 58 | sinkCb = null //for storing reference to sink callback 59 | if (end) { 60 | req.abort() //will abort receiving chunks and will emit 'end' event 61 | sinkCb = cb 62 | } else if(buffer.length>0){ 63 | cb(null, buffer.shift()) 64 | } else { 65 | sinkCb = cb //there is nothing in the buffer, so no callback yet, we should wait for next chunk, or for 'end' event 66 | } 67 | } 68 | 69 | resolve(Object.assign({ source }, req)) 70 | } 71 | 72 | }) 73 | } 74 | } 75 | 76 | module.exports = fetch 77 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Jamen Marz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pull-fetch", 3 | "version": "1.1.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "balanced-match": { 8 | "version": "1.0.0", 9 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", 10 | "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", 11 | "dev": true 12 | }, 13 | "brace-expansion": { 14 | "version": "1.1.11", 15 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 16 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 17 | "dev": true, 18 | "requires": { 19 | "balanced-match": "^1.0.0", 20 | "concat-map": "0.0.1" 21 | } 22 | }, 23 | "concat-map": { 24 | "version": "0.0.1", 25 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 26 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", 27 | "dev": true 28 | }, 29 | "deep-equal": { 30 | "version": "1.0.1", 31 | "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", 32 | "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=", 33 | "dev": true 34 | }, 35 | "define-properties": { 36 | "version": "1.1.2", 37 | "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.2.tgz", 38 | "integrity": "sha1-g6c/L+pWmJj7c3GTyPhzyvbUXJQ=", 39 | "dev": true, 40 | "requires": { 41 | "foreach": "^2.0.5", 42 | "object-keys": "^1.0.8" 43 | } 44 | }, 45 | "defined": { 46 | "version": "1.0.0", 47 | "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz", 48 | "integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=", 49 | "dev": true 50 | }, 51 | "es-abstract": { 52 | "version": "1.11.0", 53 | "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.11.0.tgz", 54 | "integrity": "sha512-ZnQrE/lXTTQ39ulXZ+J1DTFazV9qBy61x2bY071B+qGco8Z8q1QddsLdt/EF8Ai9hcWH72dWS0kFqXLxOxqslA==", 55 | "dev": true, 56 | "requires": { 57 | "es-to-primitive": "^1.1.1", 58 | "function-bind": "^1.1.1", 59 | "has": "^1.0.1", 60 | "is-callable": "^1.1.3", 61 | "is-regex": "^1.0.4" 62 | } 63 | }, 64 | "es-to-primitive": { 65 | "version": "1.1.1", 66 | "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.1.1.tgz", 67 | "integrity": "sha1-RTVSSKiJeQNLZ5Lhm7gfK3l13Q0=", 68 | "dev": true, 69 | "requires": { 70 | "is-callable": "^1.1.1", 71 | "is-date-object": "^1.0.1", 72 | "is-symbol": "^1.0.1" 73 | } 74 | }, 75 | "for-each": { 76 | "version": "0.3.2", 77 | "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.2.tgz", 78 | "integrity": "sha1-LEBFC5NI6X8oEyJZO6lnBLmr1NQ=", 79 | "dev": true, 80 | "requires": { 81 | "is-function": "~1.0.0" 82 | } 83 | }, 84 | "foreach": { 85 | "version": "2.0.5", 86 | "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz", 87 | "integrity": "sha1-C+4AUBiusmDQo6865ljdATbsG5k=", 88 | "dev": true 89 | }, 90 | "fs.realpath": { 91 | "version": "1.0.0", 92 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 93 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", 94 | "dev": true 95 | }, 96 | "function-bind": { 97 | "version": "1.1.1", 98 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", 99 | "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", 100 | "dev": true 101 | }, 102 | "glob": { 103 | "version": "7.1.2", 104 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", 105 | "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", 106 | "dev": true, 107 | "requires": { 108 | "fs.realpath": "^1.0.0", 109 | "inflight": "^1.0.4", 110 | "inherits": "2", 111 | "minimatch": "^3.0.4", 112 | "once": "^1.3.0", 113 | "path-is-absolute": "^1.0.0" 114 | } 115 | }, 116 | "has": { 117 | "version": "1.0.1", 118 | "resolved": "https://registry.npmjs.org/has/-/has-1.0.1.tgz", 119 | "integrity": "sha1-hGFzP1OLCDfJNh45qauelwTcLyg=", 120 | "dev": true, 121 | "requires": { 122 | "function-bind": "^1.0.2" 123 | } 124 | }, 125 | "inflight": { 126 | "version": "1.0.6", 127 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 128 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 129 | "dev": true, 130 | "requires": { 131 | "once": "^1.3.0", 132 | "wrappy": "1" 133 | } 134 | }, 135 | "inherits": { 136 | "version": "2.0.3", 137 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 138 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", 139 | "dev": true 140 | }, 141 | "is-callable": { 142 | "version": "1.1.3", 143 | "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.3.tgz", 144 | "integrity": "sha1-hut1OSgF3cM69xySoO7fdO52BLI=", 145 | "dev": true 146 | }, 147 | "is-date-object": { 148 | "version": "1.0.1", 149 | "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", 150 | "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=", 151 | "dev": true 152 | }, 153 | "is-function": { 154 | "version": "1.0.1", 155 | "resolved": "https://registry.npmjs.org/is-function/-/is-function-1.0.1.tgz", 156 | "integrity": "sha1-Es+5i2W1fdPRk6MSH19uL0N2ArU=", 157 | "dev": true 158 | }, 159 | "is-regex": { 160 | "version": "1.0.4", 161 | "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", 162 | "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", 163 | "dev": true, 164 | "requires": { 165 | "has": "^1.0.1" 166 | } 167 | }, 168 | "is-symbol": { 169 | "version": "1.0.1", 170 | "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.1.tgz", 171 | "integrity": "sha1-PMWfAAJRlLarLjjbrmaJJWtmBXI=", 172 | "dev": true 173 | }, 174 | "minimatch": { 175 | "version": "3.0.4", 176 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 177 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 178 | "dev": true, 179 | "requires": { 180 | "brace-expansion": "^1.1.7" 181 | } 182 | }, 183 | "object-inspect": { 184 | "version": "1.5.0", 185 | "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.5.0.tgz", 186 | "integrity": "sha512-UmOFbHbwvv+XHj7BerrhVq+knjceBdkvU5AriwLMvhv2qi+e7DJzxfBeFpILEjVzCp+xA+W/pIf06RGPWlZNfw==", 187 | "dev": true 188 | }, 189 | "object-keys": { 190 | "version": "1.0.11", 191 | "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.0.11.tgz", 192 | "integrity": "sha1-xUYBd4rVYPEULODgG8yotW0TQm0=", 193 | "dev": true 194 | }, 195 | "once": { 196 | "version": "1.4.0", 197 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 198 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 199 | "dev": true, 200 | "requires": { 201 | "wrappy": "1" 202 | } 203 | }, 204 | "path-is-absolute": { 205 | "version": "1.0.1", 206 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 207 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", 208 | "dev": true 209 | }, 210 | "path-parse": { 211 | "version": "1.0.5", 212 | "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.5.tgz", 213 | "integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME=", 214 | "dev": true 215 | }, 216 | "pull-stream": { 217 | "version": "3.6.7", 218 | "resolved": "https://registry.npmjs.org/pull-stream/-/pull-stream-3.6.7.tgz", 219 | "integrity": "sha512-XdE2/o1I2lK7A+sbbA/HjYnd5Xk7wL5CwAKzqHIgcBsluDb0LiKHNTl1K0it3/RKPshQljLf4kl1aJ12YsCCGQ==", 220 | "dev": true 221 | }, 222 | "resolve": { 223 | "version": "1.5.0", 224 | "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.5.0.tgz", 225 | "integrity": "sha512-hgoSGrc3pjzAPHNBg+KnFcK2HwlHTs/YrAGUr6qgTVUZmXv1UEXXl0bZNBKMA9fud6lRYFdPGz0xXxycPzmmiw==", 226 | "dev": true, 227 | "requires": { 228 | "path-parse": "^1.0.5" 229 | } 230 | }, 231 | "resumer": { 232 | "version": "0.0.0", 233 | "resolved": "https://registry.npmjs.org/resumer/-/resumer-0.0.0.tgz", 234 | "integrity": "sha1-8ej0YeQGS6Oegq883CqMiT0HZ1k=", 235 | "dev": true, 236 | "requires": { 237 | "through": "~2.3.4" 238 | } 239 | }, 240 | "string.prototype.trim": { 241 | "version": "1.1.2", 242 | "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.1.2.tgz", 243 | "integrity": "sha1-0E3iyJ4Tf019IG8Ia17S+ua+jOo=", 244 | "dev": true, 245 | "requires": { 246 | "define-properties": "^1.1.2", 247 | "es-abstract": "^1.5.0", 248 | "function-bind": "^1.0.2" 249 | } 250 | }, 251 | "tape": { 252 | "version": "4.9.0", 253 | "resolved": "https://registry.npmjs.org/tape/-/tape-4.9.0.tgz", 254 | "integrity": "sha512-j0jO9BiScfqtPBb9QmPLL0qvxXMz98xjkMb7x8lKipFlJZwNJkqkWPou+NU4V6T9RnVh1kuSthLE8gLrN8bBfw==", 255 | "dev": true, 256 | "requires": { 257 | "deep-equal": "~1.0.1", 258 | "defined": "~1.0.0", 259 | "for-each": "~0.3.2", 260 | "function-bind": "~1.1.1", 261 | "glob": "~7.1.2", 262 | "has": "~1.0.1", 263 | "inherits": "~2.0.3", 264 | "minimist": "~1.2.0", 265 | "object-inspect": "~1.5.0", 266 | "resolve": "~1.5.0", 267 | "resumer": "~0.0.0", 268 | "string.prototype.trim": "~1.1.2", 269 | "through": "~2.3.8" 270 | }, 271 | "dependencies": { 272 | "minimist": { 273 | "version": "1.2.0", 274 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", 275 | "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", 276 | "dev": true 277 | } 278 | } 279 | }, 280 | "through": { 281 | "version": "2.3.8", 282 | "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", 283 | "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", 284 | "dev": true 285 | }, 286 | "wrappy": { 287 | "version": "1.0.2", 288 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 289 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", 290 | "dev": true 291 | } 292 | } 293 | } 294 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pull-fetch", 3 | "description": "Fetch function for pull stream", 4 | "version": "1.1.0", 5 | "homepage": "https://github.com/jamen/pull-fetch", 6 | "author": "Jamen Marz (https://github.com/jamen)", 7 | "main": "lib/pull-fetch.js", 8 | "repository": "jamen/pull-fetch", 9 | "license": "MIT", 10 | "scripts": { 11 | "test": "node test" 12 | }, 13 | "files": [ 14 | "lib" 15 | ], 16 | "devDependencies": { 17 | "pull-stream": "^3.6.7", 18 | "tape": "^4.9.0" 19 | }, 20 | "keywords": [ 21 | "pull-stream", 22 | "fetch", 23 | "request" 24 | ], 25 | "bugs": { 26 | "url": "https://github.com/jamen/pull-fetch/issues" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | 2 | # pull-fetch 3 | 4 | > A pull-stream HTTP client for Node.js 5 | 6 | ## Install 7 | 8 | ``` 9 | npm i pull-fetch 10 | ``` 11 | 12 | ## Usage 13 | 14 | ### `fetch(options)` 15 | 16 | Make an HTTP request. Options are the same as [`http.request`][1]. If it HTTPS it will switch the function. 17 | 18 | Returns a [`pull-stream` sink][2] that reads data for the request. If the request has not body (e.g. a GET request) then function returned can be called with nothing to proceed with the response. 19 | 20 | Then the stream returns a promise that resolves into a response object. It contains any property [`http.IncomingMessage`][3] has, plus `source` for streaming the response data as a [`pull-stream` source][4] 21 | 22 | ```js 23 | const response = await fetch({ 24 | host: 'api.example.com', 25 | path: '/foobar', 26 | method: 'POST' 27 | })( 28 | values([ 'hello', 'world' ]) 29 | ) 30 | 31 | console.log(response.headers) 32 | 33 | collect((err, data) => { 34 | data = Buffer.join(data).toString() 35 | consle.log(data) 36 | })(response.source) 37 | ``` 38 | 39 | Or with no body: 40 | 41 | ```js 42 | const response = await fetch('https://api.example.com/foobar')() 43 | 44 | console.log(response.headers) 45 | 46 | drain(x => process.stdout.write(x))(response.source) 47 | ``` 48 | 49 | Combining pull-streams and promises give an intuitive way to handle the requests. There is little to no overhead with Node's APIs. 50 | 51 | [1]: https://nodejs.org/api/http.html 52 | [2]: https://github.com/pull-stream/pull-stream/blob/master/docs/spec.md#sink-streams 53 | [3]: https://nodejs.org/api/http.html#http_class_http_incomingmessage 54 | [4]: https://github.com/pull-stream/pull-stream/blob/master/docs/spec.md#pull-streams -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | 2 | const test = require('tape') 3 | const fetch = require('./lib/pull-fetch.js') 4 | const { pull, collect, values, map, onEnd } = require('pull-stream') 5 | const { createServer } = require('http') 6 | 7 | const P = process.env.TEST_PORT || 8000 8 | 9 | test('GET request', t => { 10 | t.plan(4) 11 | 12 | const server = createServer((request, response) => { 13 | t.is(request.method, 'GET', 'method') 14 | t.is(request.url, '/foobar', 'url') 15 | t.true(request.headers['x-foobar'], 'request header') 16 | 17 | response.setHeader('X-Bazqux', 'Hi') 18 | response.write('Ping') 19 | setTimeout(() => response.end('ping'), 100) 20 | }) 21 | 22 | server.listen(P, async () => { 23 | const response = await fetch({ 24 | host: 'localhost', 25 | method: 'GET', 26 | path: '/foobar', 27 | port: P, 28 | headers: { 29 | 'X-Foobar': 'Hello world' 30 | } 31 | })() 32 | 33 | pull( 34 | response, 35 | map(x => x + ''), // coerce to strings 36 | collect((err, data) => { 37 | if (err) t.error(err) 38 | else t.same(data, ['Ping', 'ping'], 'response data') 39 | server.close() 40 | }) 41 | ) 42 | }) 43 | }) 44 | 45 | test('POST request', t => { 46 | t.plan(5) 47 | 48 | const server = createServer((request, response) => { 49 | t.is(request.method, 'POST', 'method') 50 | t.is(request.url, '/foobar', 'url') 51 | t.true(request.headers['x-foobar'], 'header') 52 | 53 | const data = [] 54 | 55 | request.on('data', chunk => { 56 | data.push(chunk + '') 57 | }) 58 | 59 | request.on('end', () => { 60 | t.same(data, ['Ping', 'ping'], 'request data') 61 | 62 | response.write('Pong') 63 | setTimeout(() => response.end('pong'), 100) 64 | }) 65 | }) 66 | 67 | server.listen(P, async () => { 68 | const response = await fetch({ 69 | host: 'localhost', 70 | method: 'POST', 71 | path: '/foobar', 72 | port: P, 73 | headers: { 74 | 'X-Foobar': 'Hello world' 75 | } 76 | })( 77 | values(['Ping', 'ping']) 78 | ) 79 | 80 | pull( 81 | response, 82 | map(x => x + ''), // coerce to strings 83 | collect((err, data) => { 84 | if (err) t.error(err) 85 | else t.same(data, ['Pong', 'pong'], 'response data') 86 | server.close() 87 | }) 88 | ) 89 | }) 90 | }) 91 | 92 | test('shorthand', t => { 93 | t.plan(3) 94 | 95 | const server = createServer((request, response) => { 96 | t.is(request.method, 'GET', 'method') 97 | t.is(request.url, '/foobar', 'url') 98 | response.write('Ping') 99 | setTimeout(() => response.end('ping'), 100) 100 | }) 101 | 102 | server.listen(P, async () => { 103 | const response = await fetch(`http://localhost:${P}/foobar`)() 104 | 105 | onEnd(err => { 106 | t.error(err) 107 | server.close() 108 | })(response.source) 109 | }) 110 | }) 111 | --------------------------------------------------------------------------------