@friggframework/core 2.0.0--canary.579.312fe8b.0 → 2.0.0--canary.580.1003d8d.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/modules/requester/requester.js +37 -145
- package/package.json +5 -5
|
@@ -3,8 +3,6 @@ const { Delegate } = require('../../core');
|
|
|
3
3
|
const { FetchError } = require('../../errors');
|
|
4
4
|
const { get } = require('../../assertions');
|
|
5
5
|
|
|
6
|
-
const DEFAULT_REQUEST_TIMEOUT_MS = 60_000;
|
|
7
|
-
|
|
8
6
|
class Requester extends Delegate {
|
|
9
7
|
constructor(params) {
|
|
10
8
|
super(params);
|
|
@@ -15,30 +13,6 @@ class Requester extends Delegate {
|
|
|
15
13
|
this.delegateTypes.push(this.DLGT_INVALID_AUTH);
|
|
16
14
|
this.agent = get(params, 'agent', null);
|
|
17
15
|
|
|
18
|
-
// Per-attempt HTTP timeout. Without this the framework called fetch()
|
|
19
|
-
// with no AbortController and no timeout — a silently-hung TCP
|
|
20
|
-
// connection (server accepts but never responds) blocked the calling
|
|
21
|
-
// promise forever, cascading into stalled batches, stalled syncs,
|
|
22
|
-
// and worker-lambda timeouts.
|
|
23
|
-
//
|
|
24
|
-
// Configuration precedence:
|
|
25
|
-
// 1. Instance param: new Requester({ requestTimeoutMs: 30_000 })
|
|
26
|
-
// 2. Class static: static requestTimeoutMs = 30_000
|
|
27
|
-
// 3. Default: DEFAULT_REQUEST_TIMEOUT_MS (60s)
|
|
28
|
-
//
|
|
29
|
-
// Pass 0 (or null) to disable the timeout entirely — reserved for
|
|
30
|
-
// test doubles and documented long-running endpoints.
|
|
31
|
-
// Intentionally NOT using `get(params, ...)` here — the Frigg
|
|
32
|
-
// `get` helper throws RequiredPropertyError if the key is missing
|
|
33
|
-
// and no default is provided, which would collide with the fall-
|
|
34
|
-
// through to the class-level static override.
|
|
35
|
-
const instanceTimeout = params?.requestTimeoutMs;
|
|
36
|
-
this.requestTimeoutMs =
|
|
37
|
-
instanceTimeout !== undefined && instanceTimeout !== null
|
|
38
|
-
? instanceTimeout
|
|
39
|
-
: this.constructor.requestTimeoutMs ??
|
|
40
|
-
DEFAULT_REQUEST_TIMEOUT_MS;
|
|
41
|
-
|
|
42
16
|
// Allow passing in the fetch function
|
|
43
17
|
// Instance methods can use this.fetch without differentiating
|
|
44
18
|
this.fetch = get(params, 'fetch', fetch);
|
|
@@ -74,134 +48,52 @@ class Requester extends Delegate {
|
|
|
74
48
|
|
|
75
49
|
if (this.agent) options.agent = this.agent;
|
|
76
50
|
|
|
77
|
-
|
|
78
|
-
// recursion (with its own backoff sleeps) always gets a clean
|
|
79
|
-
// signal. Timer is cleared in the finally block regardless of
|
|
80
|
-
// outcome.
|
|
81
|
-
const timeoutMs = this.requestTimeoutMs;
|
|
82
|
-
const controller = timeoutMs > 0 ? new AbortController() : null;
|
|
83
|
-
const timeoutHandle = controller
|
|
84
|
-
? setTimeout(() => controller.abort(), timeoutMs)
|
|
85
|
-
: null;
|
|
86
|
-
const fetchOptions = controller
|
|
87
|
-
? { ...options, signal: controller.signal }
|
|
88
|
-
: options;
|
|
89
|
-
|
|
90
|
-
// Timer must stay active through body consumption. node-fetch v2
|
|
91
|
-
// resolves the fetch() promise when headers arrive, not when the
|
|
92
|
-
// body is fully read — so a server that sends headers and then
|
|
93
|
-
// stalls the body would still hang parsedBody() or
|
|
94
|
-
// FetchError.create()'s response.text() call. We clear the timer
|
|
95
|
-
// only after the body is fully consumed (success path) or
|
|
96
|
-
// deliberately before each recursive retry so the new attempt
|
|
97
|
-
// starts with its own fresh timer.
|
|
98
|
-
let timerCleared = false;
|
|
99
|
-
const clearRequestTimer = () => {
|
|
100
|
-
if (!timerCleared && timeoutHandle) {
|
|
101
|
-
clearTimeout(timeoutHandle);
|
|
102
|
-
timerCleared = true;
|
|
103
|
-
}
|
|
104
|
-
};
|
|
105
|
-
|
|
51
|
+
let response;
|
|
106
52
|
try {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
} catch (e) {
|
|
111
|
-
// AbortController fires AbortError (name) / ETIMEDOUT-shaped
|
|
112
|
-
// errors (type on node-fetch) when we hit the timeout. No
|
|
113
|
-
// retry on timeout: a slow endpoint is a downstream problem,
|
|
114
|
-
// and each retry would wait another `timeoutMs` before giving
|
|
115
|
-
// up — amplifying the hang into a per-record multi-minute
|
|
116
|
-
// stall at batch scale.
|
|
117
|
-
const isTimeout =
|
|
118
|
-
e?.name === 'AbortError' || e?.type === 'aborted';
|
|
119
|
-
if (e?.code === 'ECONNRESET' && i < this.backOff.length) {
|
|
120
|
-
clearRequestTimer();
|
|
121
|
-
const delay = this.backOff[i] * 1000;
|
|
122
|
-
await new Promise((resolve) =>
|
|
123
|
-
setTimeout(resolve, delay)
|
|
124
|
-
);
|
|
125
|
-
return this._request(url, options, i + 1);
|
|
126
|
-
}
|
|
127
|
-
const fetchError = await FetchError.create({
|
|
128
|
-
resource: encodedUrl,
|
|
129
|
-
init: options,
|
|
130
|
-
responseBody: isTimeout
|
|
131
|
-
? `Request timed out after ${timeoutMs}ms`
|
|
132
|
-
: e,
|
|
133
|
-
});
|
|
134
|
-
if (isTimeout) {
|
|
135
|
-
// Flag + machine-readable fields so callers can
|
|
136
|
-
// distinguish a timeout from a generic network error
|
|
137
|
-
// without parsing the message (which FetchError
|
|
138
|
-
// sanitizes outside of STAGE=dev).
|
|
139
|
-
fetchError.isTimeout = true;
|
|
140
|
-
fetchError.timeoutMs = timeoutMs;
|
|
141
|
-
}
|
|
142
|
-
throw fetchError;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
const { status } = response;
|
|
146
|
-
|
|
147
|
-
// If the status is retriable and there are back off requests left, retry the request
|
|
148
|
-
if ((status === 429 || status >= 500) && i < this.backOff.length) {
|
|
149
|
-
clearRequestTimer();
|
|
53
|
+
response = await this.fetch(encodedUrl, options);
|
|
54
|
+
} catch (e) {
|
|
55
|
+
if (e.code === 'ECONNRESET' && i < this.backOff.length) {
|
|
150
56
|
const delay = this.backOff[i] * 1000;
|
|
151
57
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
152
58
|
return this._request(url, options, i + 1);
|
|
153
|
-
} else if (status === 401) {
|
|
154
|
-
if (!this.isRefreshable || this.refreshCount > 0) {
|
|
155
|
-
await this.notify(this.DLGT_INVALID_AUTH);
|
|
156
|
-
} else {
|
|
157
|
-
this.refreshCount++;
|
|
158
|
-
const refreshSucceeded = await this.refreshAuth();
|
|
159
|
-
if (refreshSucceeded) {
|
|
160
|
-
clearRequestTimer();
|
|
161
|
-
return this._request(url, options, i + 1);
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
59
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
60
|
+
throw await FetchError.create({
|
|
61
|
+
resource: encodedUrl,
|
|
62
|
+
init: options,
|
|
63
|
+
responseBody: e,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
const { status } = response;
|
|
67
|
+
|
|
68
|
+
// If the status is retriable and there are back off requests left, retry the request
|
|
69
|
+
if ((status === 429 || status >= 500) && i < this.backOff.length) {
|
|
70
|
+
const delay = this.backOff[i] * 1000;
|
|
71
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
72
|
+
return this._request(url, options, i + 1);
|
|
73
|
+
} else if (status === 401) {
|
|
74
|
+
if (!this.isRefreshable || this.refreshCount > 0) {
|
|
75
|
+
await this.notify(this.DLGT_INVALID_AUTH);
|
|
76
|
+
} else {
|
|
77
|
+
this.refreshCount++;
|
|
78
|
+
const refreshSucceeded = await this.refreshAuth();
|
|
79
|
+
if (refreshSucceeded) {
|
|
80
|
+
return this._request(url, options, i + 1);
|
|
81
|
+
}
|
|
179
82
|
}
|
|
83
|
+
}
|
|
180
84
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
:
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
// the error as an AbortError on the body stream. Surface the
|
|
189
|
-
// same isTimeout flag callers use for header-phase timeouts.
|
|
190
|
-
throw this._maybeFlagTimeoutDuringBodyRead(e, timeoutMs);
|
|
191
|
-
} finally {
|
|
192
|
-
clearRequestTimer();
|
|
85
|
+
// If the error wasn't retried, throw.
|
|
86
|
+
if (status >= 400) {
|
|
87
|
+
throw await FetchError.create({
|
|
88
|
+
resource: encodedUrl,
|
|
89
|
+
init: options,
|
|
90
|
+
response,
|
|
91
|
+
});
|
|
193
92
|
}
|
|
194
|
-
}
|
|
195
93
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
const isAbort =
|
|
200
|
-
err.name === 'AbortError' || err.type === 'aborted';
|
|
201
|
-
if (!isAbort) return err;
|
|
202
|
-
err.isTimeout = true;
|
|
203
|
-
err.timeoutMs = timeoutMs;
|
|
204
|
-
return err;
|
|
94
|
+
return options.returnFullRes
|
|
95
|
+
? response
|
|
96
|
+
: await this.parsedBody(response);
|
|
205
97
|
}
|
|
206
98
|
|
|
207
99
|
async _get(options) {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@friggframework/core",
|
|
3
3
|
"prettier": "@friggframework/prettier-config",
|
|
4
|
-
"version": "2.0.0--canary.
|
|
4
|
+
"version": "2.0.0--canary.580.1003d8d.0",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@aws-sdk/client-apigatewaymanagementapi": "^3.588.0",
|
|
7
7
|
"@aws-sdk/client-kms": "^3.588.0",
|
|
@@ -38,9 +38,9 @@
|
|
|
38
38
|
}
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
|
-
"@friggframework/eslint-config": "2.0.0--canary.
|
|
42
|
-
"@friggframework/prettier-config": "2.0.0--canary.
|
|
43
|
-
"@friggframework/test": "2.0.0--canary.
|
|
41
|
+
"@friggframework/eslint-config": "2.0.0--canary.580.1003d8d.0",
|
|
42
|
+
"@friggframework/prettier-config": "2.0.0--canary.580.1003d8d.0",
|
|
43
|
+
"@friggframework/test": "2.0.0--canary.580.1003d8d.0",
|
|
44
44
|
"@prisma/client": "^6.17.0",
|
|
45
45
|
"@types/lodash": "4.17.15",
|
|
46
46
|
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
|
@@ -80,5 +80,5 @@
|
|
|
80
80
|
"publishConfig": {
|
|
81
81
|
"access": "public"
|
|
82
82
|
},
|
|
83
|
-
"gitHead": "
|
|
83
|
+
"gitHead": "1003d8deff8f587be3cd9210f1c35c5dc92531be"
|
|
84
84
|
}
|