Oniyi Http Client
Adding a plugin interface to request that allows modifications of request parameters and response data
Installation
$ npm install --save oniyi-http-client
Usage
Use default instance
const httpClient = require('oniyi-http-client');
httpClient.get('http://httpbin.org/headers', {}, (err, response, body) => {
if (err) {
return;
}
});
with request defaults
const httpClient = require('oniyi-http-client');
const customClient = httpClient.create({
defaults: {
headers: {
'custom': 'foo',
},
json: true,
}
});
customClient.get('http://httpbin.org/headers', {}, (err, response, body) => {
if (err) {
return;
}
console.log(body.headers.custom)
});
with custom phases
const httpClient = require('oniyi-http-client');
const customClient = httpClient.create({
requestPhases: ['early', 'initial', 'middle', 'final'],
responsePhases: ['initial', 'middle', 'final', 'end'],
});
Motivation
"Is there really a need for another http library?" you might ask. There isn't. The actual need is for
the ability to asynchronously hook into the process of making a http request or receiving a response.
I came across this requirement when working with various REST APIs, making requests with a number of
different credentials (representing users logged into my application). Since the flow within my app
provided me with an user
object that has an async method to retrieve this user's credentials
(e.g. an oauth access-token), I wanted to follow the DRY
(don't repeat yourself) pattern and not
manually resolve before invoking e.g. request
.
Instead I thought it would be much easier to pass the user along with the request options and have
some other module take care of resolving and injecting credentials.
Quickly more use-cases come to mind:
Also, use-cases that require to manipulate some options based on other options (maybe even compiled by
another plugin) can be solved by this phased implementation. Some REST APIs change the resource path
depending on the type of credentials being used. E.g. when using BASIC credentials, a path might be
/api/basic/foo
while when using oauth
the path changes to /api/oauth/foo
. This can be accomplished
by using e.g. oniyi-http-plugin-format-url-template
in a late phase (final
) of the onRequest PhaseLists.
Phases
This HTTP Client supports running multiple plugins / hooks in different phases before making a request
as well as after receiving a response. Both PhaseLists
are initiated with the phases initial
and final
and zipMerged
with params.requestPhases
and params.responsePhases
respectively. That means you can add more phases
by providing them in the factory params.
with custom phases
const httpClient = require('oniyi-http-client');
const customClient = httpClient.create({
requestPhases: ['early', 'initial', 'middle', 'final'],
responsePhases: ['initial', 'middle', 'final', 'end'],
});
onRequest
onRequest
is one of the (currently) two hooks that executes registered plugins in the defined phases.
After all phases have run their handlers successfully, the resulting request options from ctx.options
are used to initiate a new request.Request
. The return value from request.Request
(a readable and
writable stream) is what the returned Promise
from any of the request initiating methods from client
(makeRequest
, get
, put
, post
, ...) resolves to.
Handlers in this phaseList must comply with PluginHookHandler.
The received context argument is an OnRequestContext .
onResponse
onResponse
is the second hook and executes registered plugins after receiving the response from request
but before invoking callback
from the request execution. That means plugins using this hook / phases can
work with and modify err, response, body
before the app's callback
function is invoked. Here you can do
things like validating response's statusCode
, parsing response data (e.g. xml to json), caching, reading
set-cookie
headers and persist in async cookie jars... the possibilities are wide.
Handlers in this phaseList must comply with PluginHookHandler.
The received context argument is an OnResponseContext.
Using plugins
Every plugin can register any number of handlers for any of the phases available onRequest
as well as onResponse
.
The following example creates a plugin named plugin-2
which adds a request-header with name and value plugin-2
.
Also, it stores some data in shared state that is re-read on response and printed.
const plugin2 = {
name: 'plugin-2',
onRequest: [{
phaseName: 'initial',
handler: (ctx, next) => {
const { options, hookState } = ctx;
_.set(hookState, 'plugin-2.name', 'Bam Bam!');
setTimeout(() => {
_.set(options, 'headers.plugin-2', 'plugin-2');
next();
}, 500);
},
}],
onResponse: [{
phaseName: 'final',
handler: (ctx, next) => {
const { hookState } = ctx;
const name = _.get(hookState, 'plugin-2.name');
setTimeout(() => {
logger.info('Name in this plugin\'s store: %s', name);
next();
}, 500);
},
}],
};
client
.use(plugin2)
.get('http://httpbin.org/headers', (err, response, body) => {
if (err) {
logger.warn('got an error');
if (err.stack) {
logger.error(err.stack);
} else {
logger.error(err);
}
process.exit(0);
}
if (response) {
logger.debug('statusCode: %d', response.statusCode);
logger.debug('headers: ', response.headers);
logger.debug('body: ', body);
}
process.exit(0);
});
API
The default HttpClient instance. Can be used without any further configuration
Example (Use default instance)
const httpClient = require('oniyi-http-client');
httpClient.get('http://httpbin.org/headers', {}, (err, response, body) => {
if (err) {
return;
}
});
oniyi-http-client~create([options]) ⇒ HttpClient
Create a new HttpClient instance.
Use this method to create your own client instances and mount plugins for
your specific request scenarios
Kind: inner method of oniyi-http-client
Returns: HttpClient
- The newly created HttpClient instance
Param | Type | Default | Description |
---|
[options] | Object | {} | |
[options.defaults] | Object | | default request options for the new instance. Will be merged into options provided with each request via _.defaultsDeep() |
[options.requestPhases] | Array.<String> | | complete list of phase names for the onRequest phaseList. must include the names initial and final |
[options.responsePhases] | Array.<String> | | complete list of phase names for the onResponse phaseList. must include the names initial and final |
Example (with request defaults)
const httpClient = require('oniyi-http-client');
const customClient = httpClient.create({
defaults: {
headers: {
'custom': 'foo',
},
json: true,
}
});
customClient.get('http://httpbin.org/headers', {}, (err, response, body) => {
if (err) {
return;
}
console.log(body.headers.custom)
});
Example (with custom phases)
const httpClient = require('oniyi-http-client');
const customClient = httpClient.create({
requestPhases: ['early', 'initial', 'middle', 'final'],
responsePhases: ['initial', 'middle', 'final', 'end'],
});
HttpClient
Kind: global class
- HttpClient
- .#defaults() ⇒
Object
- .#jar([store]) ⇒
Object
- .#use(plugin, [options]) ⇒
HttpClient
- .#makeRequest(uri, [options], [callback]) ⇒
RequestPromise
- .#get(uri, [options], [callback]) ⇒
RequestPromise
- .#put(uri, [options], [callback]) ⇒
RequestPromise
- .#post(uri, [options], [callback]) ⇒
RequestPromise
- .#del(uri, [options], [callback]) ⇒
RequestPromise
- .#head(uri, [options], [callback]) ⇒
RequestPromise
- .#options(uri, [options], [callback]) ⇒
RequestPromise
httpClient.#defaults() ⇒ Object
Kind: instance method of HttpClient
Returns: Object
- a clone of this instance's defaults object
httpClient.#jar([store]) ⇒ Object
Create a new CookieJar with the provided
Store implementation.
Will use request.jar(store)
method
for creation when store
is not async, tough.CookieJar(store)
instead.
Kind: instance method of HttpClient
Returns: Object
- CookieJar
httpClient.#use(plugin, [options]) ⇒ HttpClient
Kind: instance method of HttpClient
Returns: HttpClient
- this HttpClient instance
httpClient.#makeRequest(uri, [options], [callback]) ⇒ RequestPromise
make a http request with the provided arguments.
Request arguments are parsed and compiled to one options
object, merged with this instance's defaults
.
Then, the onRequest
phaseList is onvoked with mentioned options
as well as a hookState.
After all PluginHookHandler have completed, the options from OnRequestContext are used to invoke
request. The result is used to resolve this method's returned RequestPromise.
This is useful if you want to work with request's' Streaming API.
After a response is received, a OnResponseContext is created and passed through the onResponse
phaseList before finally your provided RequestArgCallback is invoked.
Kind: instance method of HttpClient
httpClient.#get(uri, [options], [callback]) ⇒ RequestPromise
Same as #makeRequest but forces options.method
to GET
Kind: instance method of HttpClient
httpClient.#put(uri, [options], [callback]) ⇒ RequestPromise
Same as #makeRequest but forces options.method
to PUT
Kind: instance method of HttpClient
httpClient.#post(uri, [options], [callback]) ⇒ RequestPromise
Same as #makeRequest but forces options.method
to POST
Kind: instance method of HttpClient
httpClient.#del(uri, [options], [callback]) ⇒ RequestPromise
Same as #makeRequest but forces options.method
to DELETE
Kind: instance method of HttpClient
httpClient.#head(uri, [options], [callback]) ⇒ RequestPromise
Same as #makeRequest but forces options.method
to HEAD
Kind: instance method of HttpClient
httpClient.#options(uri, [options], [callback]) ⇒ RequestPromise
Same as #makeRequest but forces options.method
to OPTIONS
Kind: instance method of HttpClient
Type Definitions
PluginHook : Object
Kind: global typedef
Properties
Name | Type | Description |
---|
phaseName | string | Name of the phase that handler should be executed in. value can include pseudo-phase postfix ':before' or ':after' (e.g. 'initial:after' where 'initial' is the actual phaseName and ':after' the pseudo phase) |
handler | PluginHookHandler | handler function that is invoked when running through the according phase |
PluginHookHandler : function
Kind: global typedef
Param | Type | Description |
---|
context | Object | An object with the currently available request context. Hooks in the onRequest phaseList receive an OnRequestContext while hooks that run in the onResponse phaseList receive an OnResponseContext |
next | function | callback function that must be invoked once the handler function completed it's operations |
Hookstate : Object
A Hookstate instance is created for each request and shared across all phases
in the onRequest
and onResponse
phaseLists. PluginHookHandler can
modify this plain object at will (e.g. persist a timestamp in an onRequest
phase and read it again in another handler in an onResponse
phase)
Kind: global typedef
OnRequestContext : Object
mutable context object that gets passed through all phases in the onRequest
phaseList
Kind: global typedef
Properties
OnResponseContext : Object
mutable context object that gets passed through all phases in the onResponse
phaseList
Kind: global typedef
Properties
RequestArgUri : String
| Object
The first argument can be either a url
or an
options
object. The only required option is uri; all others are optional.
Kind: global typedef
RequestArgOptions : Object
| function
The sesond argument can bei either
options
object or callback
function.
Kind: global typedef
RequestArgCallback : function
Callback function, Invoked at the end of response receiving (or in case of error,
when the error is generated). Receives three arguments (err
, response
, body
)
that are also available in OnResponseContext
Kind: global typedef
RequestPromise : Promise
A Promise that resolves to the return value for request()
after all
PluginHookHandler in the onRequest
phaseList have completed.
If any of the PluginHookHandlers produces an error,
Promise is rejected with that error object.
Kind: global typedef
License
MIT © Benjamin Kroeger