@friggframework/core 2.0.0--canary.579.4f65577.0 → 2.0.0--canary.579.2d1eba8.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.
@@ -87,73 +87,121 @@ class Requester extends Delegate {
87
87
  ? { ...options, signal: controller.signal }
88
88
  : options;
89
89
 
90
- let response;
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
+
91
106
  try {
92
- response = await this.fetch(encodedUrl, fetchOptions);
93
- } catch (e) {
94
- // AbortController fires AbortError (name) / ETIMEDOUT-shaped
95
- // errors (type on node-fetch) when we hit the timeout. No
96
- // retry on timeout: a slow endpoint is a downstream problem,
97
- // and each retry would wait another `timeoutMs` before giving
98
- // up amplifying the hang into a per-record multi-minute
99
- // stall at batch scale.
100
- const isTimeout =
101
- e?.name === 'AbortError' || e?.type === 'aborted';
102
- if (e?.code === 'ECONNRESET' && i < this.backOff.length) {
107
+ let response;
108
+ try {
109
+ response = await this.fetch(encodedUrl, fetchOptions);
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();
103
150
  const delay = this.backOff[i] * 1000;
104
151
  await new Promise((resolve) => setTimeout(resolve, delay));
105
152
  return this._request(url, options, i + 1);
106
- }
107
- const fetchError = await FetchError.create({
108
- resource: encodedUrl,
109
- init: options,
110
- responseBody: isTimeout
111
- ? `Request timed out after ${timeoutMs}ms`
112
- : e,
113
- });
114
- if (isTimeout) {
115
- // Flag + machine-readable fields so callers can
116
- // distinguish a timeout from a generic network error
117
- // without parsing the message (which FetchError
118
- // sanitizes outside of STAGE=dev).
119
- fetchError.isTimeout = true;
120
- fetchError.timeoutMs = timeoutMs;
121
- }
122
- throw fetchError;
123
- } finally {
124
- if (timeoutHandle) clearTimeout(timeoutHandle);
125
- }
126
- const { status } = response;
127
-
128
- // If the status is retriable and there are back off requests left, retry the request
129
- if ((status === 429 || status >= 500) && i < this.backOff.length) {
130
- const delay = this.backOff[i] * 1000;
131
- await new Promise((resolve) => setTimeout(resolve, delay));
132
- return this._request(url, options, i + 1);
133
- } else if (status === 401) {
134
- if (!this.isRefreshable || this.refreshCount > 0) {
135
- await this.notify(this.DLGT_INVALID_AUTH);
136
- } else {
137
- this.refreshCount++;
138
- const refreshSucceeded = await this.refreshAuth();
139
- if (refreshSucceeded) {
140
- 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
+ }
141
163
  }
142
164
  }
143
- }
144
165
 
145
- // If the error wasn't retried, throw.
146
- if (status >= 400) {
147
- throw await FetchError.create({
148
- resource: encodedUrl,
149
- init: options,
150
- response,
151
- });
166
+ // If the error wasn't retried, throw. FetchError.create reads
167
+ // the response body (response.text()) — timer must still be
168
+ // alive to catch a stalled body stream.
169
+ if (status >= 400) {
170
+ const fetchError = await FetchError.create({
171
+ resource: encodedUrl,
172
+ init: options,
173
+ response,
174
+ });
175
+ throw this._maybeFlagTimeoutDuringBodyRead(
176
+ fetchError,
177
+ timeoutMs
178
+ );
179
+ }
180
+
181
+ // parsedBody consumes the response body stream. If the server
182
+ // stalls mid-stream the timer (still armed) aborts it.
183
+ return options.returnFullRes
184
+ ? response
185
+ : await this.parsedBody(response);
186
+ } catch (e) {
187
+ // If the abort fired during body consumption, node-fetch emits
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();
152
193
  }
194
+ }
153
195
 
154
- return options.returnFullRes
155
- ? response
156
- : await this.parsedBody(response);
196
+ _maybeFlagTimeoutDuringBodyRead(err, timeoutMs) {
197
+ if (!err || typeof err !== 'object') return err;
198
+ if (err.isTimeout) return err;
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;
157
205
  }
158
206
 
159
207
  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.579.4f65577.0",
4
+ "version": "2.0.0--canary.579.2d1eba8.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.579.4f65577.0",
42
- "@friggframework/prettier-config": "2.0.0--canary.579.4f65577.0",
43
- "@friggframework/test": "2.0.0--canary.579.4f65577.0",
41
+ "@friggframework/eslint-config": "2.0.0--canary.579.2d1eba8.0",
42
+ "@friggframework/prettier-config": "2.0.0--canary.579.2d1eba8.0",
43
+ "@friggframework/test": "2.0.0--canary.579.2d1eba8.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": "4f65577ef61f82fc02d25d22a92ffa851c179676"
83
+ "gitHead": "2d1eba87ebd7c2e83b0e95a87a41eb9037417694"
84
84
  }