npm package: @henson/diplomat
Description: A node.js HTTP client for accessing external systems and distributed services, with service discovery, auto failover and rate limiting builtin
Latest version: 3.0.0

# Henson Diplomat Library

<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
**Table of Contents**

- [Install](#install)
- [API](#api)
  - [`rateLimiter` - Throttling](#ratelimiter---throttling)
    - [Usage](#usage)
    - [Example](#example)
  - [`request` - Enhanced HTTP client](#request---enhanced-http-client)
    - [Usage](#usage-1)
  - [`rp` - Request Promise](#rp---request-promise)
    - [Usage](#usage-2)
  - [`EndpointConfig` - Endpoint Config Parser](#endpointconfig---endpoint-config-parser)
    - [Usage](#usage-3)

<!-- END doctoc generated TOC please keep comment here to allow auto update -->

## Install

```
yarn add @henson/diplomat
```

## API


### `rateLimiter` - Throttling

Rate limiter runs in either single instance mode or global mode. 

In cluster mode, all instances of the same scope in system share a maximum rate. Redis deployment is required in global mode.

#### Usage

Methods:

- `throttle(options: RatelimiterOptions): Promise<boolean>`

  Returns `false` when request is throttled.

  RateLimiterOptions:

``` typescript

interface RatelimiterOptions {
  readonly scope: string;    // the scope of throttling
  readonly interval: number; // seconds of a rate limit period
  readonly limit: number;    // maximum requests per period
  global?: boolean;
}
```

#### Example

``` typescript
  import { rateLimiter } from '@henson/diplomat';

  // create a RatelimiterOptions.
  const options = {
      scope: 'scope1',
      interval: 1,
      limit: 5,
      global: true
  };

  // Use `throttle` function to check whether rate is exceeded.
  if (await rateLimiter.throttle(options)) {
    // normal processing.
  } else {
    // rate is exceeded. response error.
  }

```

### `request` - Enhanced HTTP client

This API extends https://www.npmjs.com/package/request to support multi-endpoint, rate-limit and service-discovery.


#### Usage

> **WARNING** Streaming is not supported.

Constants:
```typescript
const enum ServiceDiscoverPolicy {
  FIRST,
  RANDOM,
}
```

Interfaces:
```typescript
interface Agents {
  httpAgent?: http.Agent;
  httpsAgent?: https.Agent;
}

interface EgressLogOptions {
  action: string;
  backendNodeType?: string;
  subscriberId?: egresslog.SubscriberId;
}

interface AlarmOptions {
  name: string;
  handler?: (res: RequestResult | undefined) => boolean;
}

interface MetricsOptions {
  name: string;
  tag?: {
    [key: string]: string | number | boolean
  };
}

interface MultiEndpointRequestOptions extends _request.CoreOptions {
  retryTimes?: number;
  retryResponseCode?: number[];
  rateLimit?: rateLimiter.RatelimiterOptions;
  serviceDiscoverPolicy?: ServiceDiscoverPolicy;
  serviceName?: string;
  agents?: Agents;
  egressLogOpts?: EgressLogOptions;
}

interface UriOptions {
  uri: string | Url | Array<string | Url>;
}

interface UrlOptions {
  url: string | Url | Array<string | Url>;
}

type OptionsWithUri = UriOptions & MultiEndpointRequestOptions;
type OptionsWithUrl = UrlOptions & MultiEndpointRequestOptions;
type Options = OptionsWithUri | OptionsWithUrl;

type InternalOptions = OptionsWithUri & OptionsWithUrl;

```

Methods:

- `request(options, callback): void`

  Extended options:

  * `uri || url: string | Url | Array<string | Url>`, fully qualified uris or parsed urls object from `url.parse()`
  * `retryTimes: number`, retry times when the request url can't connected or response code is in `retryResponseCode` array
  * `retryResponseCode: Array<number>`, define the response code to retry the request for one url.
  * `rateLimit - object`, define the throttling options, refer to module `rateLimiter`.
  * `serviceName - string`, service name that is used to discovery and communicate with service automatically.
  * `serviceDiscoverPolicy - ServiceDiscoverPolicy`, policy of the behavior for service discovery, there are 2 options.
    * `ServiceDiscoverPolicy.FIRST`, choose the first service node from the service discovery result to send the request. It is the default behavior.
    * `ServiceDiscoverPolicy.RANDOM`, choose a service node randomly from the service discovery result to send the request.
  * `agents - Agents`, the agents that are used to send HTTP/HTTPS requests and can include 2 optional items.
    * `httpAgent - http.Agent`, the agent that is used to send HTTP requests.
    * `httpsAgent - https.Agent`, the agent that is used to send HTTPS requests.
  * `egressLogOpts - EgressLogOptions`, the options includes neceessary informations of the outgoing API, and if `egressLogOpts` is not `undefined`, `request` will generate egress logs for this outgoing API.
  * `alarm - AlarmOptions`, the options for alarm. *The alarm is based on the number of the function `request` calls.*
    * `name - string`, the alarm name from configuration file.
    * `handler - function`, customized function for alarm handling. The default handler raises alarm when the final http response is error or http status code is 5xx.
  * `metrics - MetricsOptions`, the options for metrics. *The metrics are based on the number of attempts to send.*
    * `name - string`, the unique metrics name in current module.
    * `tags - object`, the customized tags to append. The default metrics tags include `statusCode` and `statusType`. The `statusType` includes `Error`, `Information`, `Success`, `Redirection`, `ClientError` and `ServerError`.

  For original options please reference to: [request parameters](https://github.com/request/request#requestoptions-callback)

- `request.defaults(options)`

  Returns a wrapper around the normal request API that defaults to whatever options you pass to it.

  For example:

  ```typescript
  //requests using baseRequest() will set the 'x-token' header
  const baseRequest = request.defaults({
    headers: {'x-token': 'my-token'}
  });

  //requests using specialRequest() will include the 'x-token' header set in
  //baseRequest and will also include the 'special' header
  const specialRequest = baseRequest.defaults({
    headers: {special: 'special value'}
  })
  ```

- `request.METHOD(options, callback): void`

  These HTTP method convenience functions act just like `request()` but with a default method already set for you:

  - `get`: Defaults to method: "GET".
  - `post`: Defaults to method: "POST".
  - `request.put`: Defaults to method: "PUT".
  - `patch`: Defaults to method: "PATCH".
  - `del` / `delete`: Defaults to method: "DELETE".
  - `head`: Defaults to method: "HEAD".
  - `options`: Defaults to method: "OPTIONS".


```typescript
import { Options, request } from '@henson/diplomat';

// uri is an array in options
// request will retry one by one untill any uri server could connected
// and response code not 502 or 504
request({
  method: 'GET',
  uri: ['http://localhost:1608/v9',
    'http://localhost:1608/v1/msisdn?imsi=IMSI0123456789',
    'http://localhost:1608/v0'],
  retryTimes: 3,
  retryStatusCode: [502, 504],
  timeout: 5000
}, (error: any, response: http.IncomingMessage, body: any) => {
  if(error) {
    console.log('Request failed:' + error.message);
  } else {
    console.log('Request succeed:', response.statusCode, body);
  }
});
```

```typescript
import * as http from 'http';
import * as https from 'https';
import { Options, request } from '@henson/diplomat';

const agentOptions: http.AgentOptions = {
  keepAlive: true,
  maxSockets: 20,
};
const httpAgent = new http.Agent(agentOptions);
const httpsAgent = new https.Agent(agentOptions);
const keepAliveAgents: Agents = {
  httpAgent,
  httpsAgent,
};

// uri is an array in the paramater
// request will retry one by one untill any uri server could connected
// and response code not 502 or 504
// agents can hold both HTTP and HTTPS agents and the right one will be choosen to send the request. 
request(['http://localhost:1608/v9',
  'https://localhost:1608/v1/msisdn?imsi=IMSI0123456789',
  'http://localhost:1608/v0'], {
  method: 'GET',
  retryTimes: 3,
  retryStatusCode: [502, 504],
  agents: keepAliveAgents,
  timeout: 5000
}, (error: any, response: http.IncomingMessage, body: any) => {
  if(error) {
    console.log('Request failed:' + error.message);
  } else {
    console.log('Request succeed:', response.statusCode, body);
  }
});
```

```typescript
import * as http from 'http';
import * as https from 'https';
import { request, Agents, ServiceDiscoverPolicy } from '@henson/diplomat';

const agentOptions: http.AgentOptions = {
  keepAlive: true,
  maxSockets: 20,
};
const httpAgent = new http.Agent(agentOptions);
const httpsAgent = new https.Agent(agentOptions);
const keepAliveAgents: Agents = {
  httpAgent,
  httpsAgent,
};

// uri is a string indicate a path on server
// serviceName is a string indicate a real service in cluster
// if the target service is running under https mode,
// a default CA file, configured by parameter 'palInternalSslCaFile' in ConfigData.json, will be used.
// if ca is assigned explicitly, the default one is ignored.
// if serviceDiscoverPolicy is assigned with ServiceDiscoverPolicy.RANDOM,
// it will choose a service node randomly to send the request.
// agents can hold both HTTP and HTTPS agents and the right one will be choosen to send the request. 
request({
  method: 'DELETE',
  uri: '/spp/token/v2/staletoken',
  serviceName: 'pushtokenmanager',
  ca: fs.readFileSync('/path/to/ca.crt'),
  serviceDiscoverPolicy: ServiceDiscoverPolicy.RANDOM,
  agents: keepAliveAgents,
  timeout: 5000
}, (error: any, response: http.IncomingMessage, body: any) => {
  if(error) {
    console.log('Request failed:' + error.message);
  } else {
    console.log('Request succeed:', response.statusCode, body);
  }
});
```

```typescript
import { request, EgressLogOptions, EndpointConfig, parseEndpointConfig, } from '@henson/diplomat';

const configObj = config.get('test', 'multipleEndpoints'); // configObj can be a multiple-endpoints string or object
const multipleEndpoints: EndpointConfig = parseEndpointConfig(configObj);

const egressLogOpts = {
  action: 'aaa',
  backendNodeType: multipleEndpoints.backendNodeType,
  subscriberId: {
    imsi: '121424141',
  },
};

request(
  uri: multipleEndpoints.UrlList, 
  {
    method: 'GET',
    retryTimes: multipleEndpoints.retryTimes,
    retryStatusCode: multipleEndpoints.retryStatusCode,
    timeout: 5000,
    egressLogOpts,
  }, (error: any, response: http.IncomingMessage, body: any) => {
  if(error) {
    console.log('Request failed:' + error.message);
  } else {
    console.log('Request succeed:', response.statusCode, body);
  }
});

```
### `rp` - Request Promise
This API wrap the request with promise

#### Usage

> **WARNING** Streaming is not supported.

Methods:

- `rp(options): Promise<RequestResponse>`
  options is same with that are passed in request

```typescript
import { RequestOptions, rp, RequestResponse } from '@henson/diplomat';

// uri is an array in options
// request will retry one by one untill any uri server could connected
// and response code not 502 or 504
const endpointNormal: RequestOptions {
  method: 'GET',
  uri: ['http://localhost:1608/v9',
    'http://localhost:1608/v1/msisdn?imsi=IMSI0123456789',
    'http://localhost:1608/v0'],
  retryTimes: 3,
  retryStatusCode: [502, 504],
  timeout: 5000
}

try {
  const res: RequestResponse = await rp(endpointNormal);
  console.log('Request succeed:', res.statusCode, res.headers, res.body);
} catch (error) {
  console.log('Request failed:' + error.message);
}

```

```typescript
import { RequestOptions, rp, RequestResponse } from '@henson/diplomat';

// uri is an array in options
// request will retry one by one untill any uri server could connected
// and response code not 502 or 504
const endpointNormal: RequestOptions {
  uri: ['http://localhost:1608/v9',
    'http://localhost:1608/v1/msisdn?imsi=IMSI0123456789',
    'http://localhost:1608/v0'],
  retryTimes: 3,
  retryStatusCode: [502, 504],
  timeout: 5000
}

try {
  const res: RequestResponse = await rp.get(endpointNormal);
  console.log('Request succeed:', res.statusCode, res.headers, res.body);
} catch (error) {
  console.log('Request failed:' + error.message);
}
```

### `EndpointConfig` - Endpoint Config Parser

It is a util to parse/use endpoint config objects.

#### Usage

Interfaces

```typescript

interface MultipleEndpoints {
  UrlList: string[];
  requestTimeout?: number;
  retryTimes?: number;
  rateLimit?: {
    interval: number;
    limit: number;
  };
}

interface EndpointConfig extends MultipleEndpoints {
  backendNodeType?: string;
  ca?: string;
}

```

Methods

- `parseEndpointConfig(confObj: any): EndpointConfig | undefined`
  Parse a JSON string or an object to a `EndpointConfig` object. 
  If `confObj` is invalid to parse, `undefined` is returned.



```typescript
import { config } from '@henson/common';
import { EndpointConfig, parseEndpointConfig } from '@henson/diplomat';

const configStrOrObj = config.get('configfile', 'dispatchUrl');
const endpointConfig = parseEndpointConfig(configStrOrObj);

```