@gis-ag/oniyi-http-client

Adding a plugin interface to "request" that allows modifications of request parameters and response data

Downloads in past

Stats

StarsIssuesVersionUpdatedCreatedSize
@gis-ag/oniyi-http-client
2.1.06 years ago6 years agoMinified + gzip package size for @gis-ag/oniyi-http-client in KB

Readme

This is a fork from oniyi-http-client
Oniyi Http Client
Adding a plugin interface to request that allows modifications of request parameters and response data

Installation

$ npm install --save @gis-ag/oniyi-http-client

Usage

Use default instance
const httpClient = require('@gis-ag/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('@gis-ag/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('@gis-ag/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 ```js const httpClient = require('@gis-ag/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](#PluginHookHandler).
The received context argument is an [OnRequestContext](#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](#PluginHookHandler).
The received context argument is an [OnResponseContext](#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.

```js
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('@gis-ag/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. | | complete list of phase names for the onRequest phaseList. must include the names initial and final | | options.responsePhases | Array. | | complete list of phase names for the onResponse phaseList. must include the names initial and final |
Example (with request defaults)
const httpClient = require('@gis-ag/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('@gis-ag/oniyi-http-client');
const customClient = httpClient.create({
 requestPhases: ['early', 'initial', 'middle', 'final'],
 responsePhases: ['initial', 'middle', 'final', 'end'],
});

HttpClient

Kind: global class
* [.#defaults()](#markdown-header-httpclientdefaults-object) ⇒ Object
* [.#jar([store])](#markdown-header-httpclientjarstore-object) ⇒ Object
* [.#use(plugin, [options])](#markdown-header-httpclientuseplugin-options-object) ⇒ Object
* [.#makeRequest(uri, [options], [callback])](#markdown-header-httpclientmakerequesturi-options-callback-requestpromise) ⇒ RequestPromise
* [.#get(uri, [options], [callback])](#markdown-header-httpclientgeturi-options-callback-requestpromise) ⇒ RequestPromise
* [.#put(uri, [options], [callback])](#markdown-header-httpclientputuri-options-callback-requestpromise) ⇒ RequestPromise
* [.#post(uri, [options], [callback])](#markdown-header-httpclientposturi-options-callback-requestpromise) ⇒ RequestPromise
* [.#del(uri, [options], [callback])](#markdown-header-httpclientdeluri-options-callback-requestpromise) ⇒ RequestPromise
* [.#head(uri, [options], [callback])](#markdown-header-httpclientheaduri-options-callback-requestpromise) ⇒ RequestPromise
* [.#options(uri, [options], [callback])](#markdown-header-httpclientoptionsuri-options-callback-requestpromise) ⇒ 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
| Param | Type | Description | | --- | --- | --- | | store | Object | tough-cookie Store |

httpClient.#use(plugin, options) ⇒ Object

Kind: instance method of HttpClient
Returns: Object - a clone of this instance's defaults object
| Param | Type | | --- | --- | | plugin | Object | | plugin.name | String | | plugin.onRequest | Array. | | plugin.onResponse | Array. | | 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()