@gcmdev/faxios 0.2.2 → 0.2.3
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/dist/lib/adapters/fetch.d.ts +4 -1
- package/dist/lib/adapters/fetch.d.ts.map +1 -1
- package/dist/lib/adapters/fetch.js +134 -76
- package/dist/lib/adapters/fetch.js.map +1 -1
- package/dist/lib/core/FaxiosHeaders.d.ts.map +1 -1
- package/dist/lib/core/FaxiosHeaders.js +31 -7
- package/dist/lib/core/FaxiosHeaders.js.map +1 -1
- package/dist/lib/core/dispatchRequest.d.ts.map +1 -1
- package/dist/lib/core/dispatchRequest.js +6 -1
- package/dist/lib/core/dispatchRequest.js.map +1 -1
- package/dist/lib/env/data.d.ts +1 -1
- package/dist/lib/env/data.js +1 -1
- package/dist/lib/helpers/progressEventReducer.d.ts.map +1 -1
- package/dist/lib/helpers/progressEventReducer.js +14 -1
- package/dist/lib/helpers/progressEventReducer.js.map +1 -1
- package/dist/lib/helpers/toFormData.d.ts.map +1 -1
- package/dist/lib/helpers/toFormData.js +12 -18
- package/dist/lib/helpers/toFormData.js.map +1 -1
- package/dist/lib/helpers/trackStream.d.ts.map +1 -1
- package/dist/lib/helpers/trackStream.js +27 -3
- package/dist/lib/helpers/trackStream.js.map +1 -1
- package/dist/lib/types.d.ts +14 -0
- package/dist/lib/types.d.ts.map +1 -1
- package/dist/lib/utils.d.ts.map +1 -1
- package/dist/lib/utils.js +9 -3
- package/dist/lib/utils.js.map +1 -1
- package/package.json +1 -1
- package/skills/configure-requests/SKILL.md +6 -3
- package/skills/configure-requests/references/axios-differences.md +1 -1
- package/src/lib/adapters/fetch.ts +152 -94
- package/src/lib/core/FaxiosHeaders.ts +34 -7
- package/src/lib/core/dispatchRequest.ts +6 -1
- package/src/lib/env/data.ts +1 -1
- package/src/lib/helpers/progressEventReducer.ts +14 -1
- package/src/lib/helpers/toFormData.ts +15 -19
- package/src/lib/helpers/trackStream.ts +38 -10
- package/src/lib/types.ts +14 -0
- package/src/lib/utils.ts +9 -3
|
@@ -202,6 +202,9 @@ catch (error) {
|
|
|
202
202
|
schema step and right before `fetch`, so an abort during pre-flight work sends nothing.
|
|
203
203
|
- A timeout always rejects with `ETIMEDOUT`. `ECONNABORTED` is never raised, and
|
|
204
204
|
`transitional.clarifyTimeoutError` has no effect.
|
|
205
|
+
- With `responseType: "stream"`, `timeout` and `signal` stay active until the body is fully
|
|
206
|
+
read or cancelled. Reading a stalled body rejects with `ETIMEDOUT` or `ERR_CANCELED`.
|
|
207
|
+
Cancel a body you don't read so the timer and signal listener are released.
|
|
205
208
|
|
|
206
209
|
### Handle errors by code
|
|
207
210
|
|
|
@@ -356,9 +359,9 @@ export only. Instances from `create()` do not have them; import them by name ins
|
|
|
356
359
|
`faxios.plugins` is undefined. Import from `@gcmdev/faxios/plugins/<name>`.
|
|
357
360
|
7. **MEDIUM: Calling statics on an instance.** `api.isCancel(e)` is undefined on a `create()`
|
|
358
361
|
instance. Use the named import.
|
|
359
|
-
8. **MEDIUM: Expecting `onUploadProgress` everywhere.** It fires only where
|
|
360
|
-
request body (
|
|
361
|
-
progress
|
|
362
|
+
8. **MEDIUM: Expecting `onUploadProgress` everywhere.** It fires only where faxios can stream
|
|
363
|
+
the request body (Node; not Deno or Bun). In browsers and web workers faxios never streams the
|
|
364
|
+
body for progress, so the callback is not called; do not promise it there. Source: `src/lib/adapters/fetch.ts` (`isBrowserRuntime`).
|
|
362
365
|
9. **MEDIUM: Retrying POST by accident or by assumption.** The retry plugin skips POST and PATCH
|
|
363
366
|
unless they are listed in `methods`; a custom `retryOn` cannot override that.
|
|
364
367
|
10. **MEDIUM: Downloading a binary body without `responseType`.** The bytes are decoded as UTF-8
|
|
@@ -23,7 +23,7 @@ behaviour outside this list as unverified until the installed types and a test p
|
|
|
23
23
|
| `proxy: {...}` / `httpAgent` / `httpsAgent` | Runtime-level proxy config. In Node the project docs pass an `undici` `ProxyAgent` as `fetchOptions.dispatcher`, using the app's own `undici` dependency | Not faxios options; faxios passes the dispatcher to `fetch` but does not test proxying |
|
|
24
24
|
| `maxRedirects`, `beforeRedirect` | fetch follows redirects; `fetchOptions: { redirect: "manual" }` to stop | `ERR_FR_TOO_MANY_REDIRECTS` is never raised |
|
|
25
25
|
| `maxRate`, `socketPath`, `decompress`, `insecureHTTPParser`, `httpVersion`, HTTP/2 options, `lookup`, `family` | none | Removed; ignored at runtime when untyped |
|
|
26
|
-
| `onUploadProgress` | same option | Fires only where fetch streams request bodies (Node);
|
|
26
|
+
| `onUploadProgress` | same option | Fires only where fetch streams request bodies (Node); never called in browsers or web workers |
|
|
27
27
|
| `responseType: "document"` | none in practice | Falls back to text |
|
|
28
28
|
| retry via `axios-retry` | `retry()` from `@gcmdev/faxios/plugins/retry` | POST/PATCH excluded by default |
|
|
29
29
|
| auth header interceptor | `authBearer()` from `@gcmdev/faxios/plugins/auth-bearer` | Sends only to the baseURL origin by default |
|
|
@@ -81,10 +81,12 @@ const rejectUsedStreamBody = (data: unknown, config: InternalFaxiosRequestConfig
|
|
|
81
81
|
if (isWebStream) sentStreamBodies.add(body);
|
|
82
82
|
};
|
|
83
83
|
|
|
84
|
+
const isBrowserRuntime = platform.hasStandardBrowserEnv || platform.hasStandardBrowserWebWorkerEnv;
|
|
85
|
+
|
|
84
86
|
// Server runtimes default User-Agent to their own (e.g. "node"), so name faxios there.
|
|
85
87
|
// Browsers send their own User-Agent; setting one makes every cross-origin request
|
|
86
88
|
// need a CORS preflight that most APIs don't allow.
|
|
87
|
-
const setsUserAgent = !
|
|
89
|
+
const setsUserAgent = !isBrowserRuntime;
|
|
88
90
|
const applyDefaultUserAgent = (headers: FaxiosRequestHeaders): void => {
|
|
89
91
|
if (setsUserAgent) headers.set("User-Agent", "faxios/" + VERSION, false);
|
|
90
92
|
};
|
|
@@ -171,8 +173,16 @@ type FetchEnv = {
|
|
|
171
173
|
Response?: unknown;
|
|
172
174
|
};
|
|
173
175
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
+
type FetchAdapter = (config: InternalFaxiosRequestConfig) => Promise<unknown>;
|
|
177
|
+
|
|
178
|
+
// Own reads only: a polluted Object.prototype.fetch must never become the transport.
|
|
179
|
+
const readOwn = (obj: unknown, key: string): unknown =>
|
|
180
|
+
typeof obj === "object" && obj !== null && utils.hasOwnProp(obj, key) ? Reflect.get(obj, key) : undefined;
|
|
181
|
+
const ownEnv = (config: unknown, key: keyof FetchEnv): unknown => readOwn(readOwn(config, "env"), key);
|
|
182
|
+
|
|
183
|
+
// Feature detection depends only on the Request and Response constructors, so one adapter
|
|
184
|
+
// serves every `env.fetch`. `env.fetch` is read for each request.
|
|
185
|
+
const factory = (env: Omit<FetchEnv, "fetch">): FetchAdapter => {
|
|
176
186
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
177
187
|
const globalObject: Record<string, unknown> = utils.global ?? globalThis;
|
|
178
188
|
const ReadableStream = globalObject["ReadableStream"] as
|
|
@@ -183,22 +193,11 @@ const factory = (env: FetchEnv) => {
|
|
|
183
193
|
| undefined;
|
|
184
194
|
|
|
185
195
|
// `undefined` falls back to the global constructor; `null` disables it.
|
|
186
|
-
const envFetch = env.fetch as FetchFn | undefined;
|
|
187
196
|
const Request = (env.Request === undefined ? globalObject["Request"] : env.Request) as AnyConstructor | undefined;
|
|
188
197
|
const Response = (env.Response === undefined ? globalObject["Response"] : env.Response) as AnyConstructor | undefined;
|
|
189
|
-
const _globalFetch = (globalThis as unknown as Record<string, unknown>)[
|
|
190
|
-
"fetch"
|
|
191
|
-
];
|
|
192
|
-
const isFetchSupported = envFetch
|
|
193
|
-
? isFunction(envFetch)
|
|
194
|
-
: isFunction(_globalFetch);
|
|
195
198
|
const isRequestSupported = isFunction(Request);
|
|
196
199
|
const isResponseSupported = isFunction(Response);
|
|
197
200
|
|
|
198
|
-
if (!isFetchSupported) {
|
|
199
|
-
return false;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
201
|
// Only members this runtime's Request exposes, so an engine that rejects members it doesn't
|
|
203
202
|
// implement (Cloudflare Workers) is never sent one.
|
|
204
203
|
const requestInitDefaults = Object.entries(REQUEST_INIT_DEFAULTS).filter(([ key ]) =>
|
|
@@ -209,7 +208,7 @@ const factory = (env: FetchEnv) => {
|
|
|
209
208
|
// .mode or undici .dispatcher apply to every request. Every own enumerable key of fetchOptions
|
|
210
209
|
// is copied, so runtime extensions (Next.js `next`, Workers `cf`, Bun `tls`) still pass through;
|
|
211
210
|
// inherited keys never do. The caller sets signal, method, headers, body, duplex, credentials.
|
|
212
|
-
const initFromFetchOptions = (fetchOptions: unknown): Record<string, unknown> => {
|
|
211
|
+
const initFromFetchOptions = (fetchOptions: unknown, envFetch: FetchFn | undefined): Record<string, unknown> => {
|
|
213
212
|
const init: Record<string, unknown> = Object.create(null);
|
|
214
213
|
for (const [ key, value ] of requestInitDefaults) init[key] = value;
|
|
215
214
|
// Node's global fetch reads `dispatcher` the same way; pin it to undici's own global one.
|
|
@@ -278,6 +277,13 @@ const factory = (env: FetchEnv) => {
|
|
|
278
277
|
return duplexAccessed && !hasContentType;
|
|
279
278
|
});
|
|
280
279
|
|
|
280
|
+
// Outside browsers a FormData body under maxBodyLength is streamed and counted as fetch reads it;
|
|
281
|
+
// browsers can't send a stream body (see buildUploadStream).
|
|
282
|
+
const countsFormDataWhileSent = !isBrowserRuntime && supportsRequestStream;
|
|
283
|
+
// Bodies counted as fetch reads them when maxBodyLength is set.
|
|
284
|
+
const isCountedWhileSent = (body: unknown): boolean =>
|
|
285
|
+
utils.isReadableStream!(body) || utils.isStream(body) || (countsFormDataWhileSent && utils.isSpecCompliantForm(body));
|
|
286
|
+
|
|
281
287
|
const supportsResponseStream =
|
|
282
288
|
isResponseSupported &&
|
|
283
289
|
isReadableStreamSupported &&
|
|
@@ -294,26 +300,22 @@ const factory = (env: FetchEnv) => {
|
|
|
294
300
|
stream: supportsResponseStream && ((res: AnyResponse) => res.body),
|
|
295
301
|
};
|
|
296
302
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
FaxiosError.ERR_NOT_SUPPORT,
|
|
312
|
-
config as InternalFaxiosRequestConfig
|
|
313
|
-
);
|
|
314
|
-
});
|
|
303
|
+
[ "text", "arrayBuffer", "blob", "formData", "stream" ].forEach(type => {
|
|
304
|
+
!resolvers[type] &&
|
|
305
|
+
(resolvers[type] = (res: AnyResponse, config?: unknown) => {
|
|
306
|
+
const method = (res as Record<string, unknown>)[type];
|
|
307
|
+
|
|
308
|
+
if (method) {
|
|
309
|
+
return (method as (this: AnyResponse) => unknown).call(res);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
throw new FaxiosError(
|
|
313
|
+
`Response type '${type}' is not supported`,
|
|
314
|
+
FaxiosError.ERR_NOT_SUPPORT,
|
|
315
|
+
config as InternalFaxiosRequestConfig
|
|
316
|
+
);
|
|
315
317
|
});
|
|
316
|
-
|
|
318
|
+
});
|
|
317
319
|
|
|
318
320
|
const getBodyLength = async (body: unknown): Promise<number | undefined> => {
|
|
319
321
|
if (body == null) {
|
|
@@ -324,14 +326,6 @@ const factory = (env: FetchEnv) => {
|
|
|
324
326
|
return (body as { size: number; }).size;
|
|
325
327
|
}
|
|
326
328
|
|
|
327
|
-
if (utils.isSpecCompliantForm(body)) {
|
|
328
|
-
const _request = new (Request as AnyConstructor)(platform.origin, {
|
|
329
|
-
method: "POST",
|
|
330
|
-
body,
|
|
331
|
-
}) as AnyRequest;
|
|
332
|
-
return (await _request.arrayBuffer()).byteLength;
|
|
333
|
-
}
|
|
334
|
-
|
|
335
329
|
if (utils.isArrayBufferView(body) || utils.isArrayBuffer(body)) {
|
|
336
330
|
return (body as ArrayBuffer | ArrayBufferView).byteLength;
|
|
337
331
|
}
|
|
@@ -346,6 +340,23 @@ const factory = (env: FetchEnv) => {
|
|
|
346
340
|
return undefined;
|
|
347
341
|
};
|
|
348
342
|
|
|
343
|
+
// The bytes of a FormData body's field values and files, from their sizes, without reading the
|
|
344
|
+
// body. A lower bound: the multipart body adds boundaries and part headers. Undefined for any
|
|
345
|
+
// other body.
|
|
346
|
+
const getFormDataFieldsLength = async (body: unknown): Promise<number | undefined> => {
|
|
347
|
+
if (!utils.isSpecCompliantForm(body)) {
|
|
348
|
+
return undefined;
|
|
349
|
+
}
|
|
350
|
+
let length = 0;
|
|
351
|
+
for (const [ , value ] of body as Iterable<[string, unknown]>) {
|
|
352
|
+
// A FormData value is a string or a File.
|
|
353
|
+
const size = utils.isString(value) ? undefined : utils.getSafeProp(value, "size");
|
|
354
|
+
// eslint-disable-next-line no-await-in-loop
|
|
355
|
+
length += typeof size === "number" ? size : (await encodeText(String(value))).byteLength;
|
|
356
|
+
}
|
|
357
|
+
return length;
|
|
358
|
+
};
|
|
359
|
+
|
|
349
360
|
const resolveBodyLength = async (
|
|
350
361
|
headers: FaxiosRequestHeaders,
|
|
351
362
|
body: unknown
|
|
@@ -436,10 +447,13 @@ const factory = (env: FetchEnv) => {
|
|
|
436
447
|
}
|
|
437
448
|
// Enforce maxBodyLength against known-size bodies before dispatch using
|
|
438
449
|
// the body's actual size — never a caller-declared Content-Length.
|
|
450
|
+
// A FormData body isn't measured, since that buffers it whole: its values alone over the limit
|
|
451
|
+
// reject it here, and where it's streamed the byte counter covers the multipart framing too.
|
|
439
452
|
if (hasMaxBodyLength && method !== "get" && method !== "head") {
|
|
440
453
|
const outboundLength = await getBodyLength(data);
|
|
441
|
-
|
|
442
|
-
|
|
454
|
+
const checkedLength = outboundLength ?? await getFormDataFieldsLength(data);
|
|
455
|
+
if (typeof checkedLength === "number" && isFinite(checkedLength)) {
|
|
456
|
+
if (checkedLength > maxBodyLength) {
|
|
443
457
|
throw new FaxiosError(
|
|
444
458
|
"Request body larger than maxBodyLength limit",
|
|
445
459
|
FaxiosError.ERR_BAD_REQUEST,
|
|
@@ -501,6 +515,10 @@ const factory = (env: FetchEnv) => {
|
|
|
501
515
|
request: unknown,
|
|
502
516
|
pendingBodyErrorRef: PendingBodyErrorRef
|
|
503
517
|
): Promise<{ data: unknown; requestContentLength: number | undefined; }> => {
|
|
518
|
+
// Browsers can't be given a stream body for upload progress: Chromium sends one only over
|
|
519
|
+
// HTTP/2 or later and WebKit not at all, so the upload would fail. onUploadProgress isn't
|
|
520
|
+
// called there (#209).
|
|
521
|
+
const uploadProgress = isBrowserRuntime ? undefined : onUploadProgress;
|
|
504
522
|
const _makeBodyLengthError = () =>
|
|
505
523
|
new FaxiosError(
|
|
506
524
|
"Request body larger than maxBodyLength limit",
|
|
@@ -524,14 +542,21 @@ const factory = (env: FetchEnv) => {
|
|
|
524
542
|
}
|
|
525
543
|
onProgress && onProgress(loadedBytes);
|
|
526
544
|
},
|
|
527
|
-
|
|
545
|
+
(err?: unknown) => {
|
|
546
|
+
// A chunk trackStream rejected (not bytes) surfaces as the coded error, not ERR_NETWORK.
|
|
547
|
+
if (err instanceof FaxiosError && !pendingBodyErrorRef.value) {
|
|
548
|
+
err.config = config;
|
|
549
|
+
pendingBodyErrorRef.value = err;
|
|
550
|
+
}
|
|
551
|
+
flush && flush();
|
|
552
|
+
}
|
|
528
553
|
);
|
|
529
554
|
|
|
530
555
|
if (
|
|
531
556
|
supportsRequestStream &&
|
|
532
557
|
method !== "get" &&
|
|
533
558
|
method !== "head" &&
|
|
534
|
-
(
|
|
559
|
+
(uploadProgress || mustEnforceStreamBody)
|
|
535
560
|
) {
|
|
536
561
|
requestContentLength =
|
|
537
562
|
requestContentLength == null
|
|
@@ -546,7 +571,7 @@ const factory = (env: FetchEnv) => {
|
|
|
546
571
|
body: data,
|
|
547
572
|
duplex: "half",
|
|
548
573
|
}) as AnyRequest;
|
|
549
|
-
data = applyUploadProgress(_request, data, headers, requestContentLength,
|
|
574
|
+
data = applyUploadProgress(_request, data, headers, requestContentLength, uploadProgress, _trackStream);
|
|
550
575
|
}
|
|
551
576
|
}
|
|
552
577
|
else if (
|
|
@@ -584,11 +609,11 @@ const factory = (env: FetchEnv) => {
|
|
|
584
609
|
hasMaxContentLength: boolean,
|
|
585
610
|
maxContentLength: number,
|
|
586
611
|
isStreamResponse: boolean,
|
|
587
|
-
|
|
612
|
+
release: (err?: unknown) => void,
|
|
588
613
|
config: InternalFaxiosRequestConfig,
|
|
589
614
|
request: unknown
|
|
590
615
|
): AnyResponse => {
|
|
591
|
-
const needsWrapping = onDownloadProgress || hasMaxContentLength ||
|
|
616
|
+
const needsWrapping = !!onDownloadProgress || hasMaxContentLength || isStreamResponse;
|
|
592
617
|
if (!supportsResponseStream || !response.body || !needsWrapping) {
|
|
593
618
|
return response;
|
|
594
619
|
}
|
|
@@ -598,9 +623,15 @@ const factory = (env: FetchEnv) => {
|
|
|
598
623
|
options[prop] = response[prop];
|
|
599
624
|
});
|
|
600
625
|
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
)
|
|
626
|
+
// Content-Length is the encoded size; the stream yields decoded bytes, so
|
|
627
|
+
// the total is unknown when the body is content-encoded.
|
|
628
|
+
const contentEncoding = String(responseHeaders.get("content-encoding") ?? "").trim()
|
|
629
|
+
.toLowerCase();
|
|
630
|
+
const responseContentLength = contentEncoding && contentEncoding !== "identity"
|
|
631
|
+
? undefined
|
|
632
|
+
: utils.toFiniteNumber(
|
|
633
|
+
(responseHeaders.getContentLength as () => unknown)()
|
|
634
|
+
);
|
|
604
635
|
|
|
605
636
|
const [ onProgress, flush ] = onDownloadProgress
|
|
606
637
|
? progressEventDecorator(
|
|
@@ -635,17 +666,18 @@ const factory = (env: FetchEnv) => {
|
|
|
635
666
|
response.body,
|
|
636
667
|
DEFAULT_CHUNK_SIZE,
|
|
637
668
|
onChunkProgress,
|
|
638
|
-
() => {
|
|
669
|
+
(err?: unknown) => {
|
|
639
670
|
flush && flush();
|
|
640
|
-
|
|
671
|
+
release(err);
|
|
641
672
|
}
|
|
642
673
|
),
|
|
643
674
|
options
|
|
644
675
|
) as AnyResponse;
|
|
645
676
|
};
|
|
646
677
|
|
|
647
|
-
|
|
648
|
-
|
|
678
|
+
// A stream body releases the request when it ends; a response without a body has nothing to wait for.
|
|
679
|
+
const unsubscribeOnComplete = (isStreamResponse: boolean, response: AnyResponse, unsubscribe: () => void) => {
|
|
680
|
+
if (!isStreamResponse || response.body == null) unsubscribe();
|
|
649
681
|
};
|
|
650
682
|
|
|
651
683
|
return async (config: InternalFaxiosRequestConfig) => {
|
|
@@ -680,6 +712,7 @@ const factory = (env: FetchEnv) => {
|
|
|
680
712
|
? (config as unknown as Record<string, unknown>)[key]
|
|
681
713
|
: undefined;
|
|
682
714
|
|
|
715
|
+
const envFetch = ownEnv(config, "fetch") as FetchFn | undefined;
|
|
683
716
|
const _fetch: FetchFn = (envFetch || (globalThis as unknown as Record<string, unknown>)["fetch"]) as FetchFn;
|
|
684
717
|
|
|
685
718
|
if (timeout != null && !Number.isFinite(Number(timeout))) {
|
|
@@ -694,13 +727,16 @@ const factory = (env: FetchEnv) => {
|
|
|
694
727
|
responseType ? (responseType + "").toLowerCase() : "text"
|
|
695
728
|
) as typeof responseType;
|
|
696
729
|
|
|
730
|
+
// Owns the request's cleanup: aborting it on a failure aborts fetch, which releases the
|
|
731
|
+
// connection and any body still being sent or received.
|
|
732
|
+
const requestController = new AbortController();
|
|
697
733
|
const composedSignal = composeSignals(
|
|
698
|
-
[ signal ],
|
|
734
|
+
[ signal, requestController.signal ],
|
|
699
735
|
timeout,
|
|
700
736
|
timeoutErrorMessage
|
|
701
|
-
) as ComposedSignal
|
|
737
|
+
) as ComposedSignal;
|
|
702
738
|
|
|
703
|
-
const unsubscribe = composedSignal
|
|
739
|
+
const unsubscribe = composedSignal.unsubscribe!.bind(composedSignal);
|
|
704
740
|
|
|
705
741
|
let request: null | Request = null;
|
|
706
742
|
const pendingBodyErrorRef: PendingBodyErrorRef = { value: null };
|
|
@@ -724,8 +760,7 @@ const factory = (env: FetchEnv) => {
|
|
|
724
760
|
|
|
725
761
|
// A streamed body under maxBodyLength must be counted as fetch consumes
|
|
726
762
|
// it; its size is never trusted from a caller-declared Content-Length.
|
|
727
|
-
const
|
|
728
|
-
const mustEnforceStreamBody = hasMaxBodyLength && isStreamLike;
|
|
763
|
+
const mustEnforceStreamBody = hasMaxBodyLength && isCountedWhileSent(data);
|
|
729
764
|
|
|
730
765
|
({ data } = await buildUploadStream(
|
|
731
766
|
data,
|
|
@@ -762,7 +797,7 @@ const factory = (env: FetchEnv) => {
|
|
|
762
797
|
// Case-insensitive check: toByteStringHeaderObject may normalize to lowercase.
|
|
763
798
|
data = toWebStream(await encodeBodyIfNeeded(data, serializedHeaders, encodeText));
|
|
764
799
|
|
|
765
|
-
const resolvedOptions = initFromFetchOptions(fetchOptions);
|
|
800
|
+
const resolvedOptions = initFromFetchOptions(fetchOptions, envFetch);
|
|
766
801
|
resolvedOptions["signal"] = composedSignal;
|
|
767
802
|
resolvedOptions["method"] = (method as string).toUpperCase();
|
|
768
803
|
resolvedOptions["headers"] = serializedHeaders;
|
|
@@ -788,6 +823,14 @@ const factory = (env: FetchEnv) => {
|
|
|
788
823
|
supportsResponseStream &&
|
|
789
824
|
(responseType === "stream" || responseType === "response");
|
|
790
825
|
|
|
826
|
+
// A stream body outlives this call, so its end, error or cancel releases the request.
|
|
827
|
+
// A buffered body is released below, or by the catch on failure.
|
|
828
|
+
const releaseStreamBody = (bodyErr?: unknown): void => {
|
|
829
|
+
if (!isStreamResponse) return;
|
|
830
|
+
if (bodyErr !== undefined) requestController.abort(bodyErr);
|
|
831
|
+
unsubscribe();
|
|
832
|
+
};
|
|
833
|
+
|
|
791
834
|
response = buildDownloadStream(
|
|
792
835
|
response,
|
|
793
836
|
responseHeaders,
|
|
@@ -795,7 +838,7 @@ const factory = (env: FetchEnv) => {
|
|
|
795
838
|
hasMaxContentLength,
|
|
796
839
|
maxContentLength!,
|
|
797
840
|
isStreamResponse,
|
|
798
|
-
|
|
841
|
+
releaseStreamBody,
|
|
799
842
|
config,
|
|
800
843
|
request
|
|
801
844
|
);
|
|
@@ -816,12 +859,12 @@ const factory = (env: FetchEnv) => {
|
|
|
816
859
|
request
|
|
817
860
|
);
|
|
818
861
|
|
|
819
|
-
unsubscribeOnComplete(isStreamResponse, unsubscribe);
|
|
862
|
+
unsubscribeOnComplete(isStreamResponse, response, unsubscribe);
|
|
820
863
|
|
|
821
864
|
return await new Promise((resolve, reject) => {
|
|
822
865
|
settle(resolve, reject, {
|
|
823
866
|
data: responseData,
|
|
824
|
-
headers:
|
|
867
|
+
headers: responseHeaders,
|
|
825
868
|
status: response.status,
|
|
826
869
|
statusText: response.statusText,
|
|
827
870
|
config,
|
|
@@ -830,42 +873,57 @@ const factory = (env: FetchEnv) => {
|
|
|
830
873
|
});
|
|
831
874
|
}
|
|
832
875
|
catch (err) {
|
|
833
|
-
|
|
834
|
-
|
|
876
|
+
let error: unknown = err;
|
|
877
|
+
try {
|
|
878
|
+
handleFetchCaughtError(err, composedSignal, pendingBodyErrorRef, config, request);
|
|
879
|
+
}
|
|
880
|
+
catch (classified) {
|
|
881
|
+
error = classified;
|
|
882
|
+
}
|
|
883
|
+
// Classify first: the abort below sets the composed signal's reason.
|
|
884
|
+
requestController.abort(error);
|
|
885
|
+
unsubscribe();
|
|
886
|
+
throw error;
|
|
835
887
|
}
|
|
836
888
|
};
|
|
837
889
|
};
|
|
838
890
|
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
const fetch = own("fetch");
|
|
846
|
-
const Request = own("Request");
|
|
847
|
-
const Response = own("Response");
|
|
848
|
-
const seeds: Array<unknown> = [ Request, Response, fetch ];
|
|
849
|
-
|
|
850
|
-
const len = seeds.length;
|
|
851
|
-
let i = len;
|
|
852
|
-
let seed: unknown;
|
|
853
|
-
let target: unknown;
|
|
854
|
-
let map: Map<unknown, unknown> = seedCache;
|
|
855
|
-
|
|
856
|
-
while (i--) {
|
|
857
|
-
seed = seeds[i];
|
|
858
|
-
target = map.get(seed);
|
|
859
|
-
|
|
860
|
-
if (target === undefined) {
|
|
861
|
-
target = i ? new Map<unknown, unknown>() : factory({ fetch, Request, Response });
|
|
862
|
-
map.set(seed, target);
|
|
863
|
-
}
|
|
891
|
+
// One adapter per Request/Response pair. Weak keys let a per-request `env.Request` or
|
|
892
|
+
// `env.Response` be collected. `undefined` (use the global) and any non-function (disabled)
|
|
893
|
+
// get stand-in keys.
|
|
894
|
+
const GLOBAL_CTOR = {};
|
|
895
|
+
const NO_CTOR = {};
|
|
896
|
+
const adapters = new WeakMap<object, WeakMap<object, FetchAdapter>>();
|
|
864
897
|
|
|
865
|
-
|
|
898
|
+
const ctorKey = (ctor: unknown): object => {
|
|
899
|
+
if (ctor === undefined) return GLOBAL_CTOR;
|
|
900
|
+
return isFunction(ctor) ? (ctor as object) : NO_CTOR;
|
|
901
|
+
};
|
|
902
|
+
|
|
903
|
+
// `false` tells dispatchRequest that this runtime has no fetch.
|
|
904
|
+
// eslint-disable-next-line sonarjs/function-return-type
|
|
905
|
+
export const getFetch = (config?: { env?: Record<string, unknown>; }): false | FetchAdapter => {
|
|
906
|
+
const fetch = ownEnv(config, "fetch");
|
|
907
|
+
if (!isFunction(fetch || (globalThis as unknown as Record<string, unknown>)["fetch"])) {
|
|
908
|
+
return false;
|
|
866
909
|
}
|
|
867
910
|
|
|
868
|
-
|
|
911
|
+
const Request = ownEnv(config, "Request");
|
|
912
|
+
const Response = ownEnv(config, "Response");
|
|
913
|
+
const requestKey = ctorKey(Request);
|
|
914
|
+
const responseKey = ctorKey(Response);
|
|
915
|
+
|
|
916
|
+
let byResponse = adapters.get(requestKey);
|
|
917
|
+
if (!byResponse) {
|
|
918
|
+
byResponse = new WeakMap();
|
|
919
|
+
adapters.set(requestKey, byResponse);
|
|
920
|
+
}
|
|
921
|
+
let adapter = byResponse.get(responseKey);
|
|
922
|
+
if (!adapter) {
|
|
923
|
+
adapter = factory({ Request, Response });
|
|
924
|
+
byResponse.set(responseKey, adapter);
|
|
925
|
+
}
|
|
926
|
+
return adapter;
|
|
869
927
|
};
|
|
870
928
|
|
|
871
929
|
getFetch();
|
|
@@ -28,6 +28,25 @@ function normalizeHeader(header: string): string {
|
|
|
28
28
|
.toLowerCase();
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
// Per instance: lowercased own key -> own key. Callers may also write and delete headers as plain
|
|
32
|
+
// properties, so an entry is only trusted while its key is still own, and a miss rebuilds the index.
|
|
33
|
+
const keyIndexes = new WeakMap<object, Map<string, string>>();
|
|
34
|
+
|
|
35
|
+
function indexKeys(headers: object): Map<string, string> {
|
|
36
|
+
const index = new Map<string, string>();
|
|
37
|
+
// Later keys overwrite earlier ones, matching utils.findKey, which scans from the end.
|
|
38
|
+
for (const key of Object.keys(headers)) {
|
|
39
|
+
index.set(key.toLowerCase(), key);
|
|
40
|
+
}
|
|
41
|
+
keyIndexes.set(headers, index);
|
|
42
|
+
return index;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function findHeaderKey(headers: object, lHeader: string): string | undefined {
|
|
46
|
+
const key = keyIndexes.get(headers)?.get(lHeader);
|
|
47
|
+
return key !== undefined && utils.hasOwnProp(headers, key) ? key : indexKeys(headers).get(lHeader);
|
|
48
|
+
}
|
|
49
|
+
|
|
31
50
|
// eslint-disable-next-line sonarjs/function-return-type
|
|
32
51
|
function normalizeValue(
|
|
33
52
|
value: FaxiosHeaderValue | undefined
|
|
@@ -187,6 +206,9 @@ class FaxiosHeaders {
|
|
|
187
206
|
rewrite?: boolean
|
|
188
207
|
): this {
|
|
189
208
|
const self = this;
|
|
209
|
+
// Set once a lookup misses, which rebuilds the index from the own keys. Only setHeader writes
|
|
210
|
+
// to self until this call returns, so from then on the index is complete and a miss is final.
|
|
211
|
+
let index: Map<string, string> | undefined;
|
|
190
212
|
|
|
191
213
|
function setHeader(
|
|
192
214
|
_value: unknown,
|
|
@@ -199,7 +221,11 @@ class FaxiosHeaders {
|
|
|
199
221
|
return;
|
|
200
222
|
}
|
|
201
223
|
|
|
202
|
-
const key =
|
|
224
|
+
const key = index ? index.get(lHeader) : findHeaderKey(self, lHeader);
|
|
225
|
+
if (key === undefined) {
|
|
226
|
+
index ??= keyIndexes.get(self)!;
|
|
227
|
+
index.set(String(_header).toLowerCase(), _header);
|
|
228
|
+
}
|
|
203
229
|
|
|
204
230
|
if (
|
|
205
231
|
!key ||
|
|
@@ -260,7 +286,7 @@ class FaxiosHeaders {
|
|
|
260
286
|
header = normalizeHeader(header);
|
|
261
287
|
|
|
262
288
|
if (header) {
|
|
263
|
-
const key =
|
|
289
|
+
const key = findHeaderKey(this, header);
|
|
264
290
|
|
|
265
291
|
if (key) {
|
|
266
292
|
const value = (this as Record<string, unknown>)[
|
|
@@ -299,7 +325,7 @@ class FaxiosHeaders {
|
|
|
299
325
|
header = normalizeHeader(header);
|
|
300
326
|
|
|
301
327
|
if (header) {
|
|
302
|
-
const key =
|
|
328
|
+
const key = findHeaderKey(this, header);
|
|
303
329
|
|
|
304
330
|
return !!(
|
|
305
331
|
key &&
|
|
@@ -325,7 +351,7 @@ class FaxiosHeaders {
|
|
|
325
351
|
_header = normalizeHeader(_header);
|
|
326
352
|
|
|
327
353
|
if (_header) {
|
|
328
|
-
const key =
|
|
354
|
+
const key = findHeaderKey(self, _header);
|
|
329
355
|
|
|
330
356
|
if (
|
|
331
357
|
key &&
|
|
@@ -381,11 +407,12 @@ class FaxiosHeaders {
|
|
|
381
407
|
|
|
382
408
|
normalize(format: boolean = false): this {
|
|
383
409
|
const self = this;
|
|
384
|
-
|
|
410
|
+
// Lowercased normalized name -> normalized name, for names already kept.
|
|
411
|
+
const kept = new Map<string, string>();
|
|
385
412
|
|
|
386
413
|
utils.forEach(this, (value: unknown, _header: unknown) => {
|
|
387
414
|
const header = _header as string;
|
|
388
|
-
const key =
|
|
415
|
+
const key = kept.get(header.toLowerCase());
|
|
389
416
|
|
|
390
417
|
if (key) {
|
|
391
418
|
(self as Record<string, unknown>)[key] = normalizeValue(
|
|
@@ -405,7 +432,7 @@ class FaxiosHeaders {
|
|
|
405
432
|
value as FaxiosHeaderValue
|
|
406
433
|
);
|
|
407
434
|
|
|
408
|
-
|
|
435
|
+
kept.set(normalized.toLowerCase(), normalized);
|
|
409
436
|
});
|
|
410
437
|
|
|
411
438
|
return this;
|
|
@@ -104,7 +104,12 @@ export default async function dispatchRequest(this: unknown, config: InternalFax
|
|
|
104
104
|
|
|
105
105
|
config.data = transformData.call(config, config.transformRequest);
|
|
106
106
|
|
|
107
|
-
|
|
107
|
+
const data: unknown = config.data;
|
|
108
|
+
// Binary and stream bodies keep their own type: fetch uses `Blob.type` or sends none.
|
|
109
|
+
const isBinaryOrStream = utils.isBlob(data) || utils.isFile(data) || utils.isArrayBuffer(data) || utils.isArrayBufferView(data) ||
|
|
110
|
+
utils.isReadableStream?.(data) || utils.isStream(data) ||
|
|
111
|
+
(typeof data === "object" && data !== null && Symbol.asyncIterator in data);
|
|
112
|
+
if (data != null && !utils.isFormData(data) && !isBinaryOrStream && [ "post", "put", "patch" ].indexOf(config.method as string) !== -1) {
|
|
108
113
|
(config.headers as unknown as { setContentType: (v: string, r: boolean) => void; }).setContentType("application/x-www-form-urlencoded", false);
|
|
109
114
|
}
|
|
110
115
|
|
package/src/lib/env/data.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = "0.2.
|
|
1
|
+
export const VERSION = "0.2.3";
|
|
@@ -51,7 +51,20 @@ export const progressEventDecorator = (total: number | undefined, throttled: Ret
|
|
|
51
51
|
];
|
|
52
52
|
};
|
|
53
53
|
|
|
54
|
+
// Progress listeners are caller code: any error they throw (or a promise they
|
|
55
|
+
// reject) is caught and ignored so it can't become an uncaught exception or
|
|
56
|
+
// change how the request settles.
|
|
54
57
|
export const asyncDecorator =
|
|
55
58
|
(fn: (...args: Array<unknown>) => unknown) =>
|
|
56
59
|
(...args: Array<unknown>) =>
|
|
57
|
-
utils.asap(() =>
|
|
60
|
+
utils.asap(() => {
|
|
61
|
+
try {
|
|
62
|
+
const result = fn(...args);
|
|
63
|
+
if (utils.isThenable(result)) {
|
|
64
|
+
(result as PromiseLike<unknown>).then(undefined, () => {});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// Ignored by design: see the onUploadProgress/onDownloadProgress docs.
|
|
69
|
+
}
|
|
70
|
+
});
|