diff --git a/API.md b/API.md index afd656c..d2fe61a 100644 --- a/API.md +++ b/API.md @@ -19,16 +19,18 @@ The following params are accepted: | Name | Type | Default | Description | | ---- | ---- | ------- | ----------- | +| `agent` | [`Agent`](https://devdocs.io/node/http#http_class_http_agent) | | optional shared agent to [manage connection persistence](https://devdocs.io/node/http#http_class_http_agent) | | `body` | `Any` | | data to serialize and send as the request body | | `deserialize` | `Function` | [`JSON.stringify`](http://devdocs.io/javascript/global_objects/json/stringify) | function with which to deserialize the response body | | `headers` | `Object` | `{}` | headers to include on the request | | `json` | `Boolean` | `true` | if `true`, assumes [json-formatted](#json-by-default) request and response | | `jwt` | `String` | | [json web token](https://jwt.io/) to include in the `Authorization` header | | `method` | `String` | `GET` | must be a valid `http` request method | +| `query` | `Object` | | data to serialize and append to the url as a query string | | `serialize` | `Function` | [`JSON.parse`](http://devdocs.io/javascript/global_objects/json/parse) | function with which to serialize the request body | | `stream` | `Boolean` | `false` | if `true`, the response `body` will be a [`stream.Readable`](http://devdocs.io/node/stream#stream_class_stream_readable) | +| `timeout` | `Number` | `0` (never) | milliseconds before the [request times out](https://devdocs.io/node/http#http_request_settimeout_timeout_callback) | | `url` | `String` | | **required:** the `url` of the request | -| `query` | `Object` | | data to serialize and append to the url as a query string | #### `Response` object diff --git a/index.js b/index.js index be6210d..a1105d3 100644 --- a/index.js +++ b/index.js @@ -6,6 +6,7 @@ const { validate } = require('@articulate/funky') const gimme = require('./lib/gimme') const schema = Joi.object({ + agent: Joi.any(), body: Joi.any(), data: Joi.any(), // deprecated deserialize: Joi.func(), @@ -13,10 +14,11 @@ const schema = Joi.object({ json: Joi.boolean(), jwt: Joi.string(), method: Joi.string().valid(http.METHODS), + query: Joi.object(), serialize: Joi.func(), stream: Joi.boolean(), + timeout: Joi.number().integer(), url: Joi.string().required(), - query: Joi.object(), }) module.exports = composeP(gimme, validate(schema)) diff --git a/lib/gimme.js b/lib/gimme.js index af87180..e595dd5 100644 --- a/lib/gimme.js +++ b/lib/gimme.js @@ -7,7 +7,7 @@ const { Readable } = require('stream') const { assoc, assocPath, evolve, flip, identity, is, - merge, nthArg, pick, pipe, replace, tryCatch + merge, nthArg, once, pick, pipe, replace, tryCatch } = require('ramda') const { name } = require('../package') @@ -24,21 +24,25 @@ const clean = pipe( const parseJSON = tryCatch(JSON.parse, nthArg(1)) +const warnOnce = once(console.warn) + const gimme = opts => { const { data, json = true } = opts - if (data) console.warn(chalk.yellow(`[${name}] The 'data' option is deprecated in favor of 'body'.`)) + if (data) warnOnce(chalk.yellow(`[${name}] The 'data' option is deprecated in favor of 'body'.`)) const { + agent, body = data, deserialize = json ? parseJSON : identity, jwt, method = 'GET', serialize = json ? JSON.stringify : identity, stream = false, + timeout = 0, } = opts const headers = merge({}, opts.headers) @@ -59,6 +63,7 @@ const gimme = opts => { const path = pathname + search const params = { auth, headers, hostname, method, path, port, protocol } + if (agent) params.agent = agent return new Promise((resolve, reject) => { const respond = res => { @@ -90,6 +95,7 @@ const gimme = opts => { const req = requester.request(params, respond) req.on('error', reject) + req.setTimeout(timeout, req.abort.bind(req)) if (method !== 'GET' && body) { if (is(Readable, body)) { diff --git a/test/00-setup.js b/test/00-setup.js index 2adf1c1..a5c8855 100644 --- a/test/00-setup.js +++ b/test/00-setup.js @@ -40,6 +40,7 @@ beforeEach(() => { nock(url).get('/error').query(true).reply(400) nock(url).get('/no-length').query(true).reply(200, { foo: 'bar' }) nock(url).get('/non-json-error').query(true).reply(400, 'string error') + nock(url).get('/timeout').query(true).socketDelay(1500).reply(200) }) afterEach(() => diff --git a/test/agent.js b/test/agent.js new file mode 100644 index 0000000..4243059 --- /dev/null +++ b/test/agent.js @@ -0,0 +1,30 @@ +const { Agent } = require('http') +const { expect } = require('chai') +const property = require('prop-factory') + +const gimme = require('..') +const { url } = require('./00-setup') + +describe('agent', () => { + let agent + const res = property() + + beforeEach(() => { + agent = new Agent({ keepAlive: true }) + res(undefined) + }) + + afterEach(() => { + agent.destroy() + }) + + describe('when supplied', () => { + beforeEach(() => + gimme({ url, agent }).then(res) + ) + + it('is allowed', () => { + expect(res().statusCode).to.equal(200) + }) + }) +}) diff --git a/test/timeout.js b/test/timeout.js new file mode 100644 index 0000000..fadf759 --- /dev/null +++ b/test/timeout.js @@ -0,0 +1,34 @@ +const { expect } = require('chai') +const property = require('prop-factory') + +const gimme = require('..') +const { url } = require('./00-setup') + +describe('timeout', () => { + const res = property() + + beforeEach(() => { + res(undefined) + }) + + describe('when not supplied', () => { + beforeEach(() => + gimme({ url: `${url}/timeout` }).then(res) + ) + + it('defaults to 0 (no timeout)', () => { + expect(res().statusCode).to.equal(200) + }) + }) + + describe('when supplied', () => { + beforeEach(() => + gimme({ url: `${url}/timeout`, timeout: 250 }).catch(res) + ) + + it('applies the timeout to the socket', () => { + expect(res()).to.be.an('Error') + expect(res().code).to.equal('ECONNRESET') + }) + }) +})