benkroeger / oniyi-http-client

Adding a plugin interface to "request" that allows modifications of request parameters and response data.
MIT License
0 stars 1 forks source link

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) {
   // handle error here
   return;
 }
 // do something with response and / or body
});

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) {
   // handle error here
   return;
 }
 console.log(body.headers.custom)
 // will log `foo`
});

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;
      // store something in the state shared across all hooks for this request
      _.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;
      // read value from state again
      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

oniyi-http-client : HttpClient

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) {
   // handle error here
   return;
 }
 // do something with response and / or body
});

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) {
   // handle error here
   return;
 }
 console.log(body.headers.custom)
 // will log `foo`
});

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

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

Param Type Description
[store] Object tough-cookie Store

httpClient.#use(plugin, [options]) ⇒ HttpClient

Kind: instance method of HttpClient
Returns: HttpClient - this HttpClient instance

Param Type
plugin Object
plugin.name String
[plugin.onRequest] Array.<PluginHook>
[plugin.onResponse] Array.<PluginHook>
[options] Object

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

Param Type
uri RequestArgUri
[options] RequestArgOptions
[callback] RequestArgCallback

httpClient.#get(uri, [options], [callback]) ⇒ RequestPromise

Same as #makeRequest but forces options.method to GET

Kind: instance method of HttpClient

Param Type
uri RequestArgUri
[options] RequestArgOptions
[callback] RequestArgCallback

httpClient.#put(uri, [options], [callback]) ⇒ RequestPromise

Same as #makeRequest but forces options.method to PUT

Kind: instance method of HttpClient

Param Type
uri RequestArgUri
[options] RequestArgOptions
[callback] RequestArgCallback

httpClient.#post(uri, [options], [callback]) ⇒ RequestPromise

Same as #makeRequest but forces options.method to POST

Kind: instance method of HttpClient

Param Type
uri RequestArgUri
[options] RequestArgOptions
[callback] RequestArgCallback

httpClient.#del(uri, [options], [callback]) ⇒ RequestPromise

Same as #makeRequest but forces options.method to DELETE

Kind: instance method of HttpClient

Param Type
uri RequestArgUri
[options] RequestArgOptions
[callback] RequestArgCallback

httpClient.#head(uri, [options], [callback]) ⇒ RequestPromise

Same as #makeRequest but forces options.method to HEAD

Kind: instance method of HttpClient

Param Type
uri RequestArgUri
[options] RequestArgOptions
[callback] RequestArgCallback

httpClient.#options(uri, [options], [callback]) ⇒ RequestPromise

Same as #makeRequest but forces options.method to OPTIONS

Kind: instance method of HttpClient

Param Type
uri RequestArgUri
[options] RequestArgOptions
[callback] RequestArgCallback

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

Name Type Description
hookState Hookstate
options Object request options

OnResponseContext : Object

mutable context object that gets passed through all phases in the onResponse phaseList

Kind: global typedef
Properties

Name Type Description
hookState Hookstate this is the Hookstate instance from this request's OnRequestContext
options Object the options property frtom this request's OnRequestContext (request options)
[requestError] Error an error when applicable (usually from (http.ClientRequest) object)
responseBody Object the response body (String or Buffer, or JSON object if the json option is supplied)
response Object an http.IncomingMessage (http.IncomingMessage) object (Response object)

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]()