The module exports one class, Gofer.
In addition it exposes gofer/hub which exports Hub.
config: A config object as described in configurationhub: An instance of Hub
This class can be used directly,
but it's mainly meant to be the base class for individual service clients.
Child classes should add serviceName and serviceVersion to their prototype.
Example:
function MyClient() {
Gofer.apply(this, arguments);
}
MyClient.prototype.serviceName = 'myService';
MyClient.prototype.serviceVersion = require('package.json').version;Add a new option mapper to all instances that are created afterwards. This can also be called on an instance which doesn't have a global effect.
mapFn: An option mapper, see option mappers
Clear the option mapper chain for all instances that are created afterwards. It can also be called on an instance which does not have a global effect.
Registers "endpoints". Endpoints are convenience methods for easier
construction of API calls and can also improve logging/tracing. The following
conditions are to be met by endpointMap:
- It maps a string identifier that is a valid property name to a function
- The function takes one argument which is
request requestworks likegofer.requestonly that it's aware of endpoint defaults
Whatever the function returns will be available as a property on instances of the Gofer class. Reasonable variants are a function or a nested objects with functions.
MyService.registerEndpoints({
simple: function(request) {
return function(cb) {
return request('/some-path', cb);
};
},
complex: function(request) {
return {
foo: function(qs, cb) {
return request('/foo', { qs: qs }, cb);
},
bar: function(entity, cb) {
return request('/bar', { json: entity, method: 'PUT' }, cb);
}
}
}
});
var my = new MyService();
my.simple(function(err, body) {});
my.complex.foo({ limit: 1 }, function(err, body) {});
my.complex.bar({ name: 'Jordan', friends: 231 }, function(err, body) {});Creates a new instance with the exact same settings and referring to the same hub.
Returns a copy with overrideConfig merged into both the endpoint- and the
service-level defaults.
Useful if you know that you'll need custom timeouts for this one call or you want to add an accessToken.
uri: A convenient way to specifyoptions.uridirectlyoptions: Anything listed below under optionscb: A callback function that receives the following arguments:error: An instance ofErrorbody: The, in most cases parsed, response bodymeta: Stats about the requestresponse: The response object with headers and statusCode
The return value is the same as the one of request.
If an HTTP status code outside of the accepted range is returned, the error object will have the following properties:
type: 'api_response_error'httpHeaders: The headers of the responsebody: The, in most cases parsed, response bodystatusCode: The http status codeminStatusCode: The lower bound of accepted status codesmaxStatusCode: The upper bound of accepted status codes
The accepted range of status codes is part of the configuration. It defaults to accepting 2xx codes only.
If there's an error that prevents any response from being returned,
you can look for code to find out what happened.
Possible values include:
ECONNECTTIMEDOUT: It took longer thanoptions.connectTimeoutallowed to establish a connectionETIMEDOUT: Request took longer thanoptions.timeoutallowedESOCKETTIMEDOUT: Same asETIMEDOUTbut signifies that headers were receivedEPIPE: Writing to the request failedECONNREFUSED: The remote host refused the connection, e.g. because nothing was listening on the portENOTFOUND: The hostname failed to resolveECONNRESET: The remote host dropped the connection. E.g. you are talking to another node based service and a process died.
Takes options.uri, discards everything but the pathname and appends it to the specified baseUrl.
applyBaseUrl('http://api.example.com/v2', { uri: '/foo' })
=== 'http://api.example.com/v2/foo';
applyBaseUrl('http://api.example.com/v2', { uri: '/foo?x=y' })
=== 'http://api.example.com/v2/foo?x=y';
applyBaseUrl('http://api.example.com/v2', { uri: { pathname: '/zapp' } })
=== 'http://api.example.com/v2/zapp';Convenience methods to make requests with the specified http method.
Just lowercase the http verb and you've got the method name,
only exception is gofer.del to avoid collision with the delete keyword.
All service-specific behavior is implemented using option mappers.
Whenever an request is made, either via an endpoint or directly via gofer.request,
the options go through the following steps:
- The endpoint defaults are applied if the request was made through an endpoint
options.serviceNameandoptions.serviceVersionis addedoptions.methodNameandoptions.endpointNameis added. The former defaults to the http verb but can be set to a custom value (e.g.addFriend). The latter is only set if the request was made through an endpoint method- The service-specific and global defaults are applied
- For every registered option mapper
mtheoptionsare set tom(options) - A
User-Agentheader is added if not present already nullandundefinedvalues are removed fromqs. If you want to pass empty values, you should use an empty string
Step 6 implies that every option mapper is a function that takes one argument options and returns transformed options.
Inside of the mapper this refers to the gofer instance.
The example contains an option mapper that handles access tokens and a default base url.
By default every gofer class starts of with one option mapper.
It just calls gofer.applyBaseUrl if options.baseUrl is passed in.
In addition to the options mentioned in the request docs, gofer offers the following options:
connectTimeout: How long to wait until a connection is establishedbaseUrl: SeeapplyBaseUrlaboveparseJSON: Thejsonoption offered by request itself will silently ignore when parsing the body fails. This option on the other hand will forward parse errors. It defaults to true if the response has a json content-type and is non-emptyminStatusCode: The lowest http status code that is acceptable. Everything below will be treated as an error. Defaults to200maxStatusCode: The highest http status code that is acceptable. Everything above will be treated as an error. Defaults to299requestId: Useful to track request through across services. It will added as anX-Request-IDheader. See events and logging belowserviceName: The name of the service that is talked to, e.g. "github". Used in the user-agentserviceVersion: By convention the client version. Used in the user-agentpathParams: If youruriincludes{tags}they will be matched by this object. You can use this instead of string manipulation as this object is also logged
In addition the following options are added that are useful for instrumentation but do not affect the actual HTTP request:
endpointName: The name of the "endpoint" or part of the API, e.g. "repos"methodName: Either just an http verb or something more specific like "repoByName". Defaults to the http verb (options.method)
All parts of the configuration end up as options. There are three levels of configuration:
globalDefaults: Used for calls to any service[serviceName].*: Only used for calls to one specific service[serviceName].endpointDefaults[endpointName].*: Only used for calls using a specific endpoint
var Gofer = require('gofer');
var util = require('util');
var config = {
"globalDefaults": { "timeout": 100, "connectTimeout": 55 },
"a": { "timeout": 1001 },
"b": {
"timeout": 99,
"connectTimeout": 70,
"endpointDefaults": {
"x": { "timeout": 23 }
}
}
};
function GoferA() { Gofer.apply(this, arguments); }
util.inherits(GoferA, Gofer);
function GoferB() { Gofer.apply(this, arguments); }
util.inherits(GoferB, Gofer);
GoferB.prototype.registerEndpoints({
x: function(request) {
return function(cb) { return request('/something', cb); }
}
});
var a = new GoferA(config), b = new GoferB(config);
a.request('/something'); // will use timeout: 1001, connectTimeout: 55
b.request('/something'); // will use timeout: 99, connectTimeout: 70
b.x(); // will use timeout: 23, connectTimeout: 70Every gofer instance has a reference to a "hub".
The hub is used to make all calls to request and exposes a number of useful events.
The following snippet shows how to share a hub across multiple gofer instances:
var GoferA = require('a-gofer'); // client for service "a"
var GoferB = require('b-gofer'); // client for service "b"
var hub = require('gofer/hub')(); // create a new hub
var goferA = new GoferA({ /* config */ }, hub);
var goferB = new GoferB({ /* config */ }, hub);
hub.on('success', function() {}); // this will fire for every successful
// request using either goferA or goferBThere are a couple of things gofer does that are opinionated but may make your life easier.
- It assumes you are using
x-request-idheaders. These can be very useful when tracing a request through multiple levels in the stack. Heroku has a nice description. - It uses unique
x-fetch-idheaders for each http request. - All timings are reported in seconds with microtime precision.
- Data added to
options.logDatawill be added tostart,fetchError,successandfailuremessages.
A service call is attempted. Event data:
{ fetchStart: Float, // time in seconds
requestOptions: options, // options passed into request
requestId: UUID, // id of the overall transaction
fetchId: UUID, // id of this specific http call
uri: String, // the URI requested
method: String, // uppercase HTTP verb, PUT/GET/...
serviceName: String, // Config key ex: github
endpointName: String, // Function name ex: repos
methodName: String, // Defaults to lowercase HTTP verb but configurable per request
pathParams: Object } // key/value pairs used in {tag} replacementWaiting for a socket. See http.globalAgent.maxSockets.
Event data:
{ maxSockets: http.globalAgent.maxSockets,
queueReport: Array[String] } // each entry contains "<host>: <queueLength>"Connected to the remote host, transfer may start.
Event contains the data from start plus:
{ connectDuration: Float } // time in seconds to establish a connectionAll went well. Event data:
{ statusCode: Integer, // the http status code
uri: String,
method: String, // uppercase http verb, PUT/GET/...
connectDuration: Float,
fetchDuration: Float,
requestId: UUID,
fetchId: UUID,
serviceName: String,
endpointName: String,
pathParams: Object }A transport error occured (e.g. timeouts).
Event contains the data from success plus:
{ statusCode: String, // the error code (e.g. ETIMEDOUT)
syscall: String, // the syscall that failed (e.g. getaddrinfo)
error: error, // the raw error object
serviceName: String,
endpointName: String,
pathParams: Object }An invalid status code was returned.
Event contains the data from success.