Skip to content

Commit 93c0ed1

Browse files
committed
removed event emitter, more docs
1 parent bf12317 commit 93c0ed1

File tree

4 files changed

+140
-64
lines changed

4 files changed

+140
-64
lines changed

README.md

Lines changed: 61 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
⚠️ This project is still work in progress, should not be used for anything just yet.
44

5-
Generic http client built on [undici](undici.nodejs.org/) with a circuit breaker, error handling and metrics out of the box.
5+
Generic http client built on [undici] with a circuit breaker using [opossum], error handling and metrics out of the box.
66

77
[![GitHub Actions status](https://github.com/podium-lib/http-client/workflows/Run%20Lint%20and%20Tests/badge.svg)](https://github.com/podium-lib/layout/actions?query=workflow%3A%22Run+Lint+and+Tests%22)
88
[![Known Vulnerabilities](https://snyk.io/test/github/podium-lib/http-client/badge.svg)](https://snyk.io/test/github/podium-lib/http-client)
@@ -39,16 +39,43 @@ const client = new HttpClient(options);
3939

4040
#### options
4141

42-
| option | default | type | required | details |
43-
|------------|---------|-----------|----------|--------------------------------------------------------------------------------------------------------------------------------------------|
44-
| threshold | `null` | `number` | `25` | Circuit breaker: How many, in %, requests should error before the circuit should trip. Ex; when 25% of requests fail, trip the circuit. |
45-
| timeout | `null` | `number` | `500` | Circuit breaker: How long, in milliseconds, a request can maximum take. Requests exceeding this limit counts against tripping the circuit. |
46-
| throwOn400 | `false` | `boolean` | `false` | If the client sahould throw on HTTP 400 errors.If true, HTTP 400 errors will counts against tripping the circuit. |
47-
| throwOn500 | `false` | `boolean` | `true` | If the client sahould throw on HTTP 500 errors.If true, HTTP 500 errors will counts against tripping the circuit. |
48-
| reset | `false` | `number` | `2000` | Circuit breaker: How long, in milliseconds, to wait before a tripped circuit should be reset. |
49-
| logger | `null` | `àb` | `false` | A logger which conform to a log4j interface |
42+
| option | default | type | required | details |
43+
|-----------------------|--------------|------------|----------|--------------------------------------------------------------------------------------------------------------------------------------------|
44+
| connections | `50` | `number` | no | See [connections](#connections) |
45+
| fallback | `undefined` | `function` | no | Function to call when requests fail |
46+
| keepAliveMaxTimeout | `undefined` | `number` | no | See [keepAliveMaxTimeout](#keepAliveMaxTimeout) |
47+
| keepAliveTimeout | `undefined` | `number` | no | See [keepAliveTimeout](#keepAliveTimeout) |
48+
| logger | `undefined ` | `object` | no | A logger which conform to a log4j interface |
49+
| pipelining | `10` | `number` | no | See [pipelining](#pipelining) |
50+
| reset | `2000` | `number` | no | Circuit breaker: How long, in milliseconds, to wait before a tripped circuit should be reset. |
51+
| threshold | `25` | `number` | no | Circuit breaker: How many, in %, requests should error before the circuit should trip. Ex; when 25% of requests fail, trip the circuit. |
52+
| throwOn400 | `false` | `boolean` | no | If the client should throw on HTTP 400 errors.If true, HTTP 400 errors will counts against tripping the circuit. |
53+
| throwOn500 | `true` | `boolean` | no | If the client should throw on HTTP 500 errors.If true, HTTP 500 errors will counts against tripping the circuit. |
54+
| timeout | `500` | `number` | no | Circuit breaker: How long, in milliseconds, a request can maximum take. Requests exceeding this limit counts against tripping the circuit. |
5055

5156

57+
##### connections
58+
Property is sent to the underlying http library.
59+
See library docs on [connections](https://undici.nodejs.org/#/docs/api/Pool?id=parameter-pooloptions)
60+
61+
##### fallback
62+
63+
Optional function to run when a request fails.
64+
65+
```js
66+
// TBA
67+
```
68+
69+
##### keepAliveMaxTimeout
70+
71+
Property is sent to the underlying http library.
72+
See library docs on [keepAliveTimeout](https://undici.nodejs.org/#/docs/api/Client?id=parameter-clientoptions)
73+
74+
##### keepAliveMaxTimeout
75+
76+
Property is sent to the underlying http library.
77+
See library docs on [keepAliveMaxTimeout](https://undici.nodejs.org/#/docs/api/Client?id=parameter-clientoptions)
78+
5279
##### logger
5380

5481
Any log4j compatible logger can be passed in and will be used for logging.
@@ -67,13 +94,35 @@ const layout = new Layout({
6794
Under the hood [abslog] is used to abstract out logging. Please see [abslog] for
6895
further details.
6996

97+
##### pipelining
98+
99+
Property is sent to the underlying http library.
100+
See library docs on [pipelining](https://undici.nodejs.org/#/?id=pipelining)
101+
102+
##### reset
103+
Circuit breaker: How long, in milliseconds, to wait before a tripped circuit should be reset.
104+
105+
##### threshold
106+
107+
Circuit breaker: How many, in %, requests should error before the circuit should trip. Ex; when 25% of requests fail, trip the circuit.
108+
109+
##### timeout
110+
Circuit breaker: How long, in milliseconds, a request can maximum take. Requests exceeding this limit counts against tripping the circuit.
111+
112+
##### throwOn400
113+
114+
If the client should throw on http 400 errors. If true, http 400 errors will count against tripping the circuit.
115+
116+
##### throwOn500
117+
If the client should throw on http 500 errors. If true, http 500 errors will count against tripping the circuit.
118+
119+
70120

71121
## Methods
72122

73-
### async request(options)
74-
### async close()
75-
### fallback()
76123

77124

78125
[@metrics/metric]: https://github.com/metrics-js/metric '@metrics/metric'
79126
[abslog]: https://github.com/trygve-lie/abslog 'abslog'
127+
[undici]: https://undici.nodejs.org/
128+
[opossum]: https://github.com/nodeshift/opossum/

lib/http-client.js

Lines changed: 32 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,53 +2,52 @@ import { Agent, request } from 'undici';
22
import createError from 'http-errors';
33
import Opossum from 'opossum';
44
import abslog from 'abslog';
5-
import EventEmitter from 'node:events';
65

76
/**
87
* @typedef HttpClientOptions
9-
* @property {Number} keepAliveMaxTimeout
10-
* @property {Number} keepAliveTimeout
11-
* @property {Number} connections
12-
* @property {Number} pipelining
13-
* @property {Boolean} throwOn400 - If the client should throw on http 400 errors. If true, http 400 errors will counts against tripping the circuit.
14-
* @property {Boolean} throwOn500 - If the client should throw on http 500 errors. If true, http 500 errors will counts against tripping the circuit.
15-
* @property {Number} threshold - Circuit breaker: How many, in %, requests should error before the circuit should trip. Ex; when 25% of requests fail, trip the circuit.
16-
* @property {Number} timeout - Circuit breaker: How long, in milliseconds, a request can maximum take. Requests exceeding this limit counts against tripping the circuit.
8+
* @property {Number} connections - @see https://undici.nodejs.org/#/docs/api/Pool?id=parameter-pooloptions
9+
* @property {Number} keepAliveMaxTimeout - @see https://undici.nodejs.org/#/docs/api/Client?id=parameter-clientoptions
10+
* @property {Number} keepAliveTimeout - @see https://undici.nodejs.org/#/docs/api/Client?id=parameter-clientoptions
1711
* @property {import('abslog')} logger - A logger instance compatible with abslog .
12+
* @property {Number} pipelining - @see https://undici.nodejs.org/#/?id=pipelining
1813
* @property {Number} reset - Circuit breaker: How long, in milliseconds, to wait before a tripped circuit should be reset.
14+
* @property {Boolean} throwOn400 - If the client should throw on http 400 errors. If true, http 400 errors will count against tripping the circuit.
15+
* @property {Boolean} throwOn500 - If the client should throw on http 500 errors. If true, http 500 errors will count against tripping the circuit.
16+
* @property {Number} threshold - Circuit breaker: How many, in %, requests should error before the circuit should trip. Ex; when 25% of requests fail, trip the circuit.
17+
* @property {Number} timeout - Circuit breaker: How long, in milliseconds, a request can maximum take. Requests exceeding this limit counts against tripping the circuit.
18+
* @property {Function} [fallaback=undefined] - Optional function to call as a fallback when breaker is open.
1919
**/
2020

21-
export default class HttpClient extends EventEmitter {
22-
#throwOn400;
23-
#throwOn500;
21+
export default class HttpClient {
22+
#abortController;
23+
#agent;
2424
#breaker;
2525
#logger;
26-
#agent;
27-
#abortController;
26+
#throwOn400;
27+
#throwOn500;
2828

2929
/**
3030
* @property {HttpClientOptions} options - options
3131
*/
3232
constructor({
3333
abortController = undefined,
3434
autoRenewAbortController = false,
35+
connections = 50,
36+
fallback = undefined,
3537
keepAliveMaxTimeout = undefined,
3638
keepAliveTimeout = undefined,
37-
connections = 50,
39+
logger = undefined,
3840
pipelining = 10,
41+
reset = 20000,
3942
throwOn400 = false,
4043
throwOn500 = true,
4144
threshold = 25,
4245
timeout = 500,
43-
logger = undefined,
44-
reset = 20000,
45-
fallback = undefined,
4646
} = {}) {
47-
super();
4847
this.#logger = abslog(logger);
4948
this.#throwOn400 = throwOn400;
5049
this.#throwOn500 = throwOn500;
51-
// Add runtime check
50+
5251
this.#abortController = abortController;
5352

5453
// TODO; Can we avoid bind here in a nice way?????
@@ -57,9 +56,9 @@ export default class HttpClient extends EventEmitter {
5756
abortController: this.#abortController,
5857
}),
5958
...(autoRenewAbortController && { autoRenewAbortController }),
60-
errorThresholdPercentage: threshold, // When X% of requests fail, trip the circuit
61-
resetTimeout: reset, // After X milliseconds, try again.
62-
timeout, // If our function takes longer than X milliseconds, trigger a failure
59+
errorThresholdPercentage: threshold,
60+
resetTimeout: reset,
61+
timeout,
6362
});
6463

6564
this.#agent = new Agent({
@@ -72,13 +71,6 @@ export default class HttpClient extends EventEmitter {
7271
if (fallback) {
7372
this.fallback(fallback);
7473
}
75-
76-
this.#breaker.on('open', () => {
77-
this.emit('open');
78-
});
79-
this.#breaker.on('close', () => {
80-
this.emit('close');
81-
});
8274
}
8375

8476
async #request(options = {}) {
@@ -113,16 +105,25 @@ export default class HttpClient extends EventEmitter {
113105

114106
/**
115107
* Function called if the request fails.
116-
* @param {Function} func
108+
* @param {import('opossum')} func
117109
*/
118110
fallback(func) {
119111
this.#breaker.fallback(func);
120112
}
121113

114+
/**
115+
* Requests a URL.
116+
* @param {any} [options]
117+
* @returns {Promise<any>}
118+
*/
122119
async request(options = {}) {
123120
return await this.#breaker.fire(options);
124121
}
125122

123+
/**
124+
* Closes the client.
125+
* @returns {Promise<void>}
126+
*/
126127
async close() {
127128
await this.#breaker.close();
128129
if (!this.#agent.destroyed && !this.#agent.closed) {

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,12 @@
4343
"@podium/semantic-release-config": "2.0.0",
4444
"@podium/typescript-config": "1.0.0",
4545
"@types/node": "20.16.10",
46+
"@types/opossum": "8.1.8",
4647
"concurrently": "9.0.1",
4748
"cronometro": "4.0.0",
4849
"eslint": "9.11.1",
49-
"prettier": "3.3.3",
5050
"npm-run-all2": "6.2.6",
51+
"prettier": "3.3.3",
5152
"table": "6.8.2",
5253
"typescript": "5.6.3",
5354
"wait-on": "8.0.1"

tests/http-client.test.js

Lines changed: 45 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,19 @@ async function queryUrl({
3232
path = '/',
3333
url = `http://${host}:${port}`,
3434
loop = 2,
35-
supresErrors = true,
35+
suppressErrors = true,
3636
}) {
37-
for (let i = 0; i < loop; i++) {
37+
const errors = [];
38+
for (let i = 0; i <= loop; i++) {
3839
try {
3940
await client.request({ path, origin: url, method: 'GET' });
4041
} catch (err) {
41-
if (!supresErrors) throw err;
42+
if (!suppressErrors) errors.push(err);
4243
}
4344
}
45+
if (errors.length > 0) {
46+
throw new Error(errors.toString());
47+
}
4448
}
4549
await test('http-client - basics', async (t) => {
4650
const url = `http://${host}:2001`;
@@ -119,34 +123,55 @@ await test('http-client - circuit breaker behaviour', async (t) => {
119123
const url = `http://${host}:${port}`;
120124
await t.test('opens on failure threshold', async () => {
121125
beforeEach();
122-
const invalidUrl = `http://${host}:3013`;
126+
const invalidUrl = `http://${host}asas:3013`;
123127
const client = new HttpClient({ threshold: 50 });
124-
let hasOpened = false;
125-
client.on('open', () => {
126-
hasOpened = true;
127-
});
128-
await queryUrl({ client, url: invalidUrl });
129128

130-
assert.strictEqual(hasOpened, true);
129+
let broken = 0;
130+
for (let i = 0; i < 5; i++) {
131+
try {
132+
await client.request({
133+
path: '/',
134+
origin: invalidUrl,
135+
method: 'GET',
136+
});
137+
} catch (err) {
138+
if (err.code === 'EOPENBREAKER') {
139+
broken++;
140+
}
141+
}
142+
}
143+
assert.strictEqual(
144+
broken,
145+
4,
146+
`breaker open on 4 out of 5 requests, was ${broken}`,
147+
);
131148
await afterEach(client);
132149
});
133150
await t.test('can reset breaker', async () => {
134151
beforeEach();
135-
const invalidUrl = `http://${host}:3013`;
136-
const client = new HttpClient({ threshold: 50, reset: 1 });
137-
await queryUrl({ client, url: invalidUrl });
138-
139-
let hasClosed = false;
140-
client.on('close', () => {
141-
hasClosed = true;
142-
});
143-
await wait();
152+
const invalidUrl = `http://${host}:3023`;
153+
const breakerReset = 10;
154+
const client = new HttpClient({ threshold: 50, reset: breakerReset });
155+
let isOpen = false;
156+
try {
157+
await queryUrl({
158+
client,
159+
loop: 4,
160+
url: invalidUrl,
161+
suppressErrors: false,
162+
});
163+
} catch (err) {
164+
if (err.toString().indexOf('Breaker is open') !== -1) {
165+
isOpen = true;
166+
}
167+
}
168+
assert.strictEqual(isOpen, true, `breaker opened, ${isOpen}`);
169+
await wait(breakerReset + 10); // wait for the breaker to close
144170
const response = await client.request({
145171
path: '/',
146172
origin: url,
147173
method: 'GET',
148174
});
149-
assert.strictEqual(hasClosed, true);
150175
assert.strictEqual(response.statusCode, 200);
151176
await afterEach(client);
152177
});

0 commit comments

Comments
 (0)