chargebee 3.8.0 → 3.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/README.md +66 -0
- package/cjs/RequestWrapper.js +118 -54
- package/cjs/createChargebee.js +1 -0
- package/cjs/environment.js +1 -1
- package/cjs/util.js +61 -36
- package/esm/RequestWrapper.js +119 -55
- package/esm/createChargebee.js +1 -0
- package/esm/environment.js +1 -1
- package/esm/util.js +59 -36
- package/package.json +1 -1
- package/types/index.d.ts +28 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
### v3.10.0 (2025-06-30)
|
|
2
|
+
* * *
|
|
3
|
+
|
|
4
|
+
### New Features
|
|
5
|
+
* Added `userAgentSuffix` to the environment config to allow appending custom text to the `User-Agent` header.
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### v3.9.0 (2025-06-23)
|
|
9
|
+
* * *
|
|
10
|
+
|
|
11
|
+
### New Features
|
|
12
|
+
* Added built-in retry support for handling transient errors and rate-limiting.
|
|
13
|
+
* Retries are now supported for HTTP status codes 500, 502, 503, 504, and 429.
|
|
14
|
+
* Includes exponential backoff for server errors and Retry-After respect for rate limits (429).
|
|
15
|
+
* Configurable via retryConfig during SDK initialization.
|
|
16
|
+
|
|
1
17
|
### v3.8.0 (2025-06-19)
|
|
2
18
|
* * *
|
|
3
19
|
|
package/README.md
CHANGED
|
@@ -143,6 +143,72 @@ const chargebeeSiteEU = new Chargebee({
|
|
|
143
143
|
|
|
144
144
|
An attribute `api_version` is added to the [Event](https://apidocs.chargebee.com/docs/api/events) resource, which indicates the API version based on which the event content is structured. In your webhook servers, ensure this `api_version` is the same as the [API version](https://apidocs.chargebee.com/docs/api#versions) used by your webhook server's client library.
|
|
145
145
|
|
|
146
|
+
### Retry Handling
|
|
147
|
+
|
|
148
|
+
Chargebee's SDK includes built-in retry logic to handle temporary network issues and server-side errors. This feature is **disabled by default** but can be **enabled when needed**.
|
|
149
|
+
|
|
150
|
+
#### Key features include:
|
|
151
|
+
|
|
152
|
+
- **Automatic retries for specific HTTP status codes**: Retries are automatically triggered for status codes `500`, `502`, `503`, and `504`.
|
|
153
|
+
- **Exponential backoff**: Retry delays increase exponentially to prevent overwhelming the server.
|
|
154
|
+
- **Rate limit management**: If a `429 Too Many Requests` response is received with a `Retry-After` header, the SDK waits for the specified duration before retrying.
|
|
155
|
+
> *Note: Exponential backoff and max retries do not apply in this case.*
|
|
156
|
+
- **Customizable retry behavior**: Retry logic can be configured using the `retryConfig` parameter in the environment configuration.
|
|
157
|
+
|
|
158
|
+
#### Example: Customizing Retry Logic
|
|
159
|
+
|
|
160
|
+
You can enable and configure the retry logic by passing a `retryConfig` object when initializing the Chargebee environment:
|
|
161
|
+
|
|
162
|
+
```typescript
|
|
163
|
+
import Chargebee from 'chargebee';
|
|
164
|
+
|
|
165
|
+
const chargebee = new Chargebee({
|
|
166
|
+
site: "{{site}}",
|
|
167
|
+
apiKey: "{{api-key}}",
|
|
168
|
+
retryConfig: {
|
|
169
|
+
enabled: true, // Enable retry logic
|
|
170
|
+
maxRetries: 5, // Maximum number of retries
|
|
171
|
+
delayMs: 300, // Initial delay between retries in milliseconds
|
|
172
|
+
retryOn: [500, 502, 503, 504], // HTTP status codes to retry on
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
try {
|
|
177
|
+
const { customer } = await chargebee.customer.create({
|
|
178
|
+
email: "john@test.com",
|
|
179
|
+
});
|
|
180
|
+
console.log("Customer created:", customer);
|
|
181
|
+
} catch (err) {
|
|
182
|
+
console.error("Request failed after retries:", err);
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
#### Example: Rate Limit retry logic
|
|
187
|
+
|
|
188
|
+
You can enable and configure the retry logic for rate-limit by passing a `retryConfig` object when initializing the Chargebee environment:
|
|
189
|
+
|
|
190
|
+
```typescript
|
|
191
|
+
import Chargebee from 'chargebee';
|
|
192
|
+
|
|
193
|
+
const chargebee = new Chargebee({
|
|
194
|
+
site: "{{site}}",
|
|
195
|
+
apiKey: "{{api-key}}",
|
|
196
|
+
retryConfig: {
|
|
197
|
+
enabled: true,
|
|
198
|
+
retryOn: [429],
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
const { customer } = await chargebee.customer.create({
|
|
204
|
+
email: "john@test.com",
|
|
205
|
+
});
|
|
206
|
+
console.log("Customer created:", customer);
|
|
207
|
+
} catch (err) {
|
|
208
|
+
console.error("Request failed after retries:", err);
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
146
212
|
## Feedback
|
|
147
213
|
|
|
148
214
|
If you find any bugs or have any questions / feedback, open an issue in this repository or reach out to us on dx@chargebee.com
|
package/cjs/RequestWrapper.js
CHANGED
|
@@ -23,72 +23,136 @@ class RequestWrapper {
|
|
|
23
23
|
}
|
|
24
24
|
return idParam;
|
|
25
25
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
static parseRetryAfter(retryAfter) {
|
|
27
|
+
if (!retryAfter)
|
|
28
|
+
return null;
|
|
29
|
+
const seconds = parseInt(retryAfter, 10);
|
|
30
|
+
if (!isNaN(seconds)) {
|
|
31
|
+
return seconds * 1000;
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
async request() {
|
|
36
|
+
let _env = {};
|
|
37
|
+
(0, util_js_1.extend)(true, _env, this.envArg);
|
|
38
|
+
const env = _env;
|
|
39
|
+
const retryConfig = Object.assign({ enabled: false, maxRetries: 3, delayMs: 200, retryOn: [500, 502, 503, 504] }, env.retryConfig);
|
|
29
40
|
const urlIdParam = this.apiCall.hasIdInUrl ? this.args[0] : null;
|
|
30
41
|
let params = this.apiCall.hasIdInUrl
|
|
31
42
|
? this.args[1]
|
|
32
43
|
: this.args[0];
|
|
33
44
|
let headers = this.apiCall.hasIdInUrl ? this.args[2] : this.args[1];
|
|
34
45
|
Object.assign(this.httpHeaders, headers);
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
46
|
+
if (this.apiCall.httpMethod === 'POST' &&
|
|
47
|
+
!this.httpHeaders['chargebee-idempotency-key'] &&
|
|
48
|
+
this.apiCall.options &&
|
|
49
|
+
this.apiCall.options.isIdempotent &&
|
|
50
|
+
retryConfig.enabled) {
|
|
51
|
+
this.httpHeaders['chargebee-idempotency-key'] = (0, util_js_1.generateUUIDv4)();
|
|
52
|
+
}
|
|
53
|
+
const makeRequest = async (attempt = 0) => {
|
|
54
|
+
let path = (0, util_js_1.getApiURL)(env, this.apiCall.urlPrefix, this.apiCall.urlSuffix, urlIdParam);
|
|
55
|
+
if (typeof params === 'undefined' || params === null) {
|
|
56
|
+
params = {};
|
|
43
57
|
}
|
|
58
|
+
if (this.apiCall.httpMethod === 'GET') {
|
|
59
|
+
const queryParam = this.apiCall.isListReq
|
|
60
|
+
? (0, util_js_1.encodeListParams)((0, util_js_1.serialize)(params))
|
|
61
|
+
: (0, util_js_1.encodeParams)((0, util_js_1.serialize)(params));
|
|
62
|
+
path += '?' + queryParam;
|
|
63
|
+
params = {};
|
|
64
|
+
}
|
|
65
|
+
const jsonKeys = this.apiCall.jsonKeys;
|
|
66
|
+
const data = this.apiCall.isJsonRequest
|
|
67
|
+
? JSON.stringify(params)
|
|
68
|
+
: (0, util_js_1.encodeParams)(params, undefined, undefined, undefined, jsonKeys);
|
|
69
|
+
const requestHeaders = Object.assign({}, this.httpHeaders);
|
|
70
|
+
if (data.length) {
|
|
71
|
+
(0, util_js_1.extend)(true, requestHeaders, {
|
|
72
|
+
'Content-Length': data.length,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
const contentType = this.apiCall.isJsonRequest
|
|
76
|
+
? 'application/json;charset=UTF-8'
|
|
77
|
+
: 'application/x-www-form-urlencoded; charset=utf-8';
|
|
78
|
+
const userAgent = `Chargebee-NodeJs-Client ${env.clientVersion}${env.userAgentSuffix ? ';' + env.userAgentSuffix : ''}`;
|
|
79
|
+
(0, util_js_1.extend)(true, requestHeaders, {
|
|
80
|
+
Authorization: 'Basic ' + node_buffer_1.Buffer.from(env.apiKey + ':').toString('base64'),
|
|
81
|
+
Accept: 'application/json',
|
|
82
|
+
'Content-Type': contentType,
|
|
83
|
+
'User-Agent': userAgent,
|
|
84
|
+
'Lang-Version': typeof process === 'undefined' ? '' : process.version,
|
|
85
|
+
});
|
|
86
|
+
if (attempt > 0) {
|
|
87
|
+
requestHeaders['X-CB-Retry-Attempt'] = attempt.toString();
|
|
88
|
+
}
|
|
89
|
+
const resp = await this.envArg.httpClient.makeApiRequest({
|
|
90
|
+
host: (0, util_js_1.getHost)(env, this.apiCall.subDomain),
|
|
91
|
+
port: env.port,
|
|
92
|
+
path,
|
|
93
|
+
method: this.apiCall.httpMethod,
|
|
94
|
+
protocol: env.protocol,
|
|
95
|
+
headers: requestHeaders,
|
|
96
|
+
data,
|
|
97
|
+
timeout: env.timeout,
|
|
98
|
+
});
|
|
99
|
+
return new Promise((resolve, reject) => {
|
|
100
|
+
(0, coreCommon_js_1.handleResponse)((err, response) => {
|
|
101
|
+
if (err)
|
|
102
|
+
return reject(err);
|
|
103
|
+
return resolve(response);
|
|
104
|
+
}, resp);
|
|
105
|
+
});
|
|
106
|
+
};
|
|
107
|
+
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
108
|
+
const withRetry = async (retryCount, startTime) => {
|
|
109
|
+
var _a, _b, _c, _d, _e, _f;
|
|
44
110
|
try {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
111
|
+
return await makeRequest(retryCount);
|
|
112
|
+
}
|
|
113
|
+
catch (err) {
|
|
114
|
+
const statusCode = (_d = (_c = (_a = err.statusCode) !== null && _a !== void 0 ? _a : (_b = err.response) === null || _b === void 0 ? void 0 : _b.statusCode) !== null && _c !== void 0 ? _c : err.http_code) !== null && _d !== void 0 ? _d : err.http_status_code;
|
|
115
|
+
const isRateLimitError = statusCode === 429 && retryConfig.enabled;
|
|
116
|
+
if (isRateLimitError) {
|
|
117
|
+
const headers = ((_e = err.response) === null || _e === void 0 ? void 0 : _e.headers) || err.headers || {};
|
|
118
|
+
const retryAfterHeader = (_f = headers['retry-after']) === null || _f === void 0 ? void 0 : _f.toLowerCase();
|
|
119
|
+
const parsedDelay = RequestWrapper.parseRetryAfter(retryAfterHeader);
|
|
120
|
+
if (!parsedDelay) {
|
|
121
|
+
(0, util_js_1.log)(env, {
|
|
122
|
+
level: 'ERROR',
|
|
123
|
+
message: `Rate limit error occurred, but no retry-after header found. Retrying in ${retryConfig.delayMs}ms.`,
|
|
124
|
+
});
|
|
125
|
+
throw err;
|
|
126
|
+
}
|
|
127
|
+
(0, util_js_1.log)(env, {
|
|
128
|
+
level: 'INFO',
|
|
129
|
+
message: `Rate limit error occurred. Retrying in ${parsedDelay}ms.`,
|
|
130
|
+
});
|
|
131
|
+
await delay(parsedDelay);
|
|
56
132
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
133
|
+
else {
|
|
134
|
+
const canRetry = retryConfig.enabled &&
|
|
135
|
+
retryConfig.retryOn.includes(statusCode) &&
|
|
136
|
+
retryCount < retryConfig.maxRetries;
|
|
137
|
+
if (!canRetry) {
|
|
138
|
+
(0, util_js_1.log)(env, {
|
|
139
|
+
level: 'ERROR',
|
|
140
|
+
message: `Request failed after ${retryCount} retries: ${err.message}`,
|
|
141
|
+
});
|
|
142
|
+
throw err;
|
|
143
|
+
}
|
|
144
|
+
let retryDelayMs = retryConfig.delayMs * Math.pow(2, retryCount) +
|
|
145
|
+
Math.random() * 100;
|
|
146
|
+
(0, util_js_1.log)(env, {
|
|
147
|
+
level: 'INFO',
|
|
148
|
+
message: `Retrying request [${retryCount + 1}/${retryConfig.maxRetries}] in ${retryDelayMs}ms due to status code ${statusCode}.`,
|
|
64
149
|
});
|
|
150
|
+
await delay(retryDelayMs);
|
|
65
151
|
}
|
|
66
|
-
|
|
67
|
-
? 'application/json;charset=UTF-8'
|
|
68
|
-
: 'application/x-www-form-urlencoded; charset=utf-8';
|
|
69
|
-
(0, util_js_1.extend)(true, this.httpHeaders, {
|
|
70
|
-
Authorization: 'Basic ' + node_buffer_1.Buffer.from(env.apiKey + ':').toString('base64'),
|
|
71
|
-
Accept: 'application/json',
|
|
72
|
-
'Content-Type': contentType,
|
|
73
|
-
'User-Agent': 'Chargebee-NodeJs-Client ' + env.clientVersion,
|
|
74
|
-
'Lang-Version': typeof process === 'undefined' ? '' : process.version,
|
|
75
|
-
});
|
|
76
|
-
const resp = await this.envArg.httpClient.makeApiRequest({
|
|
77
|
-
host: (0, util_js_1.getHost)(env, this.apiCall.subDomain),
|
|
78
|
-
port: env.port,
|
|
79
|
-
path,
|
|
80
|
-
method: this.apiCall.httpMethod,
|
|
81
|
-
protocol: env.protocol,
|
|
82
|
-
headers: this.httpHeaders,
|
|
83
|
-
data: data,
|
|
84
|
-
timeout: env.timeout,
|
|
85
|
-
});
|
|
86
|
-
(0, coreCommon_js_1.handleResponse)(callBackWrapper, resp);
|
|
152
|
+
return await withRetry(retryCount + 1, startTime);
|
|
87
153
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
}
|
|
91
|
-
});
|
|
154
|
+
};
|
|
155
|
+
const promise = withRetry(0, Date.now());
|
|
92
156
|
return (0, util_js_1.callbackifyPromise)(promise);
|
|
93
157
|
}
|
|
94
158
|
}
|
package/cjs/createChargebee.js
CHANGED
package/cjs/environment.js
CHANGED
|
@@ -11,7 +11,7 @@ exports.Environment = {
|
|
|
11
11
|
hostSuffix: '.chargebee.com',
|
|
12
12
|
apiPath: '/api/v2',
|
|
13
13
|
timeout: DEFAULT_TIME_OUT,
|
|
14
|
-
clientVersion: 'v3.
|
|
14
|
+
clientVersion: 'v3.10.0',
|
|
15
15
|
port: DEFAULT_PORT,
|
|
16
16
|
timemachineWaitInMillis: DEFAULT_TIME_MACHINE_WAIT,
|
|
17
17
|
exportWaitInMillis: DEFAULT_EXPORT_WAIT,
|
package/cjs/util.js
CHANGED
|
@@ -7,53 +7,50 @@ exports.serialize = serialize;
|
|
|
7
7
|
exports.encodeListParams = encodeListParams;
|
|
8
8
|
exports.getHost = getHost;
|
|
9
9
|
exports.encodeParams = encodeParams;
|
|
10
|
+
exports.log = log;
|
|
11
|
+
exports.generateUUIDv4 = generateUUIDv4;
|
|
10
12
|
const extend = (deep, target, copy) => {
|
|
11
13
|
_extendsFn(deep, target, copy);
|
|
12
14
|
};
|
|
13
15
|
exports.extend = extend;
|
|
14
16
|
const _extendsFn = (...args) => {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
if (
|
|
38
|
-
|
|
39
|
-
(typeof copy === 'object' || (copyIsArray = (0, exports.isArray)(copy)))) {
|
|
40
|
-
if (copyIsArray) {
|
|
41
|
-
copyIsArray = false;
|
|
42
|
-
clone = src && (0, exports.isArray)(src) ? src : [];
|
|
43
|
-
}
|
|
44
|
-
else {
|
|
45
|
-
clone = src && typeof src === 'object' ? src : {};
|
|
46
|
-
}
|
|
47
|
-
target[name] = (0, exports.extend)(deep, clone, copy);
|
|
17
|
+
let options, name, src, copy, copyIsArray, clone;
|
|
18
|
+
let target = args[0] || {};
|
|
19
|
+
let i = 1;
|
|
20
|
+
const length = args.length;
|
|
21
|
+
let deep = false;
|
|
22
|
+
if (typeof target === 'boolean') {
|
|
23
|
+
deep = target;
|
|
24
|
+
target = args[1] || {};
|
|
25
|
+
i = 2;
|
|
26
|
+
}
|
|
27
|
+
if (typeof target !== 'object' && typeof target !== 'function') {
|
|
28
|
+
target = {};
|
|
29
|
+
}
|
|
30
|
+
for (; i < length; i++) {
|
|
31
|
+
if ((options = args[i]) !== null && options !== undefined) {
|
|
32
|
+
for (name in options) {
|
|
33
|
+
src = target[name];
|
|
34
|
+
copy = options[name];
|
|
35
|
+
if (target === copy) {
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (deep && copy && (typeof copy === 'object' || (0, exports.isArray)(copy))) {
|
|
39
|
+
if ((0, exports.isArray)(copy)) {
|
|
40
|
+
clone = (0, exports.isArray)(src) ? src : [];
|
|
48
41
|
}
|
|
49
|
-
else
|
|
50
|
-
|
|
42
|
+
else {
|
|
43
|
+
clone = (0, exports.isObject)(src) ? src : {};
|
|
51
44
|
}
|
|
45
|
+
target[name] = _extendsFn(deep, clone, copy);
|
|
46
|
+
}
|
|
47
|
+
else if (copy !== undefined) {
|
|
48
|
+
target[name] = copy;
|
|
52
49
|
}
|
|
53
50
|
}
|
|
54
51
|
}
|
|
55
|
-
return target;
|
|
56
52
|
}
|
|
53
|
+
return target;
|
|
57
54
|
};
|
|
58
55
|
const isArray = (obj) => {
|
|
59
56
|
return (Array.isArray(obj) ||
|
|
@@ -221,3 +218,31 @@ function encodeParams(paramObj, serialized, scope, index, jsonKeys, level = 0) {
|
|
|
221
218
|
}
|
|
222
219
|
return serialized.join('&').replace(/%20/g, '+');
|
|
223
220
|
}
|
|
221
|
+
function log(env, { level = 'INFO', message = '', context = {}, functionName = '' }) {
|
|
222
|
+
if (!env.enableDebugLogs) {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
const timestamp = new Date().toISOString();
|
|
226
|
+
const service = 'chargebee-node';
|
|
227
|
+
const metaString = Object.entries(context)
|
|
228
|
+
.map(([key, value]) => `${key}=${value}`)
|
|
229
|
+
.join(', ');
|
|
230
|
+
const logLine = `[${timestamp}] [${level.toUpperCase()}] [${service}] ${functionName} - ${message}${metaString ? ` (${metaString})` : ''}`;
|
|
231
|
+
console.debug(logLine);
|
|
232
|
+
}
|
|
233
|
+
const crypto_1 = require("crypto");
|
|
234
|
+
function generateUUIDv4() {
|
|
235
|
+
const bytes = (0, crypto_1.randomBytes)(16);
|
|
236
|
+
// Set version to 4 (UUIDv4)
|
|
237
|
+
bytes[6] = (bytes[6] & 0x0f) | 0x40;
|
|
238
|
+
// Set variant to 10xxxxxx
|
|
239
|
+
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
|
240
|
+
const hex = bytes.toString('hex');
|
|
241
|
+
return [
|
|
242
|
+
hex.slice(0, 8),
|
|
243
|
+
hex.slice(8, 12),
|
|
244
|
+
hex.slice(12, 16),
|
|
245
|
+
hex.slice(16, 20),
|
|
246
|
+
hex.slice(20),
|
|
247
|
+
].join('-');
|
|
248
|
+
}
|
package/esm/RequestWrapper.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { extend, callbackifyPromise, getApiURL, encodeListParams, encodeParams, serialize, getHost, } from './util.js';
|
|
1
|
+
import { extend, callbackifyPromise, getApiURL, encodeListParams, encodeParams, serialize, getHost, log, generateUUIDv4 as uuidv4, } from './util.js';
|
|
2
2
|
import { handleResponse } from './coreCommon.js';
|
|
3
3
|
import { Buffer } from 'node:buffer';
|
|
4
4
|
export class RequestWrapper {
|
|
@@ -20,72 +20,136 @@ export class RequestWrapper {
|
|
|
20
20
|
}
|
|
21
21
|
return idParam;
|
|
22
22
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
static parseRetryAfter(retryAfter) {
|
|
24
|
+
if (!retryAfter)
|
|
25
|
+
return null;
|
|
26
|
+
const seconds = parseInt(retryAfter, 10);
|
|
27
|
+
if (!isNaN(seconds)) {
|
|
28
|
+
return seconds * 1000;
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
async request() {
|
|
33
|
+
let _env = {};
|
|
34
|
+
extend(true, _env, this.envArg);
|
|
35
|
+
const env = _env;
|
|
36
|
+
const retryConfig = Object.assign({ enabled: false, maxRetries: 3, delayMs: 200, retryOn: [500, 502, 503, 504] }, env.retryConfig);
|
|
26
37
|
const urlIdParam = this.apiCall.hasIdInUrl ? this.args[0] : null;
|
|
27
38
|
let params = this.apiCall.hasIdInUrl
|
|
28
39
|
? this.args[1]
|
|
29
40
|
: this.args[0];
|
|
30
41
|
let headers = this.apiCall.hasIdInUrl ? this.args[2] : this.args[1];
|
|
31
42
|
Object.assign(this.httpHeaders, headers);
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
43
|
+
if (this.apiCall.httpMethod === 'POST' &&
|
|
44
|
+
!this.httpHeaders['chargebee-idempotency-key'] &&
|
|
45
|
+
this.apiCall.options &&
|
|
46
|
+
this.apiCall.options.isIdempotent &&
|
|
47
|
+
retryConfig.enabled) {
|
|
48
|
+
this.httpHeaders['chargebee-idempotency-key'] = uuidv4();
|
|
49
|
+
}
|
|
50
|
+
const makeRequest = async (attempt = 0) => {
|
|
51
|
+
let path = getApiURL(env, this.apiCall.urlPrefix, this.apiCall.urlSuffix, urlIdParam);
|
|
52
|
+
if (typeof params === 'undefined' || params === null) {
|
|
53
|
+
params = {};
|
|
40
54
|
}
|
|
55
|
+
if (this.apiCall.httpMethod === 'GET') {
|
|
56
|
+
const queryParam = this.apiCall.isListReq
|
|
57
|
+
? encodeListParams(serialize(params))
|
|
58
|
+
: encodeParams(serialize(params));
|
|
59
|
+
path += '?' + queryParam;
|
|
60
|
+
params = {};
|
|
61
|
+
}
|
|
62
|
+
const jsonKeys = this.apiCall.jsonKeys;
|
|
63
|
+
const data = this.apiCall.isJsonRequest
|
|
64
|
+
? JSON.stringify(params)
|
|
65
|
+
: encodeParams(params, undefined, undefined, undefined, jsonKeys);
|
|
66
|
+
const requestHeaders = Object.assign({}, this.httpHeaders);
|
|
67
|
+
if (data.length) {
|
|
68
|
+
extend(true, requestHeaders, {
|
|
69
|
+
'Content-Length': data.length,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
const contentType = this.apiCall.isJsonRequest
|
|
73
|
+
? 'application/json;charset=UTF-8'
|
|
74
|
+
: 'application/x-www-form-urlencoded; charset=utf-8';
|
|
75
|
+
const userAgent = `Chargebee-NodeJs-Client ${env.clientVersion}${env.userAgentSuffix ? ';' + env.userAgentSuffix : ''}`;
|
|
76
|
+
extend(true, requestHeaders, {
|
|
77
|
+
Authorization: 'Basic ' + Buffer.from(env.apiKey + ':').toString('base64'),
|
|
78
|
+
Accept: 'application/json',
|
|
79
|
+
'Content-Type': contentType,
|
|
80
|
+
'User-Agent': userAgent,
|
|
81
|
+
'Lang-Version': typeof process === 'undefined' ? '' : process.version,
|
|
82
|
+
});
|
|
83
|
+
if (attempt > 0) {
|
|
84
|
+
requestHeaders['X-CB-Retry-Attempt'] = attempt.toString();
|
|
85
|
+
}
|
|
86
|
+
const resp = await this.envArg.httpClient.makeApiRequest({
|
|
87
|
+
host: getHost(env, this.apiCall.subDomain),
|
|
88
|
+
port: env.port,
|
|
89
|
+
path,
|
|
90
|
+
method: this.apiCall.httpMethod,
|
|
91
|
+
protocol: env.protocol,
|
|
92
|
+
headers: requestHeaders,
|
|
93
|
+
data,
|
|
94
|
+
timeout: env.timeout,
|
|
95
|
+
});
|
|
96
|
+
return new Promise((resolve, reject) => {
|
|
97
|
+
handleResponse((err, response) => {
|
|
98
|
+
if (err)
|
|
99
|
+
return reject(err);
|
|
100
|
+
return resolve(response);
|
|
101
|
+
}, resp);
|
|
102
|
+
});
|
|
103
|
+
};
|
|
104
|
+
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
105
|
+
const withRetry = async (retryCount, startTime) => {
|
|
106
|
+
var _a, _b, _c, _d, _e, _f;
|
|
41
107
|
try {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
108
|
+
return await makeRequest(retryCount);
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
const statusCode = (_d = (_c = (_a = err.statusCode) !== null && _a !== void 0 ? _a : (_b = err.response) === null || _b === void 0 ? void 0 : _b.statusCode) !== null && _c !== void 0 ? _c : err.http_code) !== null && _d !== void 0 ? _d : err.http_status_code;
|
|
112
|
+
const isRateLimitError = statusCode === 429 && retryConfig.enabled;
|
|
113
|
+
if (isRateLimitError) {
|
|
114
|
+
const headers = ((_e = err.response) === null || _e === void 0 ? void 0 : _e.headers) || err.headers || {};
|
|
115
|
+
const retryAfterHeader = (_f = headers['retry-after']) === null || _f === void 0 ? void 0 : _f.toLowerCase();
|
|
116
|
+
const parsedDelay = RequestWrapper.parseRetryAfter(retryAfterHeader);
|
|
117
|
+
if (!parsedDelay) {
|
|
118
|
+
log(env, {
|
|
119
|
+
level: 'ERROR',
|
|
120
|
+
message: `Rate limit error occurred, but no retry-after header found. Retrying in ${retryConfig.delayMs}ms.`,
|
|
121
|
+
});
|
|
122
|
+
throw err;
|
|
123
|
+
}
|
|
124
|
+
log(env, {
|
|
125
|
+
level: 'INFO',
|
|
126
|
+
message: `Rate limit error occurred. Retrying in ${parsedDelay}ms.`,
|
|
127
|
+
});
|
|
128
|
+
await delay(parsedDelay);
|
|
53
129
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
130
|
+
else {
|
|
131
|
+
const canRetry = retryConfig.enabled &&
|
|
132
|
+
retryConfig.retryOn.includes(statusCode) &&
|
|
133
|
+
retryCount < retryConfig.maxRetries;
|
|
134
|
+
if (!canRetry) {
|
|
135
|
+
log(env, {
|
|
136
|
+
level: 'ERROR',
|
|
137
|
+
message: `Request failed after ${retryCount} retries: ${err.message}`,
|
|
138
|
+
});
|
|
139
|
+
throw err;
|
|
140
|
+
}
|
|
141
|
+
let retryDelayMs = retryConfig.delayMs * Math.pow(2, retryCount) +
|
|
142
|
+
Math.random() * 100;
|
|
143
|
+
log(env, {
|
|
144
|
+
level: 'INFO',
|
|
145
|
+
message: `Retrying request [${retryCount + 1}/${retryConfig.maxRetries}] in ${retryDelayMs}ms due to status code ${statusCode}.`,
|
|
61
146
|
});
|
|
147
|
+
await delay(retryDelayMs);
|
|
62
148
|
}
|
|
63
|
-
|
|
64
|
-
? 'application/json;charset=UTF-8'
|
|
65
|
-
: 'application/x-www-form-urlencoded; charset=utf-8';
|
|
66
|
-
extend(true, this.httpHeaders, {
|
|
67
|
-
Authorization: 'Basic ' + Buffer.from(env.apiKey + ':').toString('base64'),
|
|
68
|
-
Accept: 'application/json',
|
|
69
|
-
'Content-Type': contentType,
|
|
70
|
-
'User-Agent': 'Chargebee-NodeJs-Client ' + env.clientVersion,
|
|
71
|
-
'Lang-Version': typeof process === 'undefined' ? '' : process.version,
|
|
72
|
-
});
|
|
73
|
-
const resp = await this.envArg.httpClient.makeApiRequest({
|
|
74
|
-
host: getHost(env, this.apiCall.subDomain),
|
|
75
|
-
port: env.port,
|
|
76
|
-
path,
|
|
77
|
-
method: this.apiCall.httpMethod,
|
|
78
|
-
protocol: env.protocol,
|
|
79
|
-
headers: this.httpHeaders,
|
|
80
|
-
data: data,
|
|
81
|
-
timeout: env.timeout,
|
|
82
|
-
});
|
|
83
|
-
handleResponse(callBackWrapper, resp);
|
|
149
|
+
return await withRetry(retryCount + 1, startTime);
|
|
84
150
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
}
|
|
88
|
-
});
|
|
151
|
+
};
|
|
152
|
+
const promise = withRetry(0, Date.now());
|
|
89
153
|
return callbackifyPromise(promise);
|
|
90
154
|
}
|
|
91
155
|
}
|
package/esm/createChargebee.js
CHANGED
package/esm/environment.js
CHANGED
|
@@ -8,7 +8,7 @@ export const Environment = {
|
|
|
8
8
|
hostSuffix: '.chargebee.com',
|
|
9
9
|
apiPath: '/api/v2',
|
|
10
10
|
timeout: DEFAULT_TIME_OUT,
|
|
11
|
-
clientVersion: 'v3.
|
|
11
|
+
clientVersion: 'v3.10.0',
|
|
12
12
|
port: DEFAULT_PORT,
|
|
13
13
|
timemachineWaitInMillis: DEFAULT_TIME_MACHINE_WAIT,
|
|
14
14
|
exportWaitInMillis: DEFAULT_EXPORT_WAIT,
|
package/esm/util.js
CHANGED
|
@@ -2,48 +2,43 @@ export const extend = (deep, target, copy) => {
|
|
|
2
2
|
_extendsFn(deep, target, copy);
|
|
3
3
|
};
|
|
4
4
|
const _extendsFn = (...args) => {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
if (
|
|
28
|
-
|
|
29
|
-
(typeof copy === 'object' || (copyIsArray = isArray(copy)))) {
|
|
30
|
-
if (copyIsArray) {
|
|
31
|
-
copyIsArray = false;
|
|
32
|
-
clone = src && isArray(src) ? src : [];
|
|
33
|
-
}
|
|
34
|
-
else {
|
|
35
|
-
clone = src && typeof src === 'object' ? src : {};
|
|
36
|
-
}
|
|
37
|
-
target[name] = extend(deep, clone, copy);
|
|
5
|
+
let options, name, src, copy, copyIsArray, clone;
|
|
6
|
+
let target = args[0] || {};
|
|
7
|
+
let i = 1;
|
|
8
|
+
const length = args.length;
|
|
9
|
+
let deep = false;
|
|
10
|
+
if (typeof target === 'boolean') {
|
|
11
|
+
deep = target;
|
|
12
|
+
target = args[1] || {};
|
|
13
|
+
i = 2;
|
|
14
|
+
}
|
|
15
|
+
if (typeof target !== 'object' && typeof target !== 'function') {
|
|
16
|
+
target = {};
|
|
17
|
+
}
|
|
18
|
+
for (; i < length; i++) {
|
|
19
|
+
if ((options = args[i]) !== null && options !== undefined) {
|
|
20
|
+
for (name in options) {
|
|
21
|
+
src = target[name];
|
|
22
|
+
copy = options[name];
|
|
23
|
+
if (target === copy) {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
if (deep && copy && (typeof copy === 'object' || isArray(copy))) {
|
|
27
|
+
if (isArray(copy)) {
|
|
28
|
+
clone = isArray(src) ? src : [];
|
|
38
29
|
}
|
|
39
|
-
else
|
|
40
|
-
|
|
30
|
+
else {
|
|
31
|
+
clone = isObject(src) ? src : {};
|
|
41
32
|
}
|
|
33
|
+
target[name] = _extendsFn(deep, clone, copy);
|
|
34
|
+
}
|
|
35
|
+
else if (copy !== undefined) {
|
|
36
|
+
target[name] = copy;
|
|
42
37
|
}
|
|
43
38
|
}
|
|
44
39
|
}
|
|
45
|
-
return target;
|
|
46
40
|
}
|
|
41
|
+
return target;
|
|
47
42
|
};
|
|
48
43
|
export const isArray = (obj) => {
|
|
49
44
|
return (Array.isArray(obj) ||
|
|
@@ -205,3 +200,31 @@ export function encodeParams(paramObj, serialized, scope, index, jsonKeys, level
|
|
|
205
200
|
}
|
|
206
201
|
return serialized.join('&').replace(/%20/g, '+');
|
|
207
202
|
}
|
|
203
|
+
export function log(env, { level = 'INFO', message = '', context = {}, functionName = '' }) {
|
|
204
|
+
if (!env.enableDebugLogs) {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
const timestamp = new Date().toISOString();
|
|
208
|
+
const service = 'chargebee-node';
|
|
209
|
+
const metaString = Object.entries(context)
|
|
210
|
+
.map(([key, value]) => `${key}=${value}`)
|
|
211
|
+
.join(', ');
|
|
212
|
+
const logLine = `[${timestamp}] [${level.toUpperCase()}] [${service}] ${functionName} - ${message}${metaString ? ` (${metaString})` : ''}`;
|
|
213
|
+
console.debug(logLine);
|
|
214
|
+
}
|
|
215
|
+
import { randomBytes } from 'crypto';
|
|
216
|
+
export function generateUUIDv4() {
|
|
217
|
+
const bytes = randomBytes(16);
|
|
218
|
+
// Set version to 4 (UUIDv4)
|
|
219
|
+
bytes[6] = (bytes[6] & 0x0f) | 0x40;
|
|
220
|
+
// Set variant to 10xxxxxx
|
|
221
|
+
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
|
222
|
+
const hex = bytes.toString('hex');
|
|
223
|
+
return [
|
|
224
|
+
hex.slice(0, 8),
|
|
225
|
+
hex.slice(8, 12),
|
|
226
|
+
hex.slice(12, 16),
|
|
227
|
+
hex.slice(16, 20),
|
|
228
|
+
hex.slice(20),
|
|
229
|
+
].join('-');
|
|
230
|
+
}
|
package/package.json
CHANGED
package/types/index.d.ts
CHANGED
|
@@ -122,6 +122,34 @@ export type Config = {
|
|
|
122
122
|
* @hostSuffix url host suffix, default value is .chargebee.com
|
|
123
123
|
*/
|
|
124
124
|
hostSuffix?: string;
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* @retryConfig retry configuration for the client, default value is { enabled: false, maxRetries: 3, delayMs: 1000, retryOn: [500, 502, 503, 504]}
|
|
128
|
+
*/
|
|
129
|
+
retryConfig?: RetryConfig;
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* @enableDebugLogs whether to enable debug logs, default value is false
|
|
133
|
+
*/
|
|
134
|
+
enableDebugLogs?: boolean;
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* @userAgentSuffix optional string appended to the User-Agent header for additional logging
|
|
138
|
+
*/
|
|
139
|
+
userAgentSuffix?: string;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
export type RetryConfig = {
|
|
143
|
+
/**
|
|
144
|
+
* @enabled whether to enable retry logic, default value is false
|
|
145
|
+
* @maxRetries maximum number of retries, default value is 3
|
|
146
|
+
* @delayMs delay in milliseconds between retries, default value is 1000ms
|
|
147
|
+
* @retryOn array of HTTP status codes to retry on, default value is [500, 502, 503, 504]
|
|
148
|
+
*/
|
|
149
|
+
enabled?: boolean;
|
|
150
|
+
maxRetries?: number;
|
|
151
|
+
delayMs?: number;
|
|
152
|
+
retryOn?: Array<number>;
|
|
125
153
|
};
|
|
126
154
|
declare module 'chargebee' {
|
|
127
155
|
export default class Chargebee {
|