diff --git a/EXTRA.md b/EXTRA.md index babf071..159b2c4 100644 --- a/EXTRA.md +++ b/EXTRA.md @@ -3,6 +3,36 @@ There are some niche features of @root/request which are beyond the request.js compatibility. +## async/await & Promises + +The differences in async support are explained in [README.md](/README.md), up near the top. + +If you're familiar with Promises (and async/await), then it's pretty self-explanatory. + +## ok + +Just like WHATWG `fetch`, we have `resp.ok`: + +```js +let resp = await request({ + url: 'https://example.com' +}).then(mustOk); +``` + +```js +function mustOk(resp) { + if (!resp.ok) { + // handle error + throw new Error('BAD RESPONSE'); + } + return resp; +} +``` + +## streams + +The differences in stream support are explained in [README.md](/README.md), up near the top. + ## userAgent There's a default User-Agent string describing the version of @root/request, node.js, and the OS. diff --git a/README.md b/README.md index 612f7da..7e082b9 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Written from scratch, with zero-dependencies. ## Super simple to use -@root/request is designed to be a drop-in replacement for request. It also supports Promises and async/await by default. +@root/request is designed to be a drop-in replacement for request. It also supports Promises and async/await by default, enhanced stream support, and a few other things as mentioned below. ```bash npm install --save @root/request @@ -51,7 +51,7 @@ read, the streaming behavior is **_slightly different_** from that of ```diff -var request = require('request'); +var request = require('@root/request'); - + -var stream = request({ url, headers }); +var stream = await request({ url, headers }); @@ -105,17 +105,42 @@ request({ }); ``` +Also, `await resp.stream.body()` can be used to get back the full body (the same as if you didn't use the `stream` option: + +```js +let resp = await request({ + url: 'http://www.google.com', + stream: true +}); +if (!resp.ok) { + await resp.stream.body(); + console.error(resp.body); +} +``` + ## Table of contents +- [Extra Features](/EXTRA.md) - [Forms](#forms) - [HTTP Authentication](#http-authentication) - [Custom HTTP Headers](#custom-http-headers) - [Unix Domain Sockets](#unix-domain-sockets) - [**All Available Options**](#requestoptions-callback) +## Extra Features + +The following are features that the original `request` did not have, but have been added for convenience in `@root/request`. + +- Support for `async`/`await` & `Promise`s (as explained above) +- `request({ userAgent: 'my-api/1.1' })` (for building API clients) +- `resp.ok` (just like `fetch`) +- `resp.stream` (see above) + +See [EXTRA.md](/EXTRA.md) + ## Forms -`urequest` supports `application/x-www-form-urlencoded` and `multipart/form-data` form uploads. +`@root/request` supports `application/x-www-form-urlencoded` and `multipart/form-data` form uploads. @@ -310,7 +335,7 @@ request(options, callback); ## UNIX Domain Sockets -`urequest` supports making requests to [UNIX Domain Sockets](https://en.wikipedia.org/wiki/Unix_domain_socket). To make one, use the following URL scheme: +`@root/request` supports making requests to [UNIX Domain Sockets](https://en.wikipedia.org/wiki/Unix_domain_socket). To make one, use the following URL scheme: ```js /* Pattern */ 'http://unix:SOCKET:PATH'; @@ -426,7 +451,7 @@ These HTTP method convenience functions act just like `request()` but with a def There are at least two ways to debug the operation of `request`: -1. Launch the node process like `NODE_DEBUG=urequest node script.js` +1. Launch the node process like `NODE_DEBUG=@root/request node script.js` (`lib,request,otherlib` works too). 2. Set `require('@root/request').debug = true` at any time (this does the same thing diff --git a/index.js b/index.js index 83717b9..2bc0765 100644 --- a/index.js +++ b/index.js @@ -66,6 +66,102 @@ function toJSONifier(keys) { }; } +function setupPipe(resp, opts) { + // make the response await-able + var resolve; + var reject; + var p = new Promise(function (_resolve, _reject) { + resolve = _resolve; + reject = _reject; + }); + + // or an existing write stream + if ('function' === typeof opts.stream.pipe) { + if (opts.debug) { + console.debug('[@root/request] stream piped'); + } + resp.pipe(opts.stream); + } + resp.once('error', function (e) { + if (opts.debug) { + console.debug("[@root/request] stream 'error'"); + console.error(e.stack); + } + resp.destroy(); + if ('function' === opts.stream.destroy) { + opts.stream.destroy(e); + } + reject(e); + }); + resp.once('end', function () { + if (opts.debug) { + console.debug("[@root/request] stream 'end'"); + } + if ('function' === opts.stream.destroy) { + opts.stream.end(); + // this will close the stream (i.e. sync to disk) + opts.stream.destroy(); + } + }); + resp.once('close', function () { + if (opts.debug) { + console.debug("[@root/request] stream 'close'"); + } + resolve(); + }); + return p; +} + +function handleResponse(resp, opts, cb) { + // body can be buffer, string, or json + if (null === opts.encoding) { + resp._body = []; + } else { + resp.body = ''; + } + resp._bodyLength = 0; + resp.on('readable', function () { + var chunk; + while ((chunk = resp.read())) { + if ('string' === typeof resp.body) { + resp.body += chunk.toString(opts.encoding); + } else { + resp._body.push(chunk); + resp._bodyLength += chunk.length; + } + } + }); + resp.once('end', function () { + if ('string' !== typeof resp.body) { + if (1 === resp._body.length) { + resp.body = resp._body[0]; + } else { + resp.body = Buffer.concat(resp._body, resp._bodyLength); + } + resp._body = null; + } + if (opts.json && 'string' === typeof resp.body) { + // TODO I would parse based on Content-Type + // but request.js doesn't do that. + try { + resp.body = JSON.parse(resp.body); + } catch (e) { + // ignore + } + } + + debug('\n[urequest] resp.toJSON():'); + if (module.exports.debug) { + debug(resp.toJSON()); + } + if (opts.debug) { + console.debug('[@root/request] Response Body:'); + console.debug(resp.body); + } + cb(null, resp, resp.body); + }); +} + function setDefaults(defs) { defs = defs || {}; @@ -100,9 +196,16 @@ function setDefaults(defs) { }); followRedirect = opts.followRedirect; + // copied from WHATWG fetch + resp.ok = false; + if (resp.statusCode >= 200 && resp.statusCode < 300) { + resp.ok = true; + } + resp.toJSON = toJSONifier([ 'statusCode', 'body', + 'ok', 'headers', 'request' ]); @@ -156,96 +259,18 @@ function setDefaults(defs) { } if (opts.stream) { - // make the response await-able - var resolve; - var reject; - resp.stream = new Promise(function (_resolve, _reject) { - resolve = _resolve; - reject = _reject; - }); - - // or an existing write stream - if ('function' === typeof opts.stream.pipe) { - if (opts.debug) { - console.debug('[@root/request] stream piped'); - } - resp.pipe(opts.stream); - } - resp.on('error', function (e) { - if (opts.debug) { - console.debug("[@root/request] stream 'error'"); - console.error(e.stack); - } - resp.destroy(); - if ('function' === opts.stream.destroy) { - opts.stream.destroy(e); - } - reject(e); - }); - resp.on('end', function () { - if (opts.debug) { - console.debug("[@root/request] stream 'end'"); - } - if ('function' === opts.stream.destroy) { - opts.stream.end(); - // this will close the stream (i.e. sync to disk) - opts.stream.destroy(); - } - }); - resp.on('close', function () { - if (opts.debug) { - console.debug("[@root/request] stream 'close'"); - } - resolve(); - }); - // and in all cases, return the stream + resp.stream = setupPipe(resp, opts); + // can be string, buffer, or json... why not an async function too? + resp.stream.body = async function () { + handleResponse(resp, opts, cb); + await resp.stream; + return resp.body; + }; cb(null, resp); return; } - if (null === opts.encoding) { - resp._body = []; - } else { - resp.body = ''; - } - resp._bodyLength = 0; - resp.on('data', function (chunk) { - if ('string' === typeof resp.body) { - resp.body += chunk.toString(opts.encoding); - } else { - resp._body.push(chunk); - resp._bodyLength += chunk.length; - } - }); - resp.on('end', function () { - if ('string' !== typeof resp.body) { - if (1 === resp._body.length) { - resp.body = resp._body[0]; - } else { - resp.body = Buffer.concat(resp._body, resp._bodyLength); - } - resp._body = null; - } - if (opts.json && 'string' === typeof resp.body) { - // TODO I would parse based on Content-Type - // but request.js doesn't do that. - try { - resp.body = JSON.parse(resp.body); - } catch (e) { - // ignore - } - } - - debug('\n[urequest] resp.toJSON():'); - if (module.exports.debug) { - debug(resp.toJSON()); - } - if (opts.debug) { - console.debug('[@root/request] Response Body:'); - console.debug(resp.body); - } - cb(null, resp, resp.body); - }); + handleResponse(resp, opts, cb); } var _body; @@ -437,7 +462,7 @@ function setDefaults(defs) { } } req = requester.request(finalOpts, onResponse); - req.on('error', cb); + req.once('error', cb); if (_body) { debug("\n[urequest] '" + finalOpts.method + "' (request) body"); @@ -445,7 +470,7 @@ function setDefaults(defs) { if ('function' === typeof _body.pipe) { // used for chunked encoding _body.pipe(req); - _body.on('error', function (err) { + _body.once('error', function (err) { // https://nodejs.org/api/stream.html#stream_readable_pipe_destination_options // if the Readable stream emits an error during processing, // the Writable destination is not closed automatically