A general purpose HTTP client built with extensibility in mind.
- uses
wreck
underneath the covers - circuit breaking (
circuit-state
) - extensible with lifecycle hooks (see Plugins section)
- dynamic hostname resolution
- creates secure context
- promise based - no callbacks
npm install @vrbo/service-client
const ServiceClient = require('@vrbo/service-client')
// Create a re-usable client for a service.
const client = ServiceClient.create('myservice', {
/* overrides the global defaults */
hostname: 'example.com',
port: 80
})
try {
const response = await client.request({
method: 'GET',
path: '/v1/listings/{listingId}',
pathParams: {
listingId: 'vb24523'
}
})
} catch (error) {
console.log(error)
}
For a more thorough collection of examples see the examples directory.
- protocol - The protocol to use for the request. Defaults to
"http:"
. - connectTimeout - The connection timeout. Defaults to
1000
. - maxConnectRetry - After
connectTimeout
elapses, how many attempts to retry the connection. Defaults to0
. - timeout - The number of ms to wait without receiving a response before aborting the request. Defaults to
10000
. - maxFailures - Maximum number of failures before circuit breaker flips open. See
circuit-state
. Defaults to3
. - resetTime - Time in ms before an open circuit breaker returns to a half-open state. If 0 or less, manual resets will be used. See
circuit-state
. Defaults to30000
. - agentOptions - Instead of passing
agent
, pass options to initializeagent
internally. See http.Agent or https.Agent docs.- keepAlive - Defaults to
true
. - keepAliveMsecs - Defaults to
30000
.
- keepAlive - Defaults to
Returns a new service client instance for servicename
with optional overrides
to the global defaults listed above:
- protocol - The protocol to use for the request. Defaults to
"http:"
. - hostname - The hostname to use for the request. Accepts a
string
or afunction(serviceName, serviceConfig)
that returns a string. - port - The port number to use for the request.
- basePath - The base path used to prefix every request path. This path should begin with a
/
. - connectTimeout - The connection timeout. Defaults to
1000
. - maxConnectRetry - After
connectTimeout
elapses, how many attempts to retry the connection. Defaults to0
. - timeout - The number of ms to wait without receiving a response before aborting the request. Defaults to
10000
. - maxFailures - Maximum number of failures before circuit breaker flips open. See
circuit-state
. Defaults to3
. - resetTime - Time in ms before an open circuit breaker returns to a half-open state. If 0 or less, manual resets will be used. See
circuit-state
. Defaults to30000
. - agent - An http/https agent instance. See Node docs. Defaults to an instance created internally using
agentOptions
. - agentOptions - Instead of passing
agent
, pass options to initializeagent
internally. See http.Agent or https.Agent docs.- keepAlive - Defaults to
true
. - keepAliveMsecs - Defaults to
30000
. - secureContext - A SecureContext instance. Defaults to an instance created internally using
secureContextOptions
. - secureContextOptions - Instead of passing
secureContext
here, pass options to initializesecureContext
internally. See tls.createSecureContext() options.
- keepAlive - Defaults to
- plugins - Configuration object for ServiceClient plugins.
Returns a promise that resolves into the payload in the form of a Buffer or (optionally) parsed JavaScript object (JSON).
- response - An HTTP Incoming Message object
- options - A configuration object
- timeout - The number of milliseconds to wait while reading data before aborting handling of the response. Defaults to unlimited.
- json - A value indicating how to try to parse the payload as JSON. Defaults to
true
.- true - Only try
JSON.parse
if the response indicates a JSON content-type. - false - Do not try
JSON.parse
on the response at all. - strict - Same as
true
, except throws an error for non-JSON content-type. - force - Try
JSON.parse
regardless of the content-type header.
- true - Only try
- gunzip - A value indicating the behavior to adopt when the payload is gzipped. Defaults to
undefined
meaning no gunzipping.- true - Only try to gunzip if the response indicates a gzip content-encoding.
- false - Explicitly disable gunzipping.
- force - Try to gunzip regardless of the content-encoding header.
- maxBytes - The maximum allowed response payload size. Defaults to unlimited.
Merges and overrides configuration with the global ServiceClient config.
Globally registers plugins. A helper function for calling ServiceClient.mergeConfig({plugins: []})
. See plugins.
An instance returned by ServiceClient.create()
.
- stats - The circuit breaker stats for this client instance.
- id - The generated unique identifier for this client instance.
-
config(chain, options)
- Read the configuration provided for the client. SeeHoek.reach()
.- chain - Object path syntax to the property you want.
- options - Options to pass to
Hoek.reach()
.
-
request(options)
- Makes an http request.- method - The HTTP method.
- hostPrefix - A base prefix that gets prepended to the hostname.
- path - Defaults to
'/'
. - queryParams - Object containing key-value query parameter values.
- pathParams - Object containing key-value path parameters to replace
{someKey}
value in path with. - headers - Object containing key-value pairs of headers.
- payload - The payload to send, if any.
- redirects - The number of redirects to allow.
- operation - The unique name for this endpoint request (default example:
GET
/v1/supply/properties/austin
->GET_v1_supply_properties
). Required. - timeout - The timeout for this request.
- connectTimeout - The connection timeout for this request.
- maxConnectRetry - On
connectionTimeout
elapsed, how many attempts to retry connection. - maxBytes - Maximum size for response payload.
- agent - An optional custom agent for this request.
- read - Whether or not to read the response (default:
true
). - readOptions - Options for reading the response payload. See
ServiceClient.read()
.- timeout - Defaults to
20000
- json - Defaults to
true
- gunzip - Defaults to
false
- maxBytes - No default
- timeout - Defaults to
- context - The upstream request object (usually just a Hapi
request
orserver
object) to be passed to each hook. - plugins - The request-specific configuration object for service client plugins.
-
read(response, [options])
- Read the response payload. See the documentation forServiceClient.read()
.
Service-Client plugins provide the ability to hook into different spots of a request's lifecycle, and in some hooks, await some asynchronous action.
Plugins are registered with ServiceClient.mergeConfig({plugins: []})
or ServiceClient.use([])
and affect all Service-Client instances created thereafter.
A plugin exports a function which, when executed, returns an object containing a set of hook functions to be ran during a request's lifecycle. See all available hooks below.
Example:
module.exports = function({client, context, plugins}) {
return {
request({...}) {
},
lookup({...}) {
},
response({...}) {
}
};
};
const ServiceClient = require('@vrbo/service-client');
const SCStatsD = require('@vrbo/service-client-statsd');
ServiceClient.use(SCStatsD);
// Alternatively with an array of plugins
ServiceClient.use([
SCStatsD,
Plugin1,
Plugin2
]);
/**
* In the most general terms, a plugin is a function which returns a set of hook
* functions. It can be async if necessary to perform some initial setup. Each
* plugin is initialized within the context of every request. Provided to the
* function are references to some helpful objects.
*
* @param {object} data - the object provided on every request to the plugin
* @param {object} data.client - the service client instance for this request
* @param {object} data.context - the upstream request object passed as `context` in the request options
* @param {object} data.plugins - the request-specific plugin options passed as `plugins` in the request options
*/
async function plugin({client, context, plugins}) {
/**
* Each hook is supplied with at least these pieces of data:
*
* @param {object} data - data provided to the hook on every request
* @param {object} data.context - the upstream request object (usually just a Hapi request or server object)
* @param {string} data.clientId - the unique id of this particular instance of service-client
* @param {string} data.requestId - the unique id of this particular request
* @param {number} data.ts - the unix timestamp of when this hook was executed
* @param {string} data.servicename - the name of the service associated with this instance of service-client
* @param {string} data.operation - the normalized operation name for this request (ex: 'GET_v1_test_success')
*/
return {
/**
* This hook is special. Data returned here will be deep merged with the
* request options data that will be passed on to Wreck. If you wanted
* to add headers to the request, this is where you would do it.
* (ex: return {headers: {'x-ha-blah': 'foobar'}})
*
* @param {object} data - data provided to the hook on every request
* @param {string} data.method - the request method (ex: 'GET', 'POST', etc)
* @param {string} data.path - the request path (ex: '/v1/test/success')
* @param {object} data.options - the request options to be passed to Wreck
*/
async request(data) {
},
/**
* @param {object} data - data provided to the hook on every request
* @param {string} data.method - the request method (ex: 'GET', 'POST', etc)
* @param {string} data.path - the request path (ex: '/v1/test/success')
* @param {object} data.options - the request options to be passed to Wreck
*/
async init(data) {
},
/**
* @param {object} data - data provided to the hook on every request
* @param {object} data.socket - The node socket associated with this request. See Node's net.Socket class.
*/
socket(data) {
},
/**
* @param {object} data - data provided to the hook on every request
* @param {Error} data.error - the error object
* @param {string} data.address - the ip address
* @param {string} data.family - the address type (4 or 6)
* @param {string} data.host - the hostname
*/
lookup(data) {
},
/**
* @param {object} data - data provided to the hook on every request
*/
connect(data) {
},
/**
* @param {object} data - data provided to the hook on every request
*/
secureConnect(data) {
},
/**
* This hook is special. The only value that should be returned here is
* a response object. This is helpful for authentication plugins that
* want to retry a request if an invalid response was received. A request
* made within this hook could return it's response object here.
*
* There's a catch however. Since response objects from multiple hooks
* cannot be merged, only the first response object will be taken. All
* other returned responses are discarded.
*
* @param {object} data - data provided to the hook on every request
* @param {object} data.response - the response received from Wreck before being read
*/
async response(data) {
},
/**
* @param {object} data - data provided to the hook on every request
* @param {Error} data.error - the error object resulting from timeouts, read errors, etc
*/
async error(data) {
},
/**
* @param {object} data - data provided to the hook on every request
* @param {boolean} data.open - whether or not the circuit breaker is open (see `circuit-state` module)
* @param {number} data.executions - the number of executions
* @param {number} data.successes - the number of successes
* @param {number} data.failures - the number of failures
*/
async stats(data) {
},
/**
* @param {object} data - data provided to the hook on every request
* @param {object} data.options - the request options that were passed to Wreck
* @param {Error} data.error - the error object from the circuit breaker, timeouts, read errors, etc
* @param {object} data.stats - an object containing `open`, `executions`, etc (see stats hook)
*/
async end(data) {
}
}
}
// my-example.js
const client = require('@vrbo/service-client');
ServiceClient.use(({client, context, plugins}) => {
console.log('client plugin options:', client.config('plugins.myPlugin');
console.log('context:', context);
console.log('request plugin options:', plugins.myPlugin);
return {
request() {
console.log('there was a request');
},
error() {
console.log('there was an error');
},
response() {
console.log('there was a response');
}
}
});
const client = ServiceClient.create('myservice', {
plugin: {
myPlugin: {
clientOption: 'foobar'
}
}
});
try {
const response = await client.request({
method: 'GET',
path: '/v1/test/stuff',
plugins: {
myPlugin: {
option1: 'fizzbuzz'
}
}
});
} catch (error) {
console.log(error);
}
> node ./my-example.js
config: {"clientOption": "foobar"}
context: [context object]
plugins: {"option1": "fizzbuzz"}
there was a request
there was a response
>
Any content-length
header passed into the request options will be removed as a precaution against mismatching content-length values downstream. In the case of a non-buffer payload, this can occur if escape characters were taken into consideration when calculating the content length. This mismatch will result in the downstream service waiting for bytes that will never be sent. By removing the header, we defer to Wreck to calculate the correct content length.
This package uses debug
for logging debug statements.
Use the serviceclient
namespace to log related information:
DEBUG=serviceclient:* npm test