@gcmdev/faxios 0.2.1 → 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/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/adapters/fetch-helpers.d.ts +1 -1
- package/dist/lib/adapters/fetch-helpers.d.ts.map +1 -1
- 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 +210 -92
- package/dist/lib/adapters/fetch.js.map +1 -1
- package/dist/lib/cancel/CanceledError.d.ts +1 -1
- package/dist/lib/cancel/CanceledError.d.ts.map +1 -1
- package/dist/lib/core/Faxios.d.ts +3 -3
- package/dist/lib/core/Faxios.d.ts.map +1 -1
- package/dist/lib/core/Faxios.js +8 -4
- package/dist/lib/core/Faxios.js.map +1 -1
- package/dist/lib/core/FaxiosError.d.ts +2 -2
- package/dist/lib/core/FaxiosError.d.ts.map +1 -1
- package/dist/lib/core/FaxiosHeaders.d.ts +8 -8
- 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/define.d.ts +3 -3
- package/dist/lib/core/define.d.ts.map +1 -1
- package/dist/lib/core/dispatchRequest.d.ts +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/core/mergeConfig.d.ts +2 -2
- package/dist/lib/core/mergeConfig.d.ts.map +1 -1
- package/dist/lib/core/mergeConfig.js +40 -2
- package/dist/lib/core/mergeConfig.js.map +1 -1
- package/dist/lib/core/route.d.ts +1 -1
- package/dist/lib/core/route.d.ts.map +1 -1
- package/dist/lib/core/route.js +2 -5
- package/dist/lib/core/route.js.map +1 -1
- package/dist/lib/core/transformData.d.ts +2 -2
- package/dist/lib/core/transformData.d.ts.map +1 -1
- package/dist/lib/core/validateSchema.d.ts +2 -2
- package/dist/lib/core/validateSchema.d.ts.map +1 -1
- package/dist/lib/env/data.d.ts +1 -1
- package/dist/lib/env/data.js +1 -1
- package/dist/lib/faxios.d.ts +50 -50
- package/dist/lib/faxios.d.ts.map +1 -1
- package/dist/lib/helpers/cloneConfig.d.ts +0 -1
- package/dist/lib/helpers/cloneConfig.d.ts.map +1 -1
- package/dist/lib/helpers/cloneConfig.js +2 -2
- package/dist/lib/helpers/cloneConfig.js.map +1 -1
- package/dist/lib/helpers/cookies.d.ts +1 -1
- package/dist/lib/helpers/cookies.d.ts.map +1 -1
- package/dist/lib/helpers/formDataToJSON.d.ts +1 -1
- package/dist/lib/helpers/formDataToJSON.d.ts.map +1 -1
- package/dist/lib/helpers/formDataToJSON.js +1 -1
- package/dist/lib/helpers/formDataToJSON.js.map +1 -1
- package/dist/lib/helpers/parseHeaders.d.ts +1 -1
- package/dist/lib/helpers/parseHeaders.d.ts.map +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/substitutePathParams.d.ts.map +1 -1
- package/dist/lib/helpers/substitutePathParams.js +11 -3
- package/dist/lib/helpers/substitutePathParams.js.map +1 -1
- package/dist/lib/helpers/toFormData.d.ts.map +1 -1
- package/dist/lib/helpers/toFormData.js +16 -20
- 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/helpers/validator.d.ts +1 -1
- package/dist/lib/helpers/validator.d.ts.map +1 -1
- package/dist/lib/platform.d.ts.map +1 -1
- package/dist/lib/plugins/authBearer.d.ts +9 -5
- package/dist/lib/plugins/authBearer.d.ts.map +1 -1
- package/dist/lib/plugins/authBearer.js +6 -0
- package/dist/lib/plugins/authBearer.js.map +1 -1
- package/dist/lib/plugins/definePlugin.d.ts +3 -3
- package/dist/lib/plugins/definePlugin.d.ts.map +1 -1
- package/dist/lib/plugins/retry.d.ts +23 -23
- package/dist/lib/plugins/retry.d.ts.map +1 -1
- package/dist/lib/plugins/timing.d.ts +8 -8
- package/dist/lib/plugins/timing.d.ts.map +1 -1
- package/dist/lib/types/standard-schema.d.ts +6 -6
- package/dist/lib/types/standard-schema.d.ts.map +1 -1
- package/dist/lib/types.d.ts +106 -92
- package/dist/lib/types.d.ts.map +1 -1
- package/dist/lib/utils.d.ts +4 -3
- package/dist/lib/utils.d.ts.map +1 -1
- package/dist/lib/utils.js +18 -5
- package/dist/lib/utils.js.map +1 -1
- package/package.json +7 -4
- package/skills/configure-requests/SKILL.md +14 -8
- package/skills/configure-requests/references/axios-differences.md +1 -1
- package/skills/configure-requests/references/plugins.md +37 -6
- package/src/index.ts +15 -15
- package/src/lib/adapters/fetch-helpers.ts +3 -3
- package/src/lib/adapters/fetch.ts +263 -144
- package/src/lib/cancel/CanceledError.ts +1 -1
- package/src/lib/core/Faxios.ts +17 -13
- package/src/lib/core/FaxiosError.ts +3 -3
- package/src/lib/core/FaxiosHeaders.ts +50 -23
- package/src/lib/core/define.ts +7 -7
- package/src/lib/core/dispatchRequest.ts +8 -3
- package/src/lib/core/mergeConfig.ts +42 -4
- package/src/lib/core/prepareRequest.ts +1 -1
- package/src/lib/core/route.ts +3 -6
- package/src/lib/core/transformData.ts +2 -2
- package/src/lib/core/validateSchema.ts +4 -4
- package/src/lib/defaults/index.ts +1 -1
- package/src/lib/env/data.ts +1 -1
- package/src/lib/faxios.ts +55 -55
- package/src/lib/helpers/HttpStatusCode.ts +1 -1
- package/src/lib/helpers/cloneConfig.ts +2 -3
- package/src/lib/helpers/composeSignals.ts +4 -4
- package/src/lib/helpers/cookies.ts +3 -3
- package/src/lib/helpers/formDataToJSON.ts +4 -4
- package/src/lib/helpers/parseHeaders.ts +2 -2
- package/src/lib/helpers/progressEventReducer.ts +15 -2
- package/src/lib/helpers/sanitizeHeaderValue.ts +1 -1
- package/src/lib/helpers/substitutePathParams.ts +13 -3
- package/src/lib/helpers/throttle.ts +1 -1
- package/src/lib/helpers/toFormData.ts +26 -28
- package/src/lib/helpers/trackStream.ts +40 -12
- package/src/lib/helpers/validator.ts +3 -3
- package/src/lib/platform.ts +4 -4
- package/src/lib/plugins/authBearer.ts +14 -9
- package/src/lib/plugins/definePlugin.ts +3 -3
- package/src/lib/plugins/retry.ts +27 -27
- package/src/lib/plugins/timing.ts +5 -5
- package/src/lib/types/standard-schema.ts +6 -6
- package/src/lib/types.ts +139 -125
- package/src/lib/utils.ts +30 -15
|
@@ -6,19 +6,19 @@ import { VERSION } from "../env/data.js";
|
|
|
6
6
|
import composeSignals from "../helpers/composeSignals.js";
|
|
7
7
|
import estimateDataURLDecodedBytes from "../helpers/estimateDataURLDecodedBytes.js";
|
|
8
8
|
import {
|
|
9
|
-
|
|
9
|
+
asyncDecorator,
|
|
10
10
|
progressEventDecorator,
|
|
11
|
-
|
|
11
|
+
progressEventReducer
|
|
12
12
|
} from "../helpers/progressEventReducer.js";
|
|
13
13
|
import { toByteStringHeaderObject } from "../helpers/sanitizeHeaderValue.js";
|
|
14
14
|
import { trackStream } from "../helpers/trackStream.js";
|
|
15
15
|
import platform from "../platform.js";
|
|
16
16
|
import type {
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
FaxiosRequestHeaders,
|
|
18
|
+
InternalFaxiosRequestConfig
|
|
19
19
|
} from "../types.js";
|
|
20
20
|
import utils from "../utils.js";
|
|
21
|
-
import { checkDeclaredContentLength, checkMaterializedSize,
|
|
21
|
+
import { checkDeclaredContentLength, checkMaterializedSize, cleanFormDataContentType, encodeBodyIfNeeded, handleFetchCaughtError } from "./fetch-helpers.js";
|
|
22
22
|
import type { ComposedSignal, PendingBodyErrorRef } from "./fetch-helpers.js";
|
|
23
23
|
|
|
24
24
|
// btoa is a global in Node 16+ and browsers; accessed via globalThis for no-DOM lib compat
|
|
@@ -28,29 +28,29 @@ const _btoa: (data: string) => string = (
|
|
|
28
28
|
type AnyConstructor = new (...args: Array<unknown>) => unknown;
|
|
29
29
|
type FetchFn = (input: unknown, init?: unknown) => Promise<AnyResponse>;
|
|
30
30
|
type AnyResponse = {
|
|
31
|
+
[key: string]: unknown;
|
|
32
|
+
arrayBuffer: () => Promise<ArrayBuffer>;
|
|
31
33
|
body: unknown;
|
|
32
34
|
headers: {
|
|
33
|
-
has: (name: string) => boolean;
|
|
34
|
-
get: (name: string) => string | null;
|
|
35
35
|
[key: string]: unknown;
|
|
36
|
+
get: (name: string) => null | string;
|
|
37
|
+
has: (name: string) => boolean;
|
|
36
38
|
};
|
|
37
39
|
status: number;
|
|
38
40
|
statusText: string;
|
|
39
|
-
arrayBuffer: () => Promise<ArrayBuffer>;
|
|
40
|
-
[key: string]: unknown;
|
|
41
41
|
};
|
|
42
42
|
type AnyRequest = {
|
|
43
|
+
[key: string]: unknown;
|
|
44
|
+
arrayBuffer: () => Promise<ArrayBuffer>;
|
|
45
|
+
body: null | { cancel: () => Promise<void>; };
|
|
43
46
|
headers: {
|
|
47
|
+
get: (name: string) => null | string;
|
|
44
48
|
has: (name: string) => boolean;
|
|
45
|
-
get: (name: string) => string | null;
|
|
46
49
|
};
|
|
47
|
-
body: { cancel: () => Promise<void>; } | null;
|
|
48
|
-
arrayBuffer: () => Promise<ArrayBuffer>;
|
|
49
|
-
[key: string]: unknown;
|
|
50
50
|
};
|
|
51
51
|
type AnyReadableStream = {
|
|
52
|
-
cancel: () => Promise<void>;
|
|
53
52
|
[key: string]: unknown;
|
|
53
|
+
cancel: () => Promise<void>;
|
|
54
54
|
};
|
|
55
55
|
type AnyTextEncoder = { encode: (str: string) => Uint8Array; };
|
|
56
56
|
|
|
@@ -58,10 +58,35 @@ const DEFAULT_CHUNK_SIZE = 64 * 1024;
|
|
|
58
58
|
|
|
59
59
|
const { isFunction } = utils;
|
|
60
60
|
|
|
61
|
+
// Web stream bodies this adapter has sent. fetch can read a stream without leaving it locked,
|
|
62
|
+
// and there's no portable "disturbed" check, so a second send is caught here.
|
|
63
|
+
const sentStreamBodies = new WeakSet<object>();
|
|
64
|
+
|
|
65
|
+
// A stream body can be read once. Middleware that calls next() again after a try read it
|
|
66
|
+
// would send an empty body (Node stream) or get an uncoded TypeError from fetch (web stream).
|
|
67
|
+
const rejectUsedStreamBody = (data: unknown, config: InternalFaxiosRequestConfig): void => {
|
|
68
|
+
const body = data as { bodyUsed?: unknown; destroyed?: unknown; locked?: unknown; readableDidRead?: unknown; readableEnded?: unknown; };
|
|
69
|
+
const isWebStream = utils.isReadableStream!(data);
|
|
70
|
+
if (
|
|
71
|
+
(isWebStream && (body.locked === true || sentStreamBodies.has(body))) ||
|
|
72
|
+
((utils.isRequest!(data) || utils.isResponse!(data)) && body.bodyUsed === true) ||
|
|
73
|
+
(utils.isStream(data) && (body.readableDidRead === true || body.readableEnded === true || body.destroyed === true))
|
|
74
|
+
) {
|
|
75
|
+
throw new FaxiosError(
|
|
76
|
+
"Request body stream was already used and can't be sent again; pass a new body for each try",
|
|
77
|
+
FaxiosError.ERR_BAD_REQUEST,
|
|
78
|
+
config
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
if (isWebStream) sentStreamBodies.add(body);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const isBrowserRuntime = platform.hasStandardBrowserEnv || platform.hasStandardBrowserWebWorkerEnv;
|
|
85
|
+
|
|
61
86
|
// Server runtimes default User-Agent to their own (e.g. "node"), so name faxios there.
|
|
62
87
|
// Browsers send their own User-Agent; setting one makes every cross-origin request
|
|
63
88
|
// need a CORS preflight that most APIs don't allow.
|
|
64
|
-
const setsUserAgent = !
|
|
89
|
+
const setsUserAgent = !isBrowserRuntime;
|
|
65
90
|
const applyDefaultUserAgent = (headers: FaxiosRequestHeaders): void => {
|
|
66
91
|
if (setsUserAgent) headers.set("User-Agent", "faxios/" + VERSION, false);
|
|
67
92
|
};
|
|
@@ -116,8 +141,48 @@ const maybeWithAuthCredentials = (url: string): boolean => {
|
|
|
116
141
|
return urlToCheck.includes("@");
|
|
117
142
|
};
|
|
118
143
|
|
|
119
|
-
//
|
|
120
|
-
const
|
|
144
|
+
// RequestInit members faxios sets itself; the same keys in `fetchOptions` are ignored.
|
|
145
|
+
const FAXIOS_INIT_KEYS: Record<string, true> = {
|
|
146
|
+
body: true,
|
|
147
|
+
credentials: true,
|
|
148
|
+
duplex: true,
|
|
149
|
+
headers: true,
|
|
150
|
+
method: true,
|
|
151
|
+
signal: true,
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
// Spec defaults written as own init members when the caller sets none. undici converts the init
|
|
155
|
+
// into a plain dictionary and reads absent members from it, so an absent member still picks up a
|
|
156
|
+
// polluted Object.prototype value; an own default leaves nothing to inherit.
|
|
157
|
+
const REQUEST_INIT_DEFAULTS: Record<string, unknown> = {
|
|
158
|
+
cache: "default",
|
|
159
|
+
integrity: "",
|
|
160
|
+
keepalive: false,
|
|
161
|
+
mode: "cors",
|
|
162
|
+
redirect: "follow",
|
|
163
|
+
referrer: "about:client",
|
|
164
|
+
referrerPolicy: "",
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
// Where undici (Node's fetch) keeps its global dispatcher.
|
|
168
|
+
const UNDICI_GLOBAL_DISPATCHER = Symbol.for("undici.globalDispatcher.1");
|
|
169
|
+
|
|
170
|
+
type FetchEnv = {
|
|
171
|
+
fetch?: unknown;
|
|
172
|
+
Request?: unknown;
|
|
173
|
+
Response?: unknown;
|
|
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 => {
|
|
121
186
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
122
187
|
const globalObject: Record<string, unknown> = utils.global ?? globalThis;
|
|
123
188
|
const ReadableStream = globalObject["ReadableStream"] as
|
|
@@ -127,38 +192,36 @@ const factory = (env: Record<string, unknown>) => {
|
|
|
127
192
|
| (new () => AnyTextEncoder)
|
|
128
193
|
| undefined;
|
|
129
194
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
},
|
|
134
|
-
{
|
|
135
|
-
Request: globalObject["Request"],
|
|
136
|
-
Response: globalObject["Response"],
|
|
137
|
-
},
|
|
138
|
-
env
|
|
139
|
-
);
|
|
140
|
-
|
|
141
|
-
const {
|
|
142
|
-
fetch: envFetch,
|
|
143
|
-
Request,
|
|
144
|
-
Response,
|
|
145
|
-
} = env as {
|
|
146
|
-
fetch?: FetchFn;
|
|
147
|
-
Request?: AnyConstructor;
|
|
148
|
-
Response?: AnyConstructor;
|
|
149
|
-
};
|
|
150
|
-
const _globalFetch = (globalThis as unknown as Record<string, unknown>)[
|
|
151
|
-
"fetch"
|
|
152
|
-
];
|
|
153
|
-
const isFetchSupported = envFetch
|
|
154
|
-
? isFunction(envFetch)
|
|
155
|
-
: isFunction(_globalFetch);
|
|
195
|
+
// `undefined` falls back to the global constructor; `null` disables it.
|
|
196
|
+
const Request = (env.Request === undefined ? globalObject["Request"] : env.Request) as AnyConstructor | undefined;
|
|
197
|
+
const Response = (env.Response === undefined ? globalObject["Response"] : env.Response) as AnyConstructor | undefined;
|
|
156
198
|
const isRequestSupported = isFunction(Request);
|
|
157
199
|
const isResponseSupported = isFunction(Response);
|
|
158
200
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
201
|
+
// Only members this runtime's Request exposes, so an engine that rejects members it doesn't
|
|
202
|
+
// implement (Cloudflare Workers) is never sent one.
|
|
203
|
+
const requestInitDefaults = Object.entries(REQUEST_INIT_DEFAULTS).filter(([ key ]) =>
|
|
204
|
+
isRequestSupported && utils.hasOwnInPrototypeChain((Request as AnyConstructor).prototype, key));
|
|
205
|
+
|
|
206
|
+
// Null prototype: WebIDL reads every RequestInit member with Get, which walks the prototype
|
|
207
|
+
// chain, so a plain object literal would let a polluted Object.prototype.redirect, .integrity,
|
|
208
|
+
// .mode or undici .dispatcher apply to every request. Every own enumerable key of fetchOptions
|
|
209
|
+
// is copied, so runtime extensions (Next.js `next`, Workers `cf`, Bun `tls`) still pass through;
|
|
210
|
+
// inherited keys never do. The caller sets signal, method, headers, body, duplex, credentials.
|
|
211
|
+
const initFromFetchOptions = (fetchOptions: unknown, envFetch: FetchFn | undefined): Record<string, unknown> => {
|
|
212
|
+
const init: Record<string, unknown> = Object.create(null);
|
|
213
|
+
for (const [ key, value ] of requestInitDefaults) init[key] = value;
|
|
214
|
+
// Node's global fetch reads `dispatcher` the same way; pin it to undici's own global one.
|
|
215
|
+
if (!envFetch && utils.hasOwnProp(globalThis, UNDICI_GLOBAL_DISPATCHER)) {
|
|
216
|
+
init["dispatcher"] = (globalThis as Record<PropertyKey, unknown>)[UNDICI_GLOBAL_DISPATCHER];
|
|
217
|
+
}
|
|
218
|
+
if (utils.isObject(fetchOptions)) {
|
|
219
|
+
for (const key of Object.keys(fetchOptions)) {
|
|
220
|
+
if (!utils.hasOwnProp(FAXIOS_INIT_KEYS, key)) init[key] = (fetchOptions as Record<string, unknown>)[key];
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return init;
|
|
224
|
+
};
|
|
162
225
|
|
|
163
226
|
const isReadableStreamSupported = isFunction(ReadableStream);
|
|
164
227
|
|
|
@@ -179,7 +242,7 @@ const factory = (env: Record<string, unknown>) => {
|
|
|
179
242
|
// `fetch(request, init)`. undici (Node 26.11+) starts reading an async iterable in both,
|
|
180
243
|
// so the first chunk goes to the discarded copy. One web stream is read only once.
|
|
181
244
|
// Web stream bodies must yield bytes, so string chunks (e.g. `Readable.from(["a"])`) are encoded.
|
|
182
|
-
const from = (ReadableStream as { from?: (source: unknown) => unknown; }
|
|
245
|
+
const from = (ReadableStream as undefined | { from?: (source: unknown) => unknown; })?.from;
|
|
183
246
|
const streamFrom = typeof from === "function" ? from.bind(ReadableStream) : undefined;
|
|
184
247
|
async function* toByteChunks(source: AsyncIterable<unknown>) {
|
|
185
248
|
for await (const chunk of source) yield typeof chunk === "string" ? await encodeText(chunk) : chunk;
|
|
@@ -214,6 +277,13 @@ const factory = (env: Record<string, unknown>) => {
|
|
|
214
277
|
return duplexAccessed && !hasContentType;
|
|
215
278
|
});
|
|
216
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
|
+
|
|
217
287
|
const supportsResponseStream =
|
|
218
288
|
isResponseSupported &&
|
|
219
289
|
isReadableStreamSupported &&
|
|
@@ -230,26 +300,22 @@ const factory = (env: Record<string, unknown>) => {
|
|
|
230
300
|
stream: supportsResponseStream && ((res: AnyResponse) => res.body),
|
|
231
301
|
};
|
|
232
302
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
FaxiosError.ERR_NOT_SUPPORT,
|
|
248
|
-
config as InternalFaxiosRequestConfig
|
|
249
|
-
);
|
|
250
|
-
});
|
|
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
|
+
);
|
|
251
317
|
});
|
|
252
|
-
|
|
318
|
+
});
|
|
253
319
|
|
|
254
320
|
const getBodyLength = async (body: unknown): Promise<number | undefined> => {
|
|
255
321
|
if (body == null) {
|
|
@@ -260,16 +326,8 @@ const factory = (env: Record<string, unknown>) => {
|
|
|
260
326
|
return (body as { size: number; }).size;
|
|
261
327
|
}
|
|
262
328
|
|
|
263
|
-
if (utils.isSpecCompliantForm(body)) {
|
|
264
|
-
const _request = new (Request as AnyConstructor)(platform.origin, {
|
|
265
|
-
method: "POST",
|
|
266
|
-
body,
|
|
267
|
-
}) as AnyRequest;
|
|
268
|
-
return (await _request.arrayBuffer()).byteLength;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
329
|
if (utils.isArrayBufferView(body) || utils.isArrayBuffer(body)) {
|
|
272
|
-
return (body as
|
|
330
|
+
return (body as ArrayBuffer | ArrayBufferView).byteLength;
|
|
273
331
|
}
|
|
274
332
|
|
|
275
333
|
if (utils.isURLSearchParams(body)) {
|
|
@@ -282,6 +340,23 @@ const factory = (env: Record<string, unknown>) => {
|
|
|
282
340
|
return undefined;
|
|
283
341
|
};
|
|
284
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
|
+
|
|
285
360
|
const resolveBodyLength = async (
|
|
286
361
|
headers: FaxiosRequestHeaders,
|
|
287
362
|
body: unknown
|
|
@@ -291,7 +366,7 @@ const factory = (env: Record<string, unknown>) => {
|
|
|
291
366
|
return length == null ? getBodyLength(body) : length;
|
|
292
367
|
};
|
|
293
368
|
|
|
294
|
-
const normalizeCredentials = (wc:
|
|
369
|
+
const normalizeCredentials = (wc: boolean | string): string => {
|
|
295
370
|
if (utils.isString(wc)) return wc as string;
|
|
296
371
|
return wc ? "include" : "omit";
|
|
297
372
|
};
|
|
@@ -303,7 +378,7 @@ const factory = (env: Record<string, unknown>) => {
|
|
|
303
378
|
headers: FaxiosRequestHeaders,
|
|
304
379
|
own: (key: string) => unknown
|
|
305
380
|
): string => {
|
|
306
|
-
let auth: {
|
|
381
|
+
let auth: undefined | { password: unknown; username: unknown; };
|
|
307
382
|
const configAuth = own("auth");
|
|
308
383
|
if (configAuth) {
|
|
309
384
|
auth = {
|
|
@@ -372,10 +447,13 @@ const factory = (env: Record<string, unknown>) => {
|
|
|
372
447
|
}
|
|
373
448
|
// Enforce maxBodyLength against known-size bodies before dispatch using
|
|
374
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.
|
|
375
452
|
if (hasMaxBodyLength && method !== "get" && method !== "head") {
|
|
376
453
|
const outboundLength = await getBodyLength(data);
|
|
377
|
-
|
|
378
|
-
|
|
454
|
+
const checkedLength = outboundLength ?? await getFormDataFieldsLength(data);
|
|
455
|
+
if (typeof checkedLength === "number" && isFinite(checkedLength)) {
|
|
456
|
+
if (checkedLength > maxBodyLength) {
|
|
379
457
|
throw new FaxiosError(
|
|
380
458
|
"Request body larger than maxBodyLength limit",
|
|
381
459
|
FaxiosError.ERR_BAD_REQUEST,
|
|
@@ -398,7 +476,7 @@ const factory = (env: Record<string, unknown>) => {
|
|
|
398
476
|
onUploadProgress: unknown,
|
|
399
477
|
_trackStream: (stream: unknown, onProgress?: (bytes: number) => void, flush?: () => void) => unknown
|
|
400
478
|
): unknown => {
|
|
401
|
-
let contentTypeHeader:
|
|
479
|
+
let contentTypeHeader: null | string;
|
|
402
480
|
if (
|
|
403
481
|
utils.isFormData(data) &&
|
|
404
482
|
(contentTypeHeader = _request.headers.get("content-type"))
|
|
@@ -437,6 +515,10 @@ const factory = (env: Record<string, unknown>) => {
|
|
|
437
515
|
request: unknown,
|
|
438
516
|
pendingBodyErrorRef: PendingBodyErrorRef
|
|
439
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;
|
|
440
522
|
const _makeBodyLengthError = () =>
|
|
441
523
|
new FaxiosError(
|
|
442
524
|
"Request body larger than maxBodyLength limit",
|
|
@@ -460,14 +542,21 @@ const factory = (env: Record<string, unknown>) => {
|
|
|
460
542
|
}
|
|
461
543
|
onProgress && onProgress(loadedBytes);
|
|
462
544
|
},
|
|
463
|
-
|
|
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
|
+
}
|
|
464
553
|
);
|
|
465
554
|
|
|
466
555
|
if (
|
|
467
556
|
supportsRequestStream &&
|
|
468
557
|
method !== "get" &&
|
|
469
558
|
method !== "head" &&
|
|
470
|
-
(
|
|
559
|
+
(uploadProgress || mustEnforceStreamBody)
|
|
471
560
|
) {
|
|
472
561
|
requestContentLength =
|
|
473
562
|
requestContentLength == null
|
|
@@ -482,7 +571,7 @@ const factory = (env: Record<string, unknown>) => {
|
|
|
482
571
|
body: data,
|
|
483
572
|
duplex: "half",
|
|
484
573
|
}) as AnyRequest;
|
|
485
|
-
data = applyUploadProgress(_request, data, headers, requestContentLength,
|
|
574
|
+
data = applyUploadProgress(_request, data, headers, requestContentLength, uploadProgress, _trackStream);
|
|
486
575
|
}
|
|
487
576
|
}
|
|
488
577
|
else if (
|
|
@@ -520,11 +609,11 @@ const factory = (env: Record<string, unknown>) => {
|
|
|
520
609
|
hasMaxContentLength: boolean,
|
|
521
610
|
maxContentLength: number,
|
|
522
611
|
isStreamResponse: boolean,
|
|
523
|
-
|
|
612
|
+
release: (err?: unknown) => void,
|
|
524
613
|
config: InternalFaxiosRequestConfig,
|
|
525
614
|
request: unknown
|
|
526
615
|
): AnyResponse => {
|
|
527
|
-
const needsWrapping = onDownloadProgress || hasMaxContentLength ||
|
|
616
|
+
const needsWrapping = !!onDownloadProgress || hasMaxContentLength || isStreamResponse;
|
|
528
617
|
if (!supportsResponseStream || !response.body || !needsWrapping) {
|
|
529
618
|
return response;
|
|
530
619
|
}
|
|
@@ -534,9 +623,15 @@ const factory = (env: Record<string, unknown>) => {
|
|
|
534
623
|
options[prop] = response[prop];
|
|
535
624
|
});
|
|
536
625
|
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
)
|
|
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
|
+
);
|
|
540
635
|
|
|
541
636
|
const [ onProgress, flush ] = onDownloadProgress
|
|
542
637
|
? progressEventDecorator(
|
|
@@ -571,23 +666,24 @@ const factory = (env: Record<string, unknown>) => {
|
|
|
571
666
|
response.body,
|
|
572
667
|
DEFAULT_CHUNK_SIZE,
|
|
573
668
|
onChunkProgress,
|
|
574
|
-
() => {
|
|
669
|
+
(err?: unknown) => {
|
|
575
670
|
flush && flush();
|
|
576
|
-
|
|
671
|
+
release(err);
|
|
577
672
|
}
|
|
578
673
|
),
|
|
579
674
|
options
|
|
580
675
|
) as AnyResponse;
|
|
581
676
|
};
|
|
582
677
|
|
|
583
|
-
|
|
584
|
-
|
|
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();
|
|
585
681
|
};
|
|
586
682
|
|
|
587
683
|
return async (config: InternalFaxiosRequestConfig) => {
|
|
588
684
|
const _resolved = prepareRequest(config) as InternalFaxiosRequestConfig & {
|
|
589
685
|
fetchOptions?: Record<string, unknown>;
|
|
590
|
-
withCredentials?:
|
|
686
|
+
withCredentials?: boolean | string;
|
|
591
687
|
};
|
|
592
688
|
let {
|
|
593
689
|
url,
|
|
@@ -604,7 +700,7 @@ const factory = (env: Record<string, unknown>) => {
|
|
|
604
700
|
maxBodyLength,
|
|
605
701
|
} = _resolved;
|
|
606
702
|
let headers = _resolved.headers;
|
|
607
|
-
let withCredentials:
|
|
703
|
+
let withCredentials: boolean | string =
|
|
608
704
|
_resolved.withCredentials ?? "same-origin";
|
|
609
705
|
|
|
610
706
|
const hasMaxContentLength =
|
|
@@ -616,6 +712,7 @@ const factory = (env: Record<string, unknown>) => {
|
|
|
616
712
|
? (config as unknown as Record<string, unknown>)[key]
|
|
617
713
|
: undefined;
|
|
618
714
|
|
|
715
|
+
const envFetch = ownEnv(config, "fetch") as FetchFn | undefined;
|
|
619
716
|
const _fetch: FetchFn = (envFetch || (globalThis as unknown as Record<string, unknown>)["fetch"]) as FetchFn;
|
|
620
717
|
|
|
621
718
|
if (timeout != null && !Number.isFinite(Number(timeout))) {
|
|
@@ -630,18 +727,23 @@ const factory = (env: Record<string, unknown>) => {
|
|
|
630
727
|
responseType ? (responseType + "").toLowerCase() : "text"
|
|
631
728
|
) as typeof responseType;
|
|
632
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();
|
|
633
733
|
const composedSignal = composeSignals(
|
|
634
|
-
[ signal ],
|
|
734
|
+
[ signal, requestController.signal ],
|
|
635
735
|
timeout,
|
|
636
736
|
timeoutErrorMessage
|
|
637
|
-
) as ComposedSignal
|
|
737
|
+
) as ComposedSignal;
|
|
638
738
|
|
|
639
|
-
const unsubscribe = composedSignal
|
|
739
|
+
const unsubscribe = composedSignal.unsubscribe!.bind(composedSignal);
|
|
640
740
|
|
|
641
|
-
let request:
|
|
741
|
+
let request: null | Request = null;
|
|
642
742
|
const pendingBodyErrorRef: PendingBodyErrorRef = { value: null };
|
|
643
743
|
|
|
644
744
|
try {
|
|
745
|
+
rejectUsedStreamBody(data, config);
|
|
746
|
+
|
|
645
747
|
url = applyAuthToRequest(url!, headers, own);
|
|
646
748
|
|
|
647
749
|
const preflightContentLength = await checkPreflightLimits(
|
|
@@ -658,8 +760,7 @@ const factory = (env: Record<string, unknown>) => {
|
|
|
658
760
|
|
|
659
761
|
// A streamed body under maxBodyLength must be counted as fetch consumes
|
|
660
762
|
// it; its size is never trusted from a caller-declared Content-Length.
|
|
661
|
-
const
|
|
662
|
-
const mustEnforceStreamBody = hasMaxBodyLength && isStreamLike;
|
|
763
|
+
const mustEnforceStreamBody = hasMaxBodyLength && isCountedWhileSent(data);
|
|
663
764
|
|
|
664
765
|
({ data } = await buildUploadStream(
|
|
665
766
|
data,
|
|
@@ -696,25 +797,20 @@ const factory = (env: Record<string, unknown>) => {
|
|
|
696
797
|
// Case-insensitive check: toByteStringHeaderObject may normalize to lowercase.
|
|
697
798
|
data = toWebStream(await encodeBodyIfNeeded(data, serializedHeaders, encodeText));
|
|
698
799
|
|
|
699
|
-
const resolvedOptions =
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
credentials: isCredentialsSupported ? withCredentials : undefined,
|
|
707
|
-
};
|
|
800
|
+
const resolvedOptions = initFromFetchOptions(fetchOptions, envFetch);
|
|
801
|
+
resolvedOptions["signal"] = composedSignal;
|
|
802
|
+
resolvedOptions["method"] = (method as string).toUpperCase();
|
|
803
|
+
resolvedOptions["headers"] = serializedHeaders;
|
|
804
|
+
resolvedOptions["body"] = data;
|
|
805
|
+
resolvedOptions["duplex"] = "half";
|
|
806
|
+
resolvedOptions["credentials"] = isCredentialsSupported ? withCredentials : undefined;
|
|
708
807
|
|
|
709
808
|
request = isRequestSupported
|
|
710
809
|
? new (Request as AnyConstructor)(url, resolvedOptions) as Request
|
|
711
810
|
: null;
|
|
712
811
|
|
|
713
|
-
// Pass resolvedOptions
|
|
714
|
-
//
|
|
715
|
-
// headers, body, credentials) off Object.prototype when init is absent,
|
|
716
|
-
// so a polluted Object.prototype.method would override the Request's verb.
|
|
717
|
-
// An explicit own-property init neutralizes every such gadget.
|
|
812
|
+
// Pass resolvedOptions as the fetch init even when a Request object is supplied, so the
|
|
813
|
+
// same null-prototype members apply to both calls.
|
|
718
814
|
let response = await (isRequestSupported
|
|
719
815
|
? _fetch(request, resolvedOptions)
|
|
720
816
|
: _fetch(url, resolvedOptions));
|
|
@@ -727,6 +823,14 @@ const factory = (env: Record<string, unknown>) => {
|
|
|
727
823
|
supportsResponseStream &&
|
|
728
824
|
(responseType === "stream" || responseType === "response");
|
|
729
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
|
+
|
|
730
834
|
response = buildDownloadStream(
|
|
731
835
|
response,
|
|
732
836
|
responseHeaders,
|
|
@@ -734,7 +838,7 @@ const factory = (env: Record<string, unknown>) => {
|
|
|
734
838
|
hasMaxContentLength,
|
|
735
839
|
maxContentLength!,
|
|
736
840
|
isStreamResponse,
|
|
737
|
-
|
|
841
|
+
releaseStreamBody,
|
|
738
842
|
config,
|
|
739
843
|
request
|
|
740
844
|
);
|
|
@@ -755,12 +859,12 @@ const factory = (env: Record<string, unknown>) => {
|
|
|
755
859
|
request
|
|
756
860
|
);
|
|
757
861
|
|
|
758
|
-
unsubscribeOnComplete(isStreamResponse, unsubscribe);
|
|
862
|
+
unsubscribeOnComplete(isStreamResponse, response, unsubscribe);
|
|
759
863
|
|
|
760
864
|
return await new Promise((resolve, reject) => {
|
|
761
865
|
settle(resolve, reject, {
|
|
762
866
|
data: responseData,
|
|
763
|
-
headers:
|
|
867
|
+
headers: responseHeaders,
|
|
764
868
|
status: response.status,
|
|
765
869
|
statusText: response.statusText,
|
|
766
870
|
config,
|
|
@@ -769,42 +873,57 @@ const factory = (env: Record<string, unknown>) => {
|
|
|
769
873
|
});
|
|
770
874
|
}
|
|
771
875
|
catch (err) {
|
|
772
|
-
|
|
773
|
-
|
|
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;
|
|
774
887
|
}
|
|
775
888
|
};
|
|
776
889
|
};
|
|
777
890
|
|
|
778
|
-
|
|
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>>();
|
|
779
897
|
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
Request?: unknown;
|
|
785
|
-
Response?: unknown;
|
|
786
|
-
};
|
|
787
|
-
const seeds: Array<unknown> = [ Request, Response, fetch ];
|
|
788
|
-
|
|
789
|
-
const len = seeds.length;
|
|
790
|
-
let i = len;
|
|
791
|
-
let seed: unknown;
|
|
792
|
-
let target: unknown;
|
|
793
|
-
let map: Map<unknown, unknown> = seedCache;
|
|
898
|
+
const ctorKey = (ctor: unknown): object => {
|
|
899
|
+
if (ctor === undefined) return GLOBAL_CTOR;
|
|
900
|
+
return isFunction(ctor) ? (ctor as object) : NO_CTOR;
|
|
901
|
+
};
|
|
794
902
|
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
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;
|
|
909
|
+
}
|
|
798
910
|
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
911
|
+
const Request = ownEnv(config, "Request");
|
|
912
|
+
const Response = ownEnv(config, "Response");
|
|
913
|
+
const requestKey = ctorKey(Request);
|
|
914
|
+
const responseKey = ctorKey(Response);
|
|
803
915
|
|
|
804
|
-
|
|
916
|
+
let byResponse = adapters.get(requestKey);
|
|
917
|
+
if (!byResponse) {
|
|
918
|
+
byResponse = new WeakMap();
|
|
919
|
+
adapters.set(requestKey, byResponse);
|
|
805
920
|
}
|
|
806
|
-
|
|
807
|
-
|
|
921
|
+
let adapter = byResponse.get(responseKey);
|
|
922
|
+
if (!adapter) {
|
|
923
|
+
adapter = factory({ Request, Response });
|
|
924
|
+
byResponse.set(responseKey, adapter);
|
|
925
|
+
}
|
|
926
|
+
return adapter;
|
|
808
927
|
};
|
|
809
928
|
|
|
810
929
|
getFetch();
|