resilient-http / resilient.js

Fault tolerant and reactive HTTP client for node.js and browsers
184 stars 13 forks source link
backoff balancer balancing consul discovery failover http-client javascript nodejs retry

resilient.js Build Status Code Climate NPM Downloads js-standard-style

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.

Contents

Features

Installation

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>

Environments

Runs in any ES5 compliant engine.

Node.js Chrome Firefox IE Opera Safari
+4 +33 +29 +10 +19 +7.1

Middleware

Framework-specific adapters

Related projects

How to use?

See the basic usage and examples for detailed use cases

How does it work?

The following graph represents from a high-level point of view the internal logic encapsulated in Resilient HTTP client.

Basic usage

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')

Static servers

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)
  }
})

Dynamic servers lookup

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

Middleware Layer

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.

Types of middleware

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.

Middleware API

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)
})

Command-line interface

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

API

resilient([ options ])

Creates a new resilient client with custom config

Options

The options object supports three different configuration levels

Resilient({
  service: { ... },
  balancer: { ... },
  discovery: { ... }
})

Service

Specific configuration options for the end service servers pool of the Resilient client.

Specific shared configuration options for the HTTP client for final service requests

Browser specific options

Node.js specific options

See all HTTP options supported for node.js here

Balancer

Discovery

Specific configuration for discovery servers requests, behavior and logic

Specific shared configuration options for the HTTP client for discovering processes

For node.js, see all HTTP options supported here

Request callback arguments

Response

Browser
Node.js

See http.IncomingMessage

Error

It could be an Error or plain Object instance with the following members

Built-in error codes

Events

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)
})
request:start

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

request:outgoing

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

request:incoming

Arguments: error<Error>, response<Object|http.IncomingMessage>, options<Object>, resilient<Resilient>

Fired every time a HTTP response is received from a server

request:finish

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

request:retry

Arguments: options<Object>, servers<Servers>

Fired when a request performs a retry attempt cycle, that means all the previous requests has failed

request:fallback

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

servers:refresh

Arguments: servers<Array>, resilient<Resilient>

Fired every time that service servers list is updated from discovery servers

servers:cache

Arguments: servers<Array>, resilient<Resilient>

Fired every time that servers cache is updated

discovery:refresh

Arguments: servers<Array>, resilient<Resilient>

Fired every time that discovery servers are updated form refresh servers

Methods

resilient#send(path, options, callback)

Performs a custom request with the given options. It's recommended using as generic interface to make multi verb requests

resilient#get(path, options, callback)

Return Client

Creates a GET request with optional custom options

resilient#post(path, options, callback)

Return Client

Creates a POST request with optional custom options Return Client

resilient#put(path, options, callback)

Return Client

Creates a PUT request with optional custom options

resilient#delete(path, options, callback)

Alias: del | Return Client

Creates a DELETE request with optional custom options

resilient#patch(path, options, callback)

Return Client

Creates a PATCH request with optional custom options

resilient#head(path, options, callback)

Return Client

Creates a HEAD request with optional custom options

resilient#options([ type|options, options ])

Getter/setter accessor for resilient options, optionally per type. See supported options

resilient#serviceOptions([ options ])

Getter/setter accessor for service-level config options

resilient#discoveryOptions([ options ])

Getter/setter accessor for discovery-level config options

resilient#balancer([ options ])

Return: object

Getter/Setter accessor for balancer-level config options

resilient#httpOptions(type)

Return: object

Get a map of HTTP specific options

resilient#addFailStrategy(strategy)

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
})

resilient#areServersUpdated()

Return: boolean

Returns true if servers are up-to-date. Otherwise false

resilient#servers([ type = 'service' ])

Return: Servers

Return a Servers instance with the current used servers per type. Allowed types are: service and discovery

resilient#serversURL([ type = 'service' ])

Return: array<string>

Return an array of server URLs for the given type. Allowed types are: service and discovery

resilient#resetScore([ type = 'service' ])

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

resilient#discoveryServers([ servers ])

Return: Servers

Setter/Getter for discovery servers list

resilient#discoverServers([ options, ] cb)

Return: Resilient

Pass to the callback an up-to-date list of servers asking to discovery servers

Passed arguments to the callback are:

resilient#latestServers([ options, ] cb)

Return: 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:

resilient#updateServers([ options, ] cb)

Force 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:

resilient#use(middleware)

Register a new middleware. See the middleware documentation or examples for more information

resilient#useHttpClient(fn)

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:

Note: error and response objects must be compatible with the current interface

resilient#restoreHttpClient()

Restore the native resilient HTTP client

resilient#mock(mockFn)

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

resilient#unmock()

Disable the mock/fake mode

resilient#on(event, handler)

Subscribe to an event. See supported events

resilient#off(event, handler)

Unsubscribe a given event and its handler. See supported events

resilient#once(event, handler)

Subscribe to an event with a given handler just once time. After fired, the handler will be removed

See supported events

resilient#cache.flush()

Force to flush servers cache

resilient#client()

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

resilient.VERSION

Type: string

Current semver library version

resilient.CLIENT_VERSION

Type: string

Current semver HTTP client library version

It uses request in node.js and lil-http in the browser

resilient.defaults

Type: object

Default config options

resilient.Options(options)

Create a new options store

resilient.Client(resilient)

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)

resilient.request(options [, cb])

Use the plain HTTP client (request in node.js and lil-http in the browser)

FAQ

It's required to have discovery servers in my infraestructure in order to use Resilient?

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

Can I use Resilient as a simple HTTP client without balancing?

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) {
  // ...
})

Can I use a custom HTTP client instead of the embedded one?

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

Can I use streams?

Not yet. There are plans to support it in future versions.

Can I use Resilient in production projects?

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.

How can I create custom middleware?

You can see the middleware documentation or see an example

Contributing

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

Development

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

License

MIT © Tomas Aparicio and contributors