A reactive HTTP client for node.js and browsers designed for distributed systems, providing fault tolerance capabilities with transparent server fallback, dynamic server discovery (e.g: using Consul), request retry/backoff logic, optional client-side balancing based on empirical server latency and more...
Provides a simple middleware-oriented programmatic API and featured command-line interface. It has been designed to be lightweight (just ~2K SLOC. 9KB gzipped) and dependency free.
To get started, take a look to how does it work, basic usage, examples and API docs.
Resilient is conceptually similar to Ribbon, a Netflix's project and it was inspired by Chaos Engineering.
request
package)Promise
global to be available)Via npm
npm install resilient
Via Bower
bower install resilient
Via Component
component install resilient-http/resilient.js
Or loading the script remotely
<script src="https://github.com/resilient-http/resilient.js/raw/master//cdn.rawgit.com/resilient-http/resilient.js/0.4.0/resilient.js"></script>
Runs in any ES5 compliant engine.
+4 | +33 | +29 | +10 | +19 | +7.1 |
See the basic usage and examples for detailed use cases
The following graph represents from a high-level point of view the internal logic encapsulated in Resilient HTTP client.
If require
is available, you must use it to fetch the module.
Otherwise it will be available as global exposed as resilient
var Resilient = require('resilient')
Define your service servers pool
var servers = [
'http://api1.server.com',
'http://api2.server.com',
'http://api3.server.com'
]
Create a new client and set the servers to balance
var client = Resilient({ service: { basePath: '/api/1.0' }})
client.setServers(servers)
Perform a request (the best available server will be used automatically)
client.get('/users').then(function (res) {
if (res.status === 200) {
console.log('Success:', res.data)
}
})
Define the lookup servers pool
var servers = [
'http://discover1.server.com',
'http://discover2.server.com',
'http://discover3.server.com'
]
Create a new client and set the discovering servers
var client = Resilient({ service: { basePath: '/api/1.0' }})
client.discoveryServers(servers)
Finally, perform the request (and that's all, Resilient will take care about everything to reach the best server)
client.get('/users').then(function (res) {
if (res.status === 200) {
console.log('Success:', res.data)
}
})
Note: you could use Consul or other HTTP server using a custom middleware as discovery server. For more information about the Resilient discovery interface, take a look at the documentation
For more usage cases take a look to the examples
From version 0.3.x
Resilient introduces support for duplex middleware.
It essentially provides an interceptor like layer to use external components to augment a specific functionality.
From a high-level point of view it's conceptually similar to an evented API approach, which is commonly used in a event-driven environment with JavaScript, but in this case it's slightly different in terms of flow control nature and relies more in data mutation compared to events.
The significant feature in Resilient middleware layer is that it provides bidirectional control flow for both incoming and outgoing HTTP traffic. This allows you to perform multiple actions before and after a request of a specific type is made by Resilient. This might be considered also as a sort hooks in aspect-oriented programming.
Since Resilient is divided in two communication live cycle layers, one for the discovery
servers and the other one for the service
end servers, middleware can be created for both layers:
Note: the middleware type should be defined a static member of the middleware returned function, using the type
property.
Required interface for middleware:
Function([ params ])
-> Function(options, resilient)
-> Object{ in: Function(err, res, next), out: Function(option, next) }
For non-duplex middleware you can use the following interface as well:
Function([ params ])
-> Function(options, resilient)
-> Function(err, res, next)
An example of a simple middleware implementation:
function testMiddleware(params) {
// Middleware-specific params
params = params || {}
// Do whatever you need here with the params
// Resilient will pass the Options
function middleware(options, resilient) {
// Do whatever you need here with Resilient client options
// such as defining servers
return {
'in': function (err, res, next) {
// Do something here with the err/response
next() // Don't forget to call next
},
'out': function (options, next) {
// Do something here with the out HTTP request options
next() // Don't forget to call next
}
}
}
middleware.type = 'discovery' // Default to: service
return middleware
}
An example of middleware usage:
var client = Resilient({
discovery: {
servers: ['http://server1', 'http://server2']
}
})
client.use(testMiddleware({
custom: 'options',
key: 'api-key',
timeout: 3000
}))
client.get('/').then(function (res) {
console.log(res)
}).catch(function (err) {
console.error(err)
})
For better approach you should install Resilient
as global package: npm install -g resilient
Resilient command-line HTTP client
Usage: resilient [url] [options]
Examples:
resilient http://httpbin.org/user-agent
resilient --status http://httpbin.org/status/201
resilient --info http://httpbin.org/status/204
resilient http://httpbin.org/post -x POST \
-d '{"hello":"world"}' -h "Content-Type: application/json"
resilient /api/users -s http://server1.net,http://server2.net
resilient /api/users -z http://discover1.net,http://discover2.net
resilient --discover -z http://discover1.net,http://discover2.net --discovery-timeout 500
Options:
--version, -v Show the Resilient client version
--path, -p Request path
--servers, -s Define the service servers (comma separated)
--method, -x HTTP method
--header, -h Define custom request header
--data, -d Value data to send as HTTP request body
--file, -f File path to send as HTTP request body
--retry, -r Request retry attempts [default: 0]
--timeout, -t Request timeout in miliseconds
--discover, -k Get an updated list of servers asking for discovery servers
--discovery-servers, -z Define the discovery service servers (comma separated)
--discovery-retry, -R Discovery servers retry attempts [default: 0]
--discovery-timeout, -T Discovery servers request maximum timeout in miliseconds
--info, -i Show response headers and info
--info-headers, -I Show only the response status and headers
--status, -c Print the response status code
--debug, -D Enable debug mode
--help, -H Show help
Creates a new resilient
client with custom config
The options object
supports three different configuration levels
Resilient({
service: { ... },
balancer: { ... },
discovery: { ... }
})
Specific configuration options for the end service servers pool of the Resilient client.
array<string>
- A list of valid servers URIs to reach for the given service. Default null
. It's recommended to use discovery servers instead of static service serversnumber
- Number of times to retry if all requests failed. Use Infinity
for infinitive attemps. Default 0
number
- Number of milisenconds to wait before start the request retry cycle. Default to 50
boolean
- Force to refresh service servers list asking to the discovery servers on each retry attempt. You must define the discovery servers in order to use this feature. Default true
boolean
- Enable promiscuous error handling mode. Client HTTP status errors (400-499) will be treated as failed request, retrying it until it has a valid status (when retry
option is enabled). Default false
array<object>
- A collection of rules per method and status code to match in order to omit a request retry cycle. See the usage example. Default null
array<object>
- A collection of rules per method and status code to match in order to omit a request server fallback. See usage example. Default null
array<string>
- Omit a retry cycle attempt if the request method is on the given array. Default null
array<string>
- Omit fallback to the next best available server if current HTTP method is on the given array. If you use this option, retry cycles will be disabled as well for the given methods. Default null
array<number>
- Omit a retry cycle attempt if the latest request response status code is one of the given array. Default null
array<number>
- Omit fallback to the next best available server if the latest request response status code is one of the given array. Default null
object
- Define custom request timeout values in miliseconds per HTTP method, useful to differ read/write requests. This option has priority over the timeout
option. Default: null
Specific shared configuration options for the HTTP client for final service requests
string
- Server request path as part of the final URLstring
- Server resource base path to share between all requestsstring
- Request HTTP method. Default to GET
mixed
- Payload data to send as body requestobject
- Map of strings representing HTTP headers to send to the serverobject
- Map of strings representing the query paramsnumber
- Request maximum timeout in miliseconds before to abort it. Default to 10
secondsobject
- Authentication credentials to the server. Object must have both user
and password
propertiesBrowser specific options
boolean
- Whether to set the withCredentials flag on the XHR object. See MDN for more informationstring
- Define how to handle the response data. Allowed values are: text
, arraybuffer
, blob
or document
Node.js specific options
See all HTTP options supported for node.js
here
boolean
- Enable/disable the smart client balancer. Default to true
.boolean
- Use random balance strategy using Math.random()
. Default to false
.function
- Custom balance strategy function. Interface to be implemented: function (servers []Server) => []Server
. Default to null
.boolean
- Disable emphirical built-in weight calculus based on server stats for balancing (latency, errors, success).boolean
- Enable round robin schedule algorithm (experimental). Default: false
.number
- Round robin round size. Useful to increase requests distribution across different servers. Default to 3
servers.object
- Balacer calculus percentage weight used for best available server scoring policy:
number
- Percentage weight for success request. Default to 15
.number
- Percentage weight for failed request. Default to 50
.number
- Percentage weight for request average latency. Default to 35
.Specific configuration for discovery servers requests, behavior and logic
array<string>
- A list of valid server URIs to use as discovery serversboolean
- Enable/disable discovery servers cache in case of global fallback. Useful to improve client reliability. Default true
number
- Maximum cache time to live. Default to 10
minutesnumber
- Number of times to retry if all requests failed. Use Infinity
for infinitive attemps. Default 3
number
- Number of milisenconds to wait before start the request retry cycle. Default to 1000
boolean
- Discover servers in parallel. This will improve service availability and decrement server lookup delays. Default true
number
- Servers list time to live in miliseconds. Default to 2
minutesboolean
- Enable/disable discovery servers auto refresh. This option requires refreshServers
or enableSelfRefresh
has been defined. Default true
array
- Servers list of refresh servers. This will enable automatically update discovery servers list asking for them selves to the following list of servers on each interval. Default null
number
- Discovery servers list time to live in miliseconds. Default to 5
minutesboolean
- Enable/disable self-discovery using the discovery servers pools. This requires the refreshPath
option has been defined. Default false
boolean
- Enable/disable forcing to refresh the server on the first request. This requires the refreshPath
and enableRefreshServers
options has been defined. Default true
string
- Discovery refresh servers lookup path. Example: /app/name
. This options requires you define enableSelfRefresh
to true
. Default null
object
- Custom HTTP options for discovery servers refresh. By default inherits from discovery optionsboolean
- Enable promiscuous error handling mode. Client HTTP status errors (400-499) will be treated as failed request, retrying it until it has a valid status (when retry
option is enabled). Default false
array<object>
- A collection of rules per method and status code to match in order to omit a request retry cycle. See the usage example. Default null
array<object>
- A collection of rules per method and status code to match in order to omit a request server fallback. See usage example. Default null
array<string>
- Omit a retry cycle attempt if the request HTTP method is on the given array. Default null
array<string>
- Omit fallback to the next best available server if the HTTP method is on the given array. If you use this option, retry cycles will be disabled as well for the given methods. Default null
array<number>
- Omit a retry cycle attempt if the latest request response status code is one of the given array. Default null
array<number>
- Omit fallback to the next best available server if the latest request response status code is one of the given array. Default null
object
- Define custom request timeout values in miliseconds per HTTP method, useful to differ read/write requests. This option has priority over the timeout
option. Default: null
Specific shared configuration options for the HTTP client for discovering processes
string
- Server request path as part of the final URLstring
- Server resource base path to share between all requestsnumber
- Server discovery network timeout in miliseconds. Default 2
secondsobject
- Authentication credentials required for the discovery server. Object must have both user
and password
propertiesobject
- Map of strings representing the query paramsobject
- Map of strings representing HTTP headers to send to the discovery serverstring
- Request HTTP method. Default to GET
mixed
- Optional data to send as payload to discovery servers. Default null
For node.js
, see all HTTP options supported here
null
mixed
- Body response. If the MIME type is JSON-compatible
, it will be transparently parsednumber
- HTTP response status codeobject
- Response headersobject
- Original XHR instancemixed
- Error info, usually an Error
instance (in case that an error happens)It could be an Error
or plain Object
instance with the following members
string
- Human readable error message descriptionnumber
- Internal error code or server HTTP response statusnumber
- Optional error code (node.js only)string
- Optional stack error traceobject
- Original response object in case that a custom Resilient error happends. OptionalError
- Original throwed Error instance (node.js only). OptionalXMLHttpRequest
- XHR native instance (browser only)Resilient client has a built-in support for internal states event dispacher and notifier to the public interface
This could be really useful while using an interceptor pattern in order to detect different states and data changes.
You can intercept and change any both request and response objects
subscribing to the pre/post request hooks.
Note that mutation is required, you should modify the object
by reference and do not lose it
// subscribe to every outgoing request before be dropped to the network
resilientClient.on('request:start', function handler(options, resilient) {
// mutate the options, adding an aditional header
options.headers['API-Token'] = 'awesome!'
// unsubscribe example
resilientClient.off('request:start', handler)
})
Arguments: options<Object>
, resilient<Resilient>
Fired before a request is created
You can intercept and modify the request options on the fly, but you must mutate the options object
Arguments: options<Object>
, resilient<Resilient>
Fired every time before a HTTP request is sent via network
You can intercept and modify the request options on the fly, but you must mutate the options object
Arguments: error<Error>
, response<Object|http.IncomingMessage>
, options<Object>
, resilient<Resilient>
Fired every time a HTTP response is received from a server
Arguments: error<Error>
, response<Object|http.IncomingMessage>
, resilient<Resilient>
Fired after a request was completed
You can intercept and modify the error/response on the fly, but you must mutate the object
Arguments: options<Object>
, servers<Servers>
Fired when a request performs a retry attempt cycle, that means all the previous requests has failed
Arguments: options<Object>
, response<Object>
Fired when any request (service or discovery) to a given server fails and therefore tries to perform the next server fallback
Arguments: servers<Array>
, resilient<Resilient>
Fired every time that service servers list is updated from discovery servers
Arguments: servers<Array>
, resilient<Resilient>
Fired every time that servers cache is updated
Arguments: servers<Array>
, resilient<Resilient>
Fired every time that discovery servers are updated form refresh servers
Performs a custom request with the given options. It's recommended using as generic interface to make multi verb requests
Return Client
Creates a GET request with optional custom options
Return Client
Creates a POST request with optional custom options
Return Client
Return Client
Creates a PUT request with optional custom options
Alias: del
| Return Client
Creates a DELETE request with optional custom options
Return Client
Creates a PATCH request with optional custom options
Return Client
Creates a HEAD request with optional custom options
Getter/setter accessor for resilient options, optionally per type. See supported options
Getter/setter accessor for service-level config options
Getter/setter accessor for discovery-level config options
Return: object
Getter/Setter accessor for balancer-level config options
Return: object
Get a map of HTTP specific options
Alias: failStrategy
Add a custom failure evaluator function
strategy in order to determine if Resilient should handle the request as failed or success status, retrying it accordingly if required.
Strategies should return a boolean
value indicating if the request failed (true
) or not (false
).
Use example:
var resilient = require('resilient')
var client = resilient()
client.addFailStrategy(function limitReached(err, res) {
return !err
&& +res.headers['x-ratelimit-remaining'] === 0
})
Return: boolean
Returns true
if servers are up-to-date. Otherwise false
Return: Servers
Return a Servers
instance with the current used servers per type. Allowed types are: service
and discovery
Return: array<string>
Return an array
of server URLs for the given type. Allowed types are: service
and discovery
Return: Resilient
Alias: resetStats
Reset servers stats score based on network latency and percentage of success and failed requests
This score is the average calculus of the total amount of sent requests from the client to each server.
This score is used in the scheduling algorithm in order
to determinate the best available server (in the case that the balance
option is enabled)
Allowed types are: service
and discovery
Return: Servers
Setter/Getter for discovery servers list
Return: Resilient
Pass to the callback an up-to-date list of servers asking to discovery servers
Passed arguments to the callback are:
object
- Error, if it happendarray
- Array of string
with the current service servers URLReturn: Resilient
Alias: getUpdatedServers
Pass to the callback an up-to-date list of servers, with or without discovery servers configured
Passed arguments to the callback are:
object
- Error, if it happendarray
- Array of string
with the current service servers URLForce to update the servers list from discovery servers, if they are defined, optionally passing a callback to handle the result
Passed arguments to the callback are:
object
- Error, if it happendarray
- Array of string
with the current service servers URLRegister a new middleware. See the middleware documentation or examples for more information
Use a custom HTTP client as proxy instead of the embedded resilient
native HTTP client.
Useful to define use proxy for custom frameworks or libraries in your existent project when you need to deal with some complex HTTP pre/post hooks logic and exploit custom HTTP client features
If defined, all the outgoing requests through Resilient client will be proxied to it.
Arguments passed to the client function:
object
- Resilient HTTP service optionsfunction
- Request status handler. Expected arguments are: error
, response
Note: error
and response
objects must be compatible with the current interface
Restore the native resilient
HTTP client
Define a mock/fake HTTP client error/response object
for all outgoing requests
resilient.mock(function (options, cb) {
if (options.url === 'http://discovery.server.me') {
// fake response
cb(null, { status: 200, data: ['http://server.net'] })
} else {
// fake unavailable status
cb(null, { status: 503 })
}
})
See also the useHttpClient()
method for custom request proxy forward, also useful for testing with stubs/fakes
Disable the mock/fake mode
Subscribe to an event. See supported events
Unsubscribe a given event and its handler. See supported events
Subscribe to an event with a given handler just once time. After fired, the handler will be removed
See supported events
Force to flush servers cache
Return: Client
Alias: http
Returns an HTTP client-only interface. Useful to provide encapsulation from public usage and avoid resilient-specific configuration methods to be called from the public API.
This is a restricted API useful to provide for high-level developers
Type: string
Current semver library version
Type: string
Current semver HTTP client library version
It uses request in node.js and lil-http in the browser
Type: object
Default config options
Create a new options store
Creates a new resilient HTTP client with public API
Useful to provide encapsulation to the resilient API and expose only the HTTP client (the common interface the developers want to consum)
Use the plain HTTP client (request in node.js and lil-http in the browser)
Definitely not. Discovery servers only will be used in the case that you configure them in your Resilient client. In that case Resilient will simply use the the static service servers to communicate with your backend
Yes. If your perform a request with a full URI schema, Resilient will treat it as plain request without applying any internal logic:
var client = Resilient({
service: {
servers: ['http://server1.me', 'http://server2.me']
}
})
// direct plain request (no balancing, no discovery, no fallback...)
client.get('http//custom.server/hello', function (err, res) {
// ...
})
// resilient powered request (with balancing, fallback, discovery server, cache...)
client.get('/hello', function (err, res) {
// ...
})
Of course you can do it. In browser environments this is a common premise, for example you need to use the custom HTTP client of the framework you are using in your application, or a custom library like zepto or jQuery that provides a simple AJAX interface
You can do that defining a function middleware to act as proxy pattern to intercept and wrap all the HTTP traffic via the Resilient client
var client = Resilient({})
// example using Zepto.js AJAX interface
client.useHttpClient(function httpProxy(options, cb) {
options.success = function (data, status, xhr) {
cb(null, { status: xhr.status, data: data, xhr: xhr })
}
options.error = function (xhr) {
cb({ status: xhr.status, xhr: xhr })
}
$.ajax(options)
})
For more information, see the API method documentation
Not yet. There are plans to support it in future versions.
Resilient was used in both web and node.js production applications.
The library is, indeed, relatively young and it will evolve with new features in future versions (in fact a full core and logic redesign is required), but the API consistency in not compromised between patch minor releases.
You can see the middleware documentation or see an example
Wanna help? Cool! It will be appreciated :)
You must add new test cases for any new feature or refactor you do, always following the same design/code patterns that already exist
Only node.js is required for development
Clone the repository
$ git clone https://github.com/resilient-http/resilient.js.git && cd resilient.js
Install development dependencies
$ npm install
Install browser dependencies
$ bower install
Generate browser bundle source
$ make browser
Run tests (in both node.js and headless browser)
$ make test
Run tests in real browsers
$ make test-browser
MIT © Tomas Aparicio and contributors