@aligent/microservice-util-lib 1.3.4 → 1.4.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/package.json +1 -1
- package/src/openapi-fetch-middlewares/log.js +3 -36
- package/src/openapi-fetch-middlewares/retry.d.ts +2 -1
- package/src/openapi-fetch-middlewares/retry.js +14 -11
- package/src/openapi-fetch-middlewares/types/retry.d.ts +15 -8
- package/src/openapi-fetch-middlewares/utils/body-parser.d.ts +4 -0
- package/src/openapi-fetch-middlewares/utils/body-parser.js +37 -0
- package/src/openapi-fetch-middlewares/utils/http-response-error.d.ts +37 -5
- package/src/openapi-fetch-middlewares/utils/http-response-error.js +34 -4
package/package.json
CHANGED
|
@@ -1,40 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.logMiddleware = logMiddleware;
|
|
4
|
-
|
|
5
|
-
* Parse request/response body based on Content-Type header
|
|
6
|
-
*/
|
|
7
|
-
async function parseBody(source, contentType) {
|
|
8
|
-
const normalizedContentType = contentType ? contentType.toLowerCase() : 'application/json';
|
|
9
|
-
try {
|
|
10
|
-
if (!source.body) {
|
|
11
|
-
return 'null';
|
|
12
|
-
}
|
|
13
|
-
// JSON content
|
|
14
|
-
if (normalizedContentType.includes('application/json')) {
|
|
15
|
-
return await source.json();
|
|
16
|
-
}
|
|
17
|
-
// Text content
|
|
18
|
-
if (normalizedContentType.includes('text/') ||
|
|
19
|
-
normalizedContentType.includes('application/xml') ||
|
|
20
|
-
normalizedContentType.includes('application/x-www-form-urlencoded')) {
|
|
21
|
-
return await source.text();
|
|
22
|
-
}
|
|
23
|
-
// Binary or multipart content - don't parse
|
|
24
|
-
if (normalizedContentType.includes('multipart/form-data') ||
|
|
25
|
-
normalizedContentType.includes('application/octet-stream') ||
|
|
26
|
-
normalizedContentType.includes('image/') ||
|
|
27
|
-
normalizedContentType.includes('video/') ||
|
|
28
|
-
normalizedContentType.includes('audio/')) {
|
|
29
|
-
return `[Binary content: ${contentType}]`;
|
|
30
|
-
}
|
|
31
|
-
// Unknown content type, try TEXT as default
|
|
32
|
-
return await source.text();
|
|
33
|
-
}
|
|
34
|
-
catch (error) {
|
|
35
|
-
return `[Unable to parse ${contentType} body: ${error instanceof Error ? error.message : 'Unknown error'}]`;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
4
|
+
const body_parser_1 = require("./utils/body-parser");
|
|
38
5
|
/**
|
|
39
6
|
* Creates a logging middleware for openapi-fetch clients.
|
|
40
7
|
*
|
|
@@ -94,14 +61,14 @@ function logMiddleware(clientName, logLevel = 'INFO', logger = console) {
|
|
|
94
61
|
baseUrl: options.baseUrl,
|
|
95
62
|
url: request.url,
|
|
96
63
|
params: params,
|
|
97
|
-
body: await parseBody(request.clone(), contentType),
|
|
64
|
+
body: await (0, body_parser_1.parseBody)(request.clone(), contentType),
|
|
98
65
|
});
|
|
99
66
|
},
|
|
100
67
|
async onResponse({ response }) {
|
|
101
68
|
const contentType = response.headers.get('Content-Type');
|
|
102
69
|
log(`Response from ${clientName}`, {
|
|
103
70
|
status: response.status,
|
|
104
|
-
body: await parseBody(response.clone(), contentType),
|
|
71
|
+
body: await (0, body_parser_1.parseBody)(response.clone(), contentType),
|
|
105
72
|
});
|
|
106
73
|
},
|
|
107
74
|
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Middleware } from 'openapi-fetch';
|
|
2
2
|
import type { RetryConfig, RetryContext, RetryDelayFn } from './types/retry';
|
|
3
3
|
import { HttpResponseError, isHttpResponseError } from './utils/http-response-error';
|
|
4
|
+
export type { HttpRequestData, HttpResponseData } from './utils/http-response-error';
|
|
4
5
|
/**
|
|
5
6
|
* This middleware implements retry logic with support for:
|
|
6
7
|
* - Configurable number of retry attempts
|
|
@@ -21,7 +22,7 @@ import { HttpResponseError, isHttpResponseError } from './utils/http-response-er
|
|
|
21
22
|
* const middleware = retryMiddleware({
|
|
22
23
|
* retries: 5,
|
|
23
24
|
* retryDelay: 'linear',
|
|
24
|
-
*
|
|
25
|
+
* baseDelay: 200,
|
|
25
26
|
* retryOn: [500, 502, 503, 504],
|
|
26
27
|
* onRetry: (context) => {
|
|
27
28
|
* console.log(`Retrying request (attempt ${context.attemptNumber})`);
|
|
@@ -89,15 +89,17 @@ function shouldRetryOnStatus(status, retryOn) {
|
|
|
89
89
|
return retryOn.includes(status);
|
|
90
90
|
}
|
|
91
91
|
/**
|
|
92
|
-
* Checks the response status and throws HttpResponseError if it's not "ok"
|
|
92
|
+
* Checks the response status and throws HttpResponseError if it's not "ok"
|
|
93
|
+
* and throwOnNotOk is enabled.
|
|
93
94
|
*
|
|
94
95
|
* @param {Response} response - The HTTP response object.
|
|
95
96
|
* @param {Request} request - The HTTP request object.
|
|
96
|
-
* @
|
|
97
|
+
* @param {boolean} throwOnNotOk - Whether to throw on non-ok responses.
|
|
98
|
+
* @throws {HttpResponseError} When throwOnNotOk is true and the response is not "ok".
|
|
97
99
|
*/
|
|
98
|
-
function throwErrorIfNotOkResponse(response, request) {
|
|
99
|
-
if (!response.ok) {
|
|
100
|
-
throw
|
|
100
|
+
async function throwErrorIfNotOkResponse(response, request, throwOnNotOk) {
|
|
101
|
+
if (throwOnNotOk && !response.ok) {
|
|
102
|
+
throw await http_response_error_1.HttpResponseError.create(response, request);
|
|
101
103
|
}
|
|
102
104
|
}
|
|
103
105
|
/**
|
|
@@ -120,7 +122,7 @@ function throwErrorIfNotOkResponse(response, request) {
|
|
|
120
122
|
* const middleware = retryMiddleware({
|
|
121
123
|
* retries: 5,
|
|
122
124
|
* retryDelay: 'linear',
|
|
123
|
-
*
|
|
125
|
+
* baseDelay: 200,
|
|
124
126
|
* retryOn: [500, 502, 503, 504],
|
|
125
127
|
* onRetry: (context) => {
|
|
126
128
|
* console.log(`Retrying request (attempt ${context.attemptNumber})`);
|
|
@@ -157,6 +159,7 @@ function retryMiddleware(config) {
|
|
|
157
159
|
retryDelay: getRetryDelayFn(config),
|
|
158
160
|
idempotentOnly: config?.idempotentOnly ?? true,
|
|
159
161
|
fetch: config?.fetch ?? fetch,
|
|
162
|
+
throwOnNotOk: config?.throwOnNotOk ?? true,
|
|
160
163
|
};
|
|
161
164
|
return {
|
|
162
165
|
async onResponse({ request, response }) {
|
|
@@ -164,7 +167,7 @@ function retryMiddleware(config) {
|
|
|
164
167
|
// If retryOn is specified, only use that list
|
|
165
168
|
if (config?.retryOn && config.retryOn.length > 0) {
|
|
166
169
|
if (!shouldRetryOnStatus(response.status, config.retryOn)) {
|
|
167
|
-
throwErrorIfNotOkResponse(response, request);
|
|
170
|
+
await throwErrorIfNotOkResponse(response, request, normalisedConfig.throwOnNotOk);
|
|
168
171
|
return response;
|
|
169
172
|
}
|
|
170
173
|
return await performRetries(normalisedConfig, context);
|
|
@@ -172,7 +175,7 @@ function retryMiddleware(config) {
|
|
|
172
175
|
// Otherwise, check if we should retry based on retry condition
|
|
173
176
|
const shouldRetry = await normalisedConfig.retryCondition(context, normalisedConfig.idempotentOnly);
|
|
174
177
|
if (!shouldRetry) {
|
|
175
|
-
throwErrorIfNotOkResponse(response, request);
|
|
178
|
+
await throwErrorIfNotOkResponse(response, request, normalisedConfig.throwOnNotOk);
|
|
176
179
|
return response;
|
|
177
180
|
}
|
|
178
181
|
return await performRetries(normalisedConfig, context);
|
|
@@ -221,18 +224,18 @@ async function performRetries(config, context) {
|
|
|
221
224
|
continue;
|
|
222
225
|
}
|
|
223
226
|
if (config.retryOn && !shouldRetryOnStatus(response?.status, config.retryOn)) {
|
|
224
|
-
throwErrorIfNotOkResponse(response, context.request);
|
|
227
|
+
await throwErrorIfNotOkResponse(response, context.request, config.throwOnNotOk);
|
|
225
228
|
return response;
|
|
226
229
|
}
|
|
227
230
|
const shouldRetry = await config.retryCondition(context, config.idempotentOnly);
|
|
228
231
|
if (!shouldRetry) {
|
|
229
|
-
throwErrorIfNotOkResponse(response, context.request);
|
|
232
|
+
await throwErrorIfNotOkResponse(response, context.request, config.throwOnNotOk);
|
|
230
233
|
return response;
|
|
231
234
|
}
|
|
232
235
|
} while (attempt <= maxRetries);
|
|
233
236
|
if (!response) {
|
|
234
237
|
throw context.error;
|
|
235
238
|
}
|
|
236
|
-
throwErrorIfNotOkResponse(response, context.request);
|
|
239
|
+
await throwErrorIfNotOkResponse(response, context.request, config.throwOnNotOk);
|
|
237
240
|
return response;
|
|
238
241
|
}
|
|
@@ -18,7 +18,7 @@ export interface RetryContext {
|
|
|
18
18
|
* Returns true if the request should be retried.
|
|
19
19
|
*
|
|
20
20
|
* @param {RetryContext} context - The retry context containing attempt information.
|
|
21
|
-
* @param {boolean} idempotentOnly - Whether to retry only when the HTTP method is
|
|
21
|
+
* @param {boolean} idempotentOnly - Whether to retry only when the HTTP method is idempotent.
|
|
22
22
|
* @returns {boolean | Promise<boolean>} Whether to retry the request.
|
|
23
23
|
*/
|
|
24
24
|
export type RetryConditionFn = (context: RetryContext, idempotentOnly: boolean) => boolean | Promise<boolean>;
|
|
@@ -58,16 +58,21 @@ export type OnRetryFn = (context: RetryContext) => void | Promise<void>;
|
|
|
58
58
|
* - 'exponential': Exponential backoff (100ms * 2^attemptNumber)
|
|
59
59
|
* - 'linear': Linear backoff (100ms * attemptNumber)
|
|
60
60
|
* - Custom function: Allows custom delay calculation
|
|
61
|
-
* @property {number} [
|
|
62
|
-
* @property {number} [
|
|
61
|
+
* @property {number} [baseDelay=100] - Base delay in milliseconds for built-in delay strategies.
|
|
62
|
+
* @property {number} [maxDelay=30000] - Maximum delay in milliseconds between retry attempts.
|
|
63
63
|
* @property {boolean} [shouldResetTimeout=false] - Whether to reset the timeout between retries.
|
|
64
64
|
* @property {OnRetryFn} [onRetry] - Callback function executed before each retry attempt.
|
|
65
65
|
* @property {number[]} [retryOn]
|
|
66
66
|
* - Array of HTTP status codes that should trigger a retry.
|
|
67
|
-
* -
|
|
68
|
-
* @property {boolean} [idempotentOnly]
|
|
69
|
-
* - Whether to retry only when the HTTP method is
|
|
70
|
-
* -
|
|
67
|
+
* - Defaults to 5xx, 429, and 408 errors.
|
|
68
|
+
* @property {boolean} [idempotentOnly=true]
|
|
69
|
+
* - Whether to retry only when the HTTP method is idempotent.
|
|
70
|
+
* - Defaults to `true`, retrying only on GET, HEAD, OPTIONS, PUT, or DELETE methods.
|
|
71
|
+
* @property {boolean} [throwOnNotOk=true]
|
|
72
|
+
* - Whether to throw an `HttpResponseError` when the final response has a non-OK status (i.e. not 2xx).
|
|
73
|
+
* - Defaults to `true` for backward compatibility.
|
|
74
|
+
* - Set to `false` to return the response as-is, which allows downstream middlewares
|
|
75
|
+
* (e.g. logging middleware) to inspect the response before the caller handles the error.
|
|
71
76
|
* @property {typeof fetch} [fetch]
|
|
72
77
|
* - Custom fetch function to use for retries. Defaults to the global fetch function.
|
|
73
78
|
* - Useful for testing or using a custom fetch implementation.
|
|
@@ -82,12 +87,14 @@ export interface RetryConfig {
|
|
|
82
87
|
onRetry?: OnRetryFn;
|
|
83
88
|
retryOn?: number[];
|
|
84
89
|
idempotentOnly?: boolean;
|
|
90
|
+
throwOnNotOk?: boolean;
|
|
85
91
|
fetch?: typeof fetch;
|
|
86
92
|
}
|
|
87
|
-
export type NormalisedConfig = Omit<RetryConfig, 'retries' | 'retryCondition' | 'retryDelay' | 'idempotentOnly' | 'fetch'> & {
|
|
93
|
+
export type NormalisedConfig = Omit<RetryConfig, 'retries' | 'retryCondition' | 'retryDelay' | 'idempotentOnly' | 'throwOnNotOk' | 'fetch'> & {
|
|
88
94
|
retries: number;
|
|
89
95
|
retryCondition: RetryConditionFn;
|
|
90
96
|
retryDelay: RetryDelayFn;
|
|
91
97
|
idempotentOnly: boolean;
|
|
98
|
+
throwOnNotOk: boolean;
|
|
92
99
|
fetch: typeof fetch;
|
|
93
100
|
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseBody = parseBody;
|
|
4
|
+
/**
|
|
5
|
+
* Parse request/response body based on Content-Type header
|
|
6
|
+
*/
|
|
7
|
+
async function parseBody(source, contentType) {
|
|
8
|
+
const normalizedContentType = contentType ? contentType.toLowerCase() : 'application/json';
|
|
9
|
+
try {
|
|
10
|
+
if (!source.body) {
|
|
11
|
+
return 'null';
|
|
12
|
+
}
|
|
13
|
+
// JSON content
|
|
14
|
+
if (normalizedContentType.includes('application/json')) {
|
|
15
|
+
return await source.json();
|
|
16
|
+
}
|
|
17
|
+
// Text content
|
|
18
|
+
if (normalizedContentType.includes('text/') ||
|
|
19
|
+
normalizedContentType.includes('application/xml') ||
|
|
20
|
+
normalizedContentType.includes('application/x-www-form-urlencoded')) {
|
|
21
|
+
return await source.text();
|
|
22
|
+
}
|
|
23
|
+
// Binary or multipart content - don't parse
|
|
24
|
+
if (normalizedContentType.includes('multipart/form-data') ||
|
|
25
|
+
normalizedContentType.includes('application/octet-stream') ||
|
|
26
|
+
normalizedContentType.includes('image/') ||
|
|
27
|
+
normalizedContentType.includes('video/') ||
|
|
28
|
+
normalizedContentType.includes('audio/')) {
|
|
29
|
+
return `[Binary content: ${contentType}]`;
|
|
30
|
+
}
|
|
31
|
+
// Unknown content type, try TEXT as default
|
|
32
|
+
return await source.text();
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
return `[Unable to parse ${contentType} body: ${error instanceof Error ? error.message : 'Unknown error'}]`;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -1,15 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Serializable snapshot of an HTTP request.
|
|
3
|
+
* Captures all meaningful data at creation time so it can be
|
|
4
|
+
* logged, serialized, or inspected without stream-consumption issues.
|
|
5
|
+
*/
|
|
6
|
+
export interface HttpRequestData {
|
|
7
|
+
readonly method: string;
|
|
8
|
+
readonly url: string;
|
|
9
|
+
readonly params: Record<string, string>;
|
|
10
|
+
readonly headers: Record<string, string>;
|
|
11
|
+
readonly body: unknown;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Serializable snapshot of an HTTP response.
|
|
15
|
+
* Captures all meaningful data at creation time so it can be
|
|
16
|
+
* logged, serialized, or inspected without stream-consumption issues.
|
|
17
|
+
*/
|
|
18
|
+
export interface HttpResponseData {
|
|
19
|
+
readonly status: number;
|
|
20
|
+
readonly statusText: string;
|
|
21
|
+
readonly headers: Record<string, string>;
|
|
22
|
+
readonly body: unknown;
|
|
23
|
+
}
|
|
1
24
|
/**
|
|
2
25
|
* Custom error class for HTTP response errors, similar to Axios Error.
|
|
3
26
|
* Provides detailed information about the failed request and response.
|
|
27
|
+
*
|
|
28
|
+
* Stores pre-read, serializable snapshots of the request and response
|
|
29
|
+
* rather than raw Request/Response objects, ensuring bodies are always
|
|
30
|
+
* available for logging and debugging.
|
|
4
31
|
*/
|
|
5
32
|
export declare class HttpResponseError extends Error {
|
|
6
33
|
readonly name = "HttpResponseError";
|
|
7
34
|
readonly status: number;
|
|
8
35
|
readonly statusText: string;
|
|
9
|
-
readonly response:
|
|
10
|
-
readonly request:
|
|
36
|
+
readonly response: HttpResponseData;
|
|
37
|
+
readonly request: HttpRequestData;
|
|
11
38
|
readonly isHttpResponseError = true;
|
|
12
|
-
constructor(
|
|
39
|
+
private constructor();
|
|
40
|
+
/**
|
|
41
|
+
* Creates an HttpResponseError with pre-read request and response bodies.
|
|
42
|
+
* Bodies are read eagerly so they are available for logging/serialization.
|
|
43
|
+
*/
|
|
44
|
+
static create(response: Response, request: Request): Promise<HttpResponseError>;
|
|
13
45
|
/**
|
|
14
46
|
* Returns a JSON representation of the error for logging/debugging.
|
|
15
47
|
*/
|
|
@@ -18,8 +50,8 @@ export declare class HttpResponseError extends Error {
|
|
|
18
50
|
message: string;
|
|
19
51
|
status: number;
|
|
20
52
|
statusText: string;
|
|
21
|
-
|
|
22
|
-
|
|
53
|
+
request: HttpRequestData;
|
|
54
|
+
response: HttpResponseData;
|
|
23
55
|
};
|
|
24
56
|
}
|
|
25
57
|
/**
|
|
@@ -2,9 +2,14 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.HttpResponseError = void 0;
|
|
4
4
|
exports.isHttpResponseError = isHttpResponseError;
|
|
5
|
+
const body_parser_1 = require("./body-parser");
|
|
5
6
|
/**
|
|
6
7
|
* Custom error class for HTTP response errors, similar to Axios Error.
|
|
7
8
|
* Provides detailed information about the failed request and response.
|
|
9
|
+
*
|
|
10
|
+
* Stores pre-read, serializable snapshots of the request and response
|
|
11
|
+
* rather than raw Request/Response objects, ensuring bodies are always
|
|
12
|
+
* available for logging and debugging.
|
|
8
13
|
*/
|
|
9
14
|
class HttpResponseError extends Error {
|
|
10
15
|
name = 'HttpResponseError';
|
|
@@ -13,17 +18,42 @@ class HttpResponseError extends Error {
|
|
|
13
18
|
response;
|
|
14
19
|
request;
|
|
15
20
|
isHttpResponseError = true;
|
|
16
|
-
constructor(
|
|
21
|
+
constructor(request, response) {
|
|
17
22
|
super(`${response.status}: ${response.statusText}`);
|
|
18
23
|
this.status = response.status;
|
|
19
24
|
this.statusText = response.statusText;
|
|
20
|
-
this.response = response;
|
|
21
25
|
this.request = request;
|
|
26
|
+
this.response = response;
|
|
22
27
|
// Maintains proper stack trace for where error was thrown (V8 engines)
|
|
23
28
|
if (Error.captureStackTrace) {
|
|
24
29
|
Error.captureStackTrace(this, HttpResponseError);
|
|
25
30
|
}
|
|
26
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* Creates an HttpResponseError with pre-read request and response bodies.
|
|
34
|
+
* Bodies are read eagerly so they are available for logging/serialization.
|
|
35
|
+
*/
|
|
36
|
+
static async create(response, request) {
|
|
37
|
+
const url = new URL(request.url);
|
|
38
|
+
const [requestBody, responseBody] = await Promise.all([
|
|
39
|
+
(0, body_parser_1.parseBody)(request.clone(), request.headers.get('content-type')),
|
|
40
|
+
(0, body_parser_1.parseBody)(response.clone(), response.headers.get('content-type')),
|
|
41
|
+
]);
|
|
42
|
+
const requestData = {
|
|
43
|
+
method: request.method,
|
|
44
|
+
url: request.url,
|
|
45
|
+
params: Object.fromEntries(url.searchParams.entries()),
|
|
46
|
+
headers: Object.fromEntries(request.headers.entries()),
|
|
47
|
+
body: requestBody,
|
|
48
|
+
};
|
|
49
|
+
const responseData = {
|
|
50
|
+
status: response.status,
|
|
51
|
+
statusText: response.statusText,
|
|
52
|
+
headers: Object.fromEntries(response.headers.entries()),
|
|
53
|
+
body: responseBody,
|
|
54
|
+
};
|
|
55
|
+
return new HttpResponseError(requestData, responseData);
|
|
56
|
+
}
|
|
27
57
|
/**
|
|
28
58
|
* Returns a JSON representation of the error for logging/debugging.
|
|
29
59
|
*/
|
|
@@ -33,8 +63,8 @@ class HttpResponseError extends Error {
|
|
|
33
63
|
message: this.message,
|
|
34
64
|
status: this.status,
|
|
35
65
|
statusText: this.statusText,
|
|
36
|
-
|
|
37
|
-
|
|
66
|
+
request: this.request,
|
|
67
|
+
response: this.response,
|
|
38
68
|
};
|
|
39
69
|
}
|
|
40
70
|
}
|