good-guy-http
Good guy HTTP is an HTTP client library based on the request module, adding the following stuff on top:
- easy promise-based interface
- caching GET and other idempotent requests, either in-memory or using your chosen cache
- this automatically obeys 'Cache-control' headers, but you can provide defaults for when it's missing
- retrying failed requests
- collapsing identical requests made at the same time into one
- reporting HTTP error statuses as errors (promise rejections)
- sane but strict defaults regarding timeouts etc.
- implementation of the circuit breaker pattern
- optional postprocessing of response to cache expensive parsing/munging operations
- supports everything request supports by passing all the options to it
All of this is optional and you can opt-out of some or all of it.
Usage
var goodGuy = require('good-guy-http')();
goodGuy('http://news.ycombinator.com').then(function(response) {
console.log(response.body);
});
That's the basics. If you want to change the default behaviour, pass a configuration object:
var goodGuy = require('good-guy-http')({
maxRetries: 2,
collapseIdenticalRequests: true,
allowServingStale: true,
cache: ...,
cacheResponseTimeout: 500
maxResponseSize: 1024*1024
errorLogger: console.error,
postprocess: false,
usePromise: Promise,
defaultCaching: {
cached: true,
timeToLive: 5000,
mustRevalidate: false
},
forceCaching: {...},
clientErrorCaching: {
cached: true,
timeToLive: 60000,
mustRevalidate: false
},
circuitBreaking: {
errorThreshold: 50
},
idempotent: ...
});
You can also pass options to the request
module through this configuration object. Any options good guy doesn't
recognize will be used to configure the underlying request
object:
var goodGuy = require('good-guy-http')({
timeout: 100
});
Good guy objects can also be reconfigured on the fly by adding good guy options to the request:
goodGuy({url: 'http://extremely-flaky-server.org', maxRetries: 10}).then(...);
The goodguy interface
Mirrors what request
does almost exactly. Any object that the request
module can handle can also be passed to good-guy-http
.
All options will be passed onto request. The get
, post
, etc. convenience methods are also present.
All functions support both a promise-based interface (when no callback is passed) and a traditional callback-based one
(when a callback function is passed as the second parameter).
The response object that you will receive will not be a http.IncomingMessage, since those are difficult to cache. Instead, you will get a plain old object with statusCode
, headers
, body
and httpVersion
in all the same places they would be in normal responses.
Caches
Any object that has these methods can be used as a cache:
store(key, object)
- returning a promise that resolves when the object is storedretrieve(key)
- returning a promise that resolves with the previously stored object, or undefined if no object is foundevict(key)
- returning a promise that resolves when the object is evicted from cache
By default, an in-memory cache limited to the 500 most recently used requests is used, but you can easily override this:
var goodGuyLib = require('good-guy-http');
var goodGuy = goodGuyLib({cache: goodGuyLib.inMemoryCache(10)});
var goodGuy = goodGuyLib({cache: false});
var goodGuy = goodGuyLib({cache: customCache});
Cache modules
Only idempotent requests are cached
Idempotence
By default only HEAD, GET and OPTIONS request are treated as idempotent.
That means only requests mentioned above could be cached or retried as they, in short terms, do not modify state
(for full explanation of idempotence follow the wikipedia description on the topic).
That behaviour could be changed using idempotent
key in options.
For some services does that are not REST-ish (e.g. RPC) marking request as idempotent is quite useful.
For example this way failed ElasticSearch query will be retried up to 5 times:
goodGuy({
url: 'http://elasticsearch-server.io/_search',
method: 'POST',
maxRetries: 5,
idempotent: true,
json: true,
body: {
query: {
match_all: {}
}
}
})
.then(...);
Circuit breaker
To avoid overloading external services that are having trouble coping with the load, good-guy-http uses a circuit breaker
(based on Yammer's circuit-breaker-js) by default. Each host is treated as a separate service and
has a separate circuit breaker.
Once the breaker trips (too many failures), your requests will start failing with a CircuitBrokenError. It can be easily
identified by having the code
property set to ECIRCUIT
. Once the situation improves, requests will start going
through normally again.
You can configure the error threshold or turn the whole feature off:
var goodGuyLib = require('good-guy-http');
var goodGuy = goodGuyLib({
circuitBreaking: { errorThreshold: 75 }
});
var goodGuy = goodGuyLib({circuitBreaking: false});