@gcmdev/faxios 0.2.1 → 0.2.2

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.
Files changed (119) hide show
  1. package/dist/index.d.ts +3 -3
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +1 -1
  4. package/dist/index.js.map +1 -1
  5. package/dist/lib/adapters/fetch-helpers.d.ts +1 -1
  6. package/dist/lib/adapters/fetch-helpers.d.ts.map +1 -1
  7. package/dist/lib/adapters/fetch.d.ts.map +1 -1
  8. package/dist/lib/adapters/fetch.js +86 -26
  9. package/dist/lib/adapters/fetch.js.map +1 -1
  10. package/dist/lib/cancel/CanceledError.d.ts +1 -1
  11. package/dist/lib/cancel/CanceledError.d.ts.map +1 -1
  12. package/dist/lib/core/Faxios.d.ts +3 -3
  13. package/dist/lib/core/Faxios.d.ts.map +1 -1
  14. package/dist/lib/core/Faxios.js +8 -4
  15. package/dist/lib/core/Faxios.js.map +1 -1
  16. package/dist/lib/core/FaxiosError.d.ts +2 -2
  17. package/dist/lib/core/FaxiosError.d.ts.map +1 -1
  18. package/dist/lib/core/FaxiosHeaders.d.ts +8 -8
  19. package/dist/lib/core/FaxiosHeaders.d.ts.map +1 -1
  20. package/dist/lib/core/define.d.ts +3 -3
  21. package/dist/lib/core/define.d.ts.map +1 -1
  22. package/dist/lib/core/dispatchRequest.d.ts +1 -1
  23. package/dist/lib/core/dispatchRequest.d.ts.map +1 -1
  24. package/dist/lib/core/mergeConfig.d.ts +2 -2
  25. package/dist/lib/core/mergeConfig.d.ts.map +1 -1
  26. package/dist/lib/core/mergeConfig.js +40 -2
  27. package/dist/lib/core/mergeConfig.js.map +1 -1
  28. package/dist/lib/core/route.d.ts +1 -1
  29. package/dist/lib/core/route.d.ts.map +1 -1
  30. package/dist/lib/core/route.js +2 -5
  31. package/dist/lib/core/route.js.map +1 -1
  32. package/dist/lib/core/transformData.d.ts +2 -2
  33. package/dist/lib/core/transformData.d.ts.map +1 -1
  34. package/dist/lib/core/validateSchema.d.ts +2 -2
  35. package/dist/lib/core/validateSchema.d.ts.map +1 -1
  36. package/dist/lib/env/data.d.ts +1 -1
  37. package/dist/lib/env/data.js +1 -1
  38. package/dist/lib/faxios.d.ts +50 -50
  39. package/dist/lib/faxios.d.ts.map +1 -1
  40. package/dist/lib/helpers/cloneConfig.d.ts +0 -1
  41. package/dist/lib/helpers/cloneConfig.d.ts.map +1 -1
  42. package/dist/lib/helpers/cloneConfig.js +2 -2
  43. package/dist/lib/helpers/cloneConfig.js.map +1 -1
  44. package/dist/lib/helpers/cookies.d.ts +1 -1
  45. package/dist/lib/helpers/cookies.d.ts.map +1 -1
  46. package/dist/lib/helpers/formDataToJSON.d.ts +1 -1
  47. package/dist/lib/helpers/formDataToJSON.d.ts.map +1 -1
  48. package/dist/lib/helpers/formDataToJSON.js +1 -1
  49. package/dist/lib/helpers/formDataToJSON.js.map +1 -1
  50. package/dist/lib/helpers/parseHeaders.d.ts +1 -1
  51. package/dist/lib/helpers/parseHeaders.d.ts.map +1 -1
  52. package/dist/lib/helpers/substitutePathParams.d.ts.map +1 -1
  53. package/dist/lib/helpers/substitutePathParams.js +11 -3
  54. package/dist/lib/helpers/substitutePathParams.js.map +1 -1
  55. package/dist/lib/helpers/toFormData.d.ts.map +1 -1
  56. package/dist/lib/helpers/toFormData.js +4 -2
  57. package/dist/lib/helpers/toFormData.js.map +1 -1
  58. package/dist/lib/helpers/validator.d.ts +1 -1
  59. package/dist/lib/helpers/validator.d.ts.map +1 -1
  60. package/dist/lib/platform.d.ts.map +1 -1
  61. package/dist/lib/plugins/authBearer.d.ts +9 -5
  62. package/dist/lib/plugins/authBearer.d.ts.map +1 -1
  63. package/dist/lib/plugins/authBearer.js +6 -0
  64. package/dist/lib/plugins/authBearer.js.map +1 -1
  65. package/dist/lib/plugins/definePlugin.d.ts +3 -3
  66. package/dist/lib/plugins/definePlugin.d.ts.map +1 -1
  67. package/dist/lib/plugins/retry.d.ts +23 -23
  68. package/dist/lib/plugins/retry.d.ts.map +1 -1
  69. package/dist/lib/plugins/timing.d.ts +8 -8
  70. package/dist/lib/plugins/timing.d.ts.map +1 -1
  71. package/dist/lib/types/standard-schema.d.ts +6 -6
  72. package/dist/lib/types/standard-schema.d.ts.map +1 -1
  73. package/dist/lib/types.d.ts +92 -92
  74. package/dist/lib/types.d.ts.map +1 -1
  75. package/dist/lib/utils.d.ts +4 -3
  76. package/dist/lib/utils.d.ts.map +1 -1
  77. package/dist/lib/utils.js +9 -2
  78. package/dist/lib/utils.js.map +1 -1
  79. package/package.json +7 -4
  80. package/skills/configure-requests/SKILL.md +8 -5
  81. package/skills/configure-requests/references/plugins.md +37 -6
  82. package/src/index.ts +15 -15
  83. package/src/lib/adapters/fetch-helpers.ts +3 -3
  84. package/src/lib/adapters/fetch.ts +125 -64
  85. package/src/lib/cancel/CanceledError.ts +1 -1
  86. package/src/lib/core/Faxios.ts +17 -13
  87. package/src/lib/core/FaxiosError.ts +3 -3
  88. package/src/lib/core/FaxiosHeaders.ts +16 -16
  89. package/src/lib/core/define.ts +7 -7
  90. package/src/lib/core/dispatchRequest.ts +2 -2
  91. package/src/lib/core/mergeConfig.ts +42 -4
  92. package/src/lib/core/prepareRequest.ts +1 -1
  93. package/src/lib/core/route.ts +3 -6
  94. package/src/lib/core/transformData.ts +2 -2
  95. package/src/lib/core/validateSchema.ts +4 -4
  96. package/src/lib/defaults/index.ts +1 -1
  97. package/src/lib/env/data.ts +1 -1
  98. package/src/lib/faxios.ts +55 -55
  99. package/src/lib/helpers/HttpStatusCode.ts +1 -1
  100. package/src/lib/helpers/cloneConfig.ts +2 -3
  101. package/src/lib/helpers/composeSignals.ts +4 -4
  102. package/src/lib/helpers/cookies.ts +3 -3
  103. package/src/lib/helpers/formDataToJSON.ts +4 -4
  104. package/src/lib/helpers/parseHeaders.ts +2 -2
  105. package/src/lib/helpers/progressEventReducer.ts +1 -1
  106. package/src/lib/helpers/sanitizeHeaderValue.ts +1 -1
  107. package/src/lib/helpers/substitutePathParams.ts +13 -3
  108. package/src/lib/helpers/throttle.ts +1 -1
  109. package/src/lib/helpers/toFormData.ts +11 -9
  110. package/src/lib/helpers/trackStream.ts +3 -3
  111. package/src/lib/helpers/validator.ts +3 -3
  112. package/src/lib/platform.ts +4 -4
  113. package/src/lib/plugins/authBearer.ts +14 -9
  114. package/src/lib/plugins/definePlugin.ts +3 -3
  115. package/src/lib/plugins/retry.ts +27 -27
  116. package/src/lib/plugins/timing.ts +5 -5
  117. package/src/lib/types/standard-schema.ts +6 -6
  118. package/src/lib/types.ts +125 -125
  119. package/src/lib/utils.ts +21 -12
@@ -86,8 +86,8 @@ const res = await faxios.get("https://api.example.com/users/{id}/posts", {
86
86
  // GET https://api.example.com/users/42/posts?page=2&tag=news
87
87
  ```
88
88
 
89
- `pathParams` values are `encodeURIComponent`-encoded. A missing or `null` value rejects with
90
- `ERR_BAD_OPTION_VALUE`.
89
+ `pathParams` values are `encodeURIComponent`-encoded. A missing or `null` value, or `.`/`..`
90
+ (also percent-encoded, such as `%2e%2e`), rejects with `ERR_BAD_OPTION_VALUE`.
91
91
 
92
92
  Binary downloads need `responseType`, and a type argument because `data` is `unknown`:
93
93
 
@@ -273,6 +273,7 @@ plugin's request options (such as `retry: false`). Before writing plugin code, r
273
273
  `baseURL` origin when `baseURL` is absolute; otherwise only to relative request URLs. Use
274
274
  `origins` to allow more. It leaves an explicit `Authorization` header alone unless
275
275
  `overwrite: true`. Install `retry` before it if each attempt should call `getToken` again.
276
+ A `getToken` result that isn't a non-empty string sends the request without the header.
276
277
  - `retry` defaults: 3 attempts, statuses 408/429/500/502/503/504 plus `ERR_NETWORK` and
277
278
  `ETIMEDOUT`, exponential backoff from 100 ms with full jitter. A 429/503 `Retry-After` sets the
278
279
  wait unless `respectRetryAfter: false`. Turn that off when something else, such as a queue
@@ -304,9 +305,11 @@ const { data } = await testApi.get("https://api.example.com/feed"); // { ok: tru
304
305
  await faxios.get("https://api.example.com/feed", { fetchOptions: { cache: "no-store" } });
305
306
  ```
306
307
 
307
- - `fetchOptions` is spread into the init object passed to `fetch`. faxios sets `signal`, `method`,
308
- `headers`, `body`, `duplex` and `credentials` itself, and those override the same keys in
309
- `fetchOptions`.
308
+ - Every own enumerable key of `fetchOptions` is copied into a null-prototype init passed to
309
+ `fetch`, including runtime extensions (Next.js `next`, Workers `cf`, undici `dispatcher`).
310
+ Inherited keys are ignored. faxios sets `signal`, `method`, `headers`, `body`, `duplex` and
311
+ `credentials` itself and ignores those keys in `fetchOptions`.
312
+ - `env.fetch`, `env.Request` and `env.Response` count only as own properties of `env`.
310
313
  - Plain-object values inside `fetchOptions` are copied when configs merge; class instances are
311
314
  passed by reference.
312
315
  - Proxies are runtime configuration, not a faxios option. In Node, the project docs route a
@@ -85,21 +85,49 @@ Check: `faxios.get("/orders", { trace: "x" })` on an instance without the plugin
85
85
  installing it before `authBearer`:
86
86
 
87
87
  ```ts
88
- import faxios, { FaxiosError, type FaxiosContext } from "@gcmdev/faxios";
88
+ import faxios, { type FaxiosContext } from "@gcmdev/faxios";
89
89
  import { definePlugin } from "@gcmdev/faxios/plugins";
90
90
  import { authBearer, type AuthBearerCapability } from "@gcmdev/faxios/plugins/auth-bearer";
91
91
 
92
+ function requireSignIn() {
93
+ return definePlugin({
94
+ name: "requireSignIn",
95
+ middleware: async (ctx: FaxiosContext<unknown, AuthBearerCapability>, next) => {
96
+ if (!(await ctx.capabilities.auth.getToken())) throw new Error("Sign in first");
97
+ return next(ctx);
98
+ },
99
+ });
100
+ }
101
+
102
+ const api = faxios
103
+ .create({ baseURL: "https://api.example.com" })
104
+ .use(authBearer(() => "token"))
105
+ .use(requireSignIn());
106
+ ```
107
+
108
+ ## Refresh a token after a 401
109
+
110
+ Install the refresh plugin before `authBearer` and never set `Authorization` in it. The retry
111
+ runs `authBearer` again, which calls `getToken` and applies its origin rule, so a third-party 401
112
+ is retried without the token. Share one in-flight refresh so rotating refresh tokens aren't spent twice:
113
+
114
+ ```ts
115
+ import faxios, { FaxiosError } from "@gcmdev/faxios";
116
+ import { definePlugin } from "@gcmdev/faxios/plugins";
117
+ import { authBearer } from "@gcmdev/faxios/plugins/auth-bearer";
118
+
92
119
  function refreshOn401(refresh: () => Promise<void>) {
120
+ let refreshing: Promise<void> | undefined;
93
121
  return definePlugin({
94
122
  name: "refreshOn401",
95
- middleware: async (ctx: FaxiosContext<unknown, AuthBearerCapability>, next) => {
123
+ middleware: async (ctx, next) => {
96
124
  try {
97
125
  return await next(ctx);
98
126
  }
99
127
  catch (error) {
100
128
  if (!(error instanceof FaxiosError) || error.response?.status !== 401) throw error;
101
- await refresh();
102
- ctx.config.headers.set("Authorization", `Bearer ${await ctx.capabilities.auth.getToken()}`);
129
+ refreshing ??= refresh().finally(() => { refreshing = undefined; });
130
+ await refreshing;
103
131
  return next(ctx);
104
132
  }
105
133
  },
@@ -109,10 +137,13 @@ function refreshOn401(refresh: () => Promise<void>) {
109
137
  let token = "old";
110
138
  const api = faxios
111
139
  .create({ baseURL: "https://api.example.com" })
112
- .use(authBearer(() => token))
113
- .use(refreshOn401(async () => { token = "new"; }));
140
+ .use(refreshOn401(async () => { token = "new"; }))
141
+ .use(authBearer(() => token));
114
142
  ```
115
143
 
144
+ Calling `next` again re-sends the body. A stream body is used up by the first try; sending it
145
+ again rejects with `ERR_BAD_REQUEST`.
146
+
116
147
  ## Rules `.use()` enforces at runtime
117
148
 
118
149
  - `name` must be a non-empty own string, otherwise `ERR_BAD_OPTION_VALUE`.
package/src/index.ts CHANGED
@@ -11,7 +11,7 @@ import type { CreateFaxiosDefaults } from "./lib/types.ts";
11
11
  export { default as Faxios } from "./lib/core/Faxios.ts";
12
12
  export { default as FaxiosError, isSchemaValidationError } from "./lib/core/FaxiosError.ts";
13
13
  export type { SchemaValidationError } from "./lib/core/FaxiosError.ts";
14
- export { default as CanceledError, default as Cancel } from "./lib/cancel/CanceledError.ts";
14
+ export { default as Cancel, default as CanceledError } from "./lib/cancel/CanceledError.ts";
15
15
  export { default as isCancel } from "./lib/cancel/isCancel.ts";
16
16
  export { default as isFaxiosError } from "./lib/helpers/isFaxiosError.ts";
17
17
  export { default as toFormData } from "./lib/helpers/toFormData.ts";
@@ -23,28 +23,28 @@ export { VERSION } from "./lib/env/data.ts";
23
23
  export type { FaxiosInstance, FaxiosStatic } from "./lib/faxios.ts";
24
24
  export type { StandardSchemaV1 } from "./lib/types/standard-schema.ts";
25
25
  export type { BasePerCallConfig, DefineConfig, DefinedEndpoint, FaxiosLike, PerCallConfig } from "./lib/core/define.ts";
26
- export type { RouteConfig, RouteMethodConfig, RouteBuilder } from "./lib/core/route.ts";
26
+ export type { RouteBuilder, RouteConfig, RouteMethodConfig } from "./lib/core/route.ts";
27
27
 
28
28
  export type {
29
+ CreateFaxiosDefaults,
30
+ FaxiosConfigHeaders,
31
+ FaxiosContext,
32
+ FaxiosHeaderValue,
33
+ FaxiosMiddleware,
34
+ FaxiosNext,
35
+ FaxiosPlugin,
36
+ FaxiosPromise,
29
37
  FaxiosRequestConfig,
30
- InternalFaxiosRequestConfig,
31
- FaxiosResponse,
32
38
  FaxiosRequestHeaders,
39
+ FaxiosResponse,
33
40
  FaxiosResponseHeadersLike,
34
- FaxiosHeaderValue,
35
- Method,
36
- ResponseType,
37
41
  GenericHTMLFormElement,
42
+ HeadersDefaults,
43
+ InternalFaxiosRequestConfig,
44
+ Method,
38
45
  RawFaxiosRequestConfig,
39
- FaxiosPromise,
40
- CreateFaxiosDefaults,
41
- FaxiosConfigHeaders,
42
46
  RawFaxiosRequestHeaders,
43
- HeadersDefaults,
44
- FaxiosContext,
45
- FaxiosNext,
46
- FaxiosMiddleware,
47
- FaxiosPlugin
47
+ ResponseType
48
48
  } from "./lib/types.ts";
49
49
 
50
50
  // Instance-synthesized members (no 1:1 source module). Types are self-contained
@@ -3,9 +3,9 @@
3
3
  import FaxiosError from "../core/FaxiosError.js";
4
4
  import type FaxiosHeaders from "../core/FaxiosHeaders.js";
5
5
  import type {
6
- InternalFaxiosRequestConfig,
7
6
  FaxiosRequestHeaders,
8
- FaxiosResponse
7
+ FaxiosResponse,
8
+ InternalFaxiosRequestConfig
9
9
  } from "../types.js";
10
10
  import utils from "../utils.js";
11
11
 
@@ -144,7 +144,7 @@ export const cleanFormDataContentType = (
144
144
  headers: FaxiosRequestHeaders
145
145
  ): void => {
146
146
  if (utils.isFormData(data)) {
147
- const contentType = headers.getContentType() as string | null | undefined;
147
+ const contentType = headers.getContentType() as null | string | undefined;
148
148
  if (
149
149
  contentType &&
150
150
  /^multipart\/form-data/i.test(contentType) &&
@@ -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
- progressEventReducer,
9
+ asyncDecorator,
10
10
  progressEventDecorator,
11
- asyncDecorator
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
- InternalFaxiosRequestConfig,
18
- FaxiosRequestHeaders
17
+ FaxiosRequestHeaders,
18
+ InternalFaxiosRequestConfig
19
19
  } from "../types.js";
20
20
  import utils from "../utils.js";
21
- import { checkDeclaredContentLength, checkMaterializedSize, handleFetchCaughtError, cleanFormDataContentType, encodeBodyIfNeeded } from "./fetch-helpers.js";
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,6 +58,29 @@ 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
+
61
84
  // Server runtimes default User-Agent to their own (e.g. "node"), so name faxios there.
62
85
  // Browsers send their own User-Agent; setting one makes every cross-origin request
63
86
  // need a CORS preflight that most APIs don't allow.
@@ -116,8 +139,40 @@ const maybeWithAuthCredentials = (url: string): boolean => {
116
139
  return urlToCheck.includes("@");
117
140
  };
118
141
 
142
+ // RequestInit members faxios sets itself; the same keys in `fetchOptions` are ignored.
143
+ const FAXIOS_INIT_KEYS: Record<string, true> = {
144
+ body: true,
145
+ credentials: true,
146
+ duplex: true,
147
+ headers: true,
148
+ method: true,
149
+ signal: true,
150
+ };
151
+
152
+ // Spec defaults written as own init members when the caller sets none. undici converts the init
153
+ // into a plain dictionary and reads absent members from it, so an absent member still picks up a
154
+ // polluted Object.prototype value; an own default leaves nothing to inherit.
155
+ const REQUEST_INIT_DEFAULTS: Record<string, unknown> = {
156
+ cache: "default",
157
+ integrity: "",
158
+ keepalive: false,
159
+ mode: "cors",
160
+ redirect: "follow",
161
+ referrer: "about:client",
162
+ referrerPolicy: "",
163
+ };
164
+
165
+ // Where undici (Node's fetch) keeps its global dispatcher.
166
+ const UNDICI_GLOBAL_DISPATCHER = Symbol.for("undici.globalDispatcher.1");
167
+
168
+ type FetchEnv = {
169
+ fetch?: unknown;
170
+ Request?: unknown;
171
+ Response?: unknown;
172
+ };
173
+
119
174
  // eslint-disable-next-line sonarjs/function-return-type
120
- const factory = (env: Record<string, unknown>) => {
175
+ const factory = (env: FetchEnv) => {
121
176
  // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
122
177
  const globalObject: Record<string, unknown> = utils.global ?? globalThis;
123
178
  const ReadableStream = globalObject["ReadableStream"] as
@@ -127,26 +182,10 @@ const factory = (env: Record<string, unknown>) => {
127
182
  | (new () => AnyTextEncoder)
128
183
  | undefined;
129
184
 
130
- env = utils.merge.call(
131
- {
132
- skipUndefined: true,
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
- };
185
+ // `undefined` falls back to the global constructor; `null` disables it.
186
+ const envFetch = env.fetch as FetchFn | undefined;
187
+ const Request = (env.Request === undefined ? globalObject["Request"] : env.Request) as AnyConstructor | undefined;
188
+ const Response = (env.Response === undefined ? globalObject["Response"] : env.Response) as AnyConstructor | undefined;
150
189
  const _globalFetch = (globalThis as unknown as Record<string, unknown>)[
151
190
  "fetch"
152
191
  ];
@@ -160,6 +199,31 @@ const factory = (env: Record<string, unknown>) => {
160
199
  return false;
161
200
  }
162
201
 
202
+ // Only members this runtime's Request exposes, so an engine that rejects members it doesn't
203
+ // implement (Cloudflare Workers) is never sent one.
204
+ const requestInitDefaults = Object.entries(REQUEST_INIT_DEFAULTS).filter(([ key ]) =>
205
+ isRequestSupported && utils.hasOwnInPrototypeChain((Request as AnyConstructor).prototype, key));
206
+
207
+ // Null prototype: WebIDL reads every RequestInit member with Get, which walks the prototype
208
+ // chain, so a plain object literal would let a polluted Object.prototype.redirect, .integrity,
209
+ // .mode or undici .dispatcher apply to every request. Every own enumerable key of fetchOptions
210
+ // is copied, so runtime extensions (Next.js `next`, Workers `cf`, Bun `tls`) still pass through;
211
+ // inherited keys never do. The caller sets signal, method, headers, body, duplex, credentials.
212
+ const initFromFetchOptions = (fetchOptions: unknown): Record<string, unknown> => {
213
+ const init: Record<string, unknown> = Object.create(null);
214
+ for (const [ key, value ] of requestInitDefaults) init[key] = value;
215
+ // Node's global fetch reads `dispatcher` the same way; pin it to undici's own global one.
216
+ if (!envFetch && utils.hasOwnProp(globalThis, UNDICI_GLOBAL_DISPATCHER)) {
217
+ init["dispatcher"] = (globalThis as Record<PropertyKey, unknown>)[UNDICI_GLOBAL_DISPATCHER];
218
+ }
219
+ if (utils.isObject(fetchOptions)) {
220
+ for (const key of Object.keys(fetchOptions)) {
221
+ if (!utils.hasOwnProp(FAXIOS_INIT_KEYS, key)) init[key] = (fetchOptions as Record<string, unknown>)[key];
222
+ }
223
+ }
224
+ return init;
225
+ };
226
+
163
227
  const isReadableStreamSupported = isFunction(ReadableStream);
164
228
 
165
229
  const encodeText: (str: string) => Promise<Uint8Array> | Uint8Array =
@@ -179,7 +243,7 @@ const factory = (env: Record<string, unknown>) => {
179
243
  // `fetch(request, init)`. undici (Node 26.11+) starts reading an async iterable in both,
180
244
  // so the first chunk goes to the discarded copy. One web stream is read only once.
181
245
  // 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; } | undefined)?.from;
246
+ const from = (ReadableStream as undefined | { from?: (source: unknown) => unknown; })?.from;
183
247
  const streamFrom = typeof from === "function" ? from.bind(ReadableStream) : undefined;
184
248
  async function* toByteChunks(source: AsyncIterable<unknown>) {
185
249
  for await (const chunk of source) yield typeof chunk === "string" ? await encodeText(chunk) : chunk;
@@ -269,7 +333,7 @@ const factory = (env: Record<string, unknown>) => {
269
333
  }
270
334
 
271
335
  if (utils.isArrayBufferView(body) || utils.isArrayBuffer(body)) {
272
- return (body as ArrayBufferView | ArrayBuffer).byteLength;
336
+ return (body as ArrayBuffer | ArrayBufferView).byteLength;
273
337
  }
274
338
 
275
339
  if (utils.isURLSearchParams(body)) {
@@ -291,7 +355,7 @@ const factory = (env: Record<string, unknown>) => {
291
355
  return length == null ? getBodyLength(body) : length;
292
356
  };
293
357
 
294
- const normalizeCredentials = (wc: string | boolean): string => {
358
+ const normalizeCredentials = (wc: boolean | string): string => {
295
359
  if (utils.isString(wc)) return wc as string;
296
360
  return wc ? "include" : "omit";
297
361
  };
@@ -303,7 +367,7 @@ const factory = (env: Record<string, unknown>) => {
303
367
  headers: FaxiosRequestHeaders,
304
368
  own: (key: string) => unknown
305
369
  ): string => {
306
- let auth: { username: unknown; password: unknown; } | undefined;
370
+ let auth: undefined | { password: unknown; username: unknown; };
307
371
  const configAuth = own("auth");
308
372
  if (configAuth) {
309
373
  auth = {
@@ -398,7 +462,7 @@ const factory = (env: Record<string, unknown>) => {
398
462
  onUploadProgress: unknown,
399
463
  _trackStream: (stream: unknown, onProgress?: (bytes: number) => void, flush?: () => void) => unknown
400
464
  ): unknown => {
401
- let contentTypeHeader: string | null;
465
+ let contentTypeHeader: null | string;
402
466
  if (
403
467
  utils.isFormData(data) &&
404
468
  (contentTypeHeader = _request.headers.get("content-type"))
@@ -587,7 +651,7 @@ const factory = (env: Record<string, unknown>) => {
587
651
  return async (config: InternalFaxiosRequestConfig) => {
588
652
  const _resolved = prepareRequest(config) as InternalFaxiosRequestConfig & {
589
653
  fetchOptions?: Record<string, unknown>;
590
- withCredentials?: string | boolean;
654
+ withCredentials?: boolean | string;
591
655
  };
592
656
  let {
593
657
  url,
@@ -604,7 +668,7 @@ const factory = (env: Record<string, unknown>) => {
604
668
  maxBodyLength,
605
669
  } = _resolved;
606
670
  let headers = _resolved.headers;
607
- let withCredentials: string | boolean =
671
+ let withCredentials: boolean | string =
608
672
  _resolved.withCredentials ?? "same-origin";
609
673
 
610
674
  const hasMaxContentLength =
@@ -638,10 +702,12 @@ const factory = (env: Record<string, unknown>) => {
638
702
 
639
703
  const unsubscribe = composedSignal?.unsubscribe?.bind(composedSignal);
640
704
 
641
- let request: Request | null = null;
705
+ let request: null | Request = null;
642
706
  const pendingBodyErrorRef: PendingBodyErrorRef = { value: null };
643
707
 
644
708
  try {
709
+ rejectUsedStreamBody(data, config);
710
+
645
711
  url = applyAuthToRequest(url!, headers, own);
646
712
 
647
713
  const preflightContentLength = await checkPreflightLimits(
@@ -696,25 +762,20 @@ const factory = (env: Record<string, unknown>) => {
696
762
  // Case-insensitive check: toByteStringHeaderObject may normalize to lowercase.
697
763
  data = toWebStream(await encodeBodyIfNeeded(data, serializedHeaders, encodeText));
698
764
 
699
- const resolvedOptions = {
700
- ...fetchOptions,
701
- signal: composedSignal,
702
- method: (method as string).toUpperCase(),
703
- headers: serializedHeaders,
704
- body: data,
705
- duplex: "half",
706
- credentials: isCredentialsSupported ? withCredentials : undefined,
707
- };
765
+ const resolvedOptions = initFromFetchOptions(fetchOptions);
766
+ resolvedOptions["signal"] = composedSignal;
767
+ resolvedOptions["method"] = (method as string).toUpperCase();
768
+ resolvedOptions["headers"] = serializedHeaders;
769
+ resolvedOptions["body"] = data;
770
+ resolvedOptions["duplex"] = "half";
771
+ resolvedOptions["credentials"] = isCredentialsSupported ? withCredentials : undefined;
708
772
 
709
773
  request = isRequestSupported
710
774
  ? new (Request as AnyConstructor)(url, resolvedOptions) as Request
711
775
  : null;
712
776
 
713
- // Pass resolvedOptions (all own-properties) as the fetch init even when a
714
- // Request object is supplied: undici re-reads RequestInit fields (method,
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.
777
+ // Pass resolvedOptions as the fetch init even when a Request object is supplied, so the
778
+ // same null-prototype members apply to both calls.
718
779
  let response = await (isRequestSupported
719
780
  ? _fetch(request, resolvedOptions)
720
781
  : _fetch(url, resolvedOptions));
@@ -778,12 +839,12 @@ const factory = (env: Record<string, unknown>) => {
778
839
  const seedCache = new Map<unknown, unknown>();
779
840
 
780
841
  export const getFetch = (config?: { env?: Record<string, unknown>; }) => {
781
- const env: Record<string, unknown> = (config && config.env) || {};
782
- const { fetch, Request, Response } = env as {
783
- fetch?: unknown;
784
- Request?: unknown;
785
- Response?: unknown;
786
- };
842
+ // Own reads only: a polluted Object.prototype.fetch must never become the transport.
843
+ const env = config && utils.hasOwnProp(config, "env") ? config.env : undefined;
844
+ const own = (key: keyof FetchEnv): unknown => (env && utils.hasOwnProp(env, key) ? env[key] : undefined);
845
+ const fetch = own("fetch");
846
+ const Request = own("Request");
847
+ const Response = own("Response");
787
848
  const seeds: Array<unknown> = [ Request, Response, fetch ];
788
849
 
789
850
  const len = seeds.length;
@@ -797,7 +858,7 @@ export const getFetch = (config?: { env?: Record<string, unknown>; }) => {
797
858
  target = map.get(seed);
798
859
 
799
860
  if (target === undefined) {
800
- target = i ? new Map<unknown, unknown>() : factory(env);
861
+ target = i ? new Map<unknown, unknown>() : factory({ fetch, Request, Response });
801
862
  map.set(seed, target);
802
863
  }
803
864
 
@@ -13,7 +13,7 @@ class CanceledError extends FaxiosError {
13
13
  *
14
14
  * @returns {CanceledError} The created error.
15
15
  */
16
- constructor(message?: string | null, config?: FaxiosRequestConfig, request?: unknown) {
16
+ constructor(message?: null | string, config?: FaxiosRequestConfig, request?: unknown) {
17
17
  super(message == null ? "canceled" : message, FaxiosError.ERR_CANCELED, config as InternalFaxiosRequestConfig, request);
18
18
  this.name = "CanceledError";
19
19
  (this as unknown as Record<string, unknown>)["__CANCEL__"] = true;
@@ -2,10 +2,9 @@
2
2
 
3
3
  import CanceledError from "../cancel/CanceledError.js";
4
4
  import buildURL from "../helpers/buildURL.js";
5
- import cloneConfig, { DANGEROUS_KEYS } from "../helpers/cloneConfig.js";
5
+ import cloneConfig from "../helpers/cloneConfig.js";
6
6
  import validator from "../helpers/validator.js";
7
7
  import type { ValidatorFn } from "../helpers/validator.js";
8
- import type { StandardSchemaV1 } from "../types/standard-schema.js";
9
8
  import type {
10
9
  FaxiosContext,
11
10
  FaxiosMiddleware,
@@ -21,6 +20,7 @@ import type {
21
20
  SchemaConfig,
22
21
  StringLiteralsOrString
23
22
  } from "../types.js";
23
+ import type { StandardSchemaV1 } from "../types/standard-schema.js";
24
24
  import utils from "../utils.js";
25
25
  import buildFullPath from "./buildFullPath.js";
26
26
  import { createDefinedEndpoint } from "./define.js";
@@ -29,21 +29,21 @@ import dispatchRequest from "./dispatchRequest.js";
29
29
  import FaxiosError from "./FaxiosError.js";
30
30
  import FaxiosHeaders from "./FaxiosHeaders.js";
31
31
  import mergeConfig from "./mergeConfig.js";
32
- import type { RouteConfig, RouteBuilder } from "./route.js";
32
+ import type { RouteBuilder, RouteConfig } from "./route.js";
33
33
  import { createRouteBuilder } from "./route.js";
34
34
 
35
35
  type TransitionalFn = (
36
- validator: ValidatorFn | false | undefined,
36
+ validator: false | undefined | ValidatorFn,
37
37
  version?: string,
38
38
  message?: string
39
39
  ) => ValidatorFn;
40
40
  type SpellingFn = (correctSpelling: string) => ValidatorFn;
41
41
  const validators = validator.validators as Record<
42
42
  string,
43
- ValidatorFn | undefined
43
+ undefined | ValidatorFn
44
44
  > & {
45
- transitional?: TransitionalFn;
46
45
  spelling?: SpellingFn;
46
+ transitional?: TransitionalFn;
47
47
  };
48
48
 
49
49
  // Every method header group declared on HeadersDefaults. `satisfies` keeps this
@@ -133,7 +133,10 @@ function resolveAllowAbsoluteUrls(
133
133
  defaults: FaxiosRequestConfig
134
134
  ): void {
135
135
  if (config.allowAbsoluteUrls === undefined) {
136
- config.allowAbsoluteUrls = defaults.allowAbsoluteUrls ?? !defaults.baseURL;
136
+ // Own reads only: instance defaults may be a plain object (the default instance's are).
137
+ const allowAbsoluteUrls = utils.hasOwnProp(defaults, "allowAbsoluteUrls") ? defaults.allowAbsoluteUrls : undefined;
138
+ const baseURL = utils.hasOwnProp(defaults, "baseURL") ? defaults.baseURL : undefined;
139
+ config.allowAbsoluteUrls = allowAbsoluteUrls ?? !baseURL;
137
140
  }
138
141
  }
139
142
 
@@ -143,10 +146,10 @@ type AnyContextMiddleware = FaxiosMiddleware<unknown, unknown>;
143
146
  type AnyContextNext = FaxiosNext<unknown, unknown>;
144
147
 
145
148
  type MiddlewareEntry = {
149
+ provided: Array<string>;
146
150
  // The value passed to use(), kept only for eject()'s identity check.
147
151
  ref: unknown;
148
152
  run: AnyContextMiddleware;
149
- provided: Array<string>;
150
153
  };
151
154
 
152
155
  function composeMiddleware(entries: ReadonlyArray<MiddlewareEntry>, dispatch: AnyContextNext): AnyContextNext {
@@ -272,7 +275,7 @@ class Faxios {
272
275
 
273
276
  #addCapabilities(pluginName: string, provides: unknown): Array<string> {
274
277
  const source = provides as Record<string, unknown>;
275
- const keys = Object.keys(source).filter(key => !DANGEROUS_KEYS.has(key));
278
+ const keys = Object.keys(source).filter(key => !utils.isDangerousKey(key));
276
279
  for (const key of keys) {
277
280
  if (utils.hasOwnProp(this.#capabilities, key)) {
278
281
  throw new FaxiosError(`Plugin "${pluginName}" provides capability "${key}", which another plugin already provides`, FaxiosError.ERR_BAD_OPTION);
@@ -295,9 +298,9 @@ class Faxios {
295
298
  */
296
299
 
297
300
  async request<O, D = unknown>(config: SchemaConfig<O, D>): Promise<FaxiosResponse<O, D>>;
298
- async request<T = unknown, R = FaxiosResponse<T>, D = unknown>(configOrUrl: string | FaxiosRequestConfig<D>, config?: FaxiosRequestConfig<D>): Promise<R>;
301
+ async request<T = unknown, R = FaxiosResponse<T>, D = unknown>(configOrUrl: FaxiosRequestConfig<D> | string, config?: FaxiosRequestConfig<D>): Promise<R>;
299
302
  async request(
300
- configOrUrl: string | FaxiosRequestConfig,
303
+ configOrUrl: FaxiosRequestConfig | string,
301
304
  config?: FaxiosRequestConfig
302
305
  ) {
303
306
  try {
@@ -313,7 +316,7 @@ class Faxios {
313
316
  }
314
317
 
315
318
  async #request(
316
- configOrUrl: string | FaxiosRequestConfig,
319
+ configOrUrl: FaxiosRequestConfig | string,
317
320
  config?: FaxiosRequestConfig
318
321
  ): Promise<unknown> {
319
322
  /*eslint no-param-reassign:0*/
@@ -361,8 +364,9 @@ class Faxios {
361
364
  );
362
365
 
363
366
  // Set config.method
367
+ const defaultMethod = utils.hasOwnProp(this.defaults, "method") ? this.defaults.method : undefined;
364
368
  config.method = (
365
- (config.method || this.defaults.method || "get") as string
369
+ (config.method || defaultMethod || "get") as string
366
370
  ).toLowerCase();
367
371
 
368
372
  // Flatten headers
@@ -1,8 +1,8 @@
1
1
  // @ts-self-types="./FaxiosError.d.ts" — required for Deno: index.d.ts re-exports SchemaValidationError from here
2
2
  "use strict";
3
3
 
4
+ import type { FaxiosResponse, InternalFaxiosRequestConfig } from "../types.js";
4
5
  import type { StandardSchemaV1 } from "../types/standard-schema.js";
5
- import type { InternalFaxiosRequestConfig, FaxiosResponse } from "../types.js";
6
6
  import utils from "../utils.js";
7
7
  import FaxiosHeaders from "./FaxiosHeaders.js";
8
8
 
@@ -247,9 +247,9 @@ export type SchemaValidationError =
247
247
  }
248
248
  | FaxiosError & {
249
249
  code:
250
- | typeof FaxiosError.ERR_BAD_REQUEST_SCHEMA
251
250
  | typeof FaxiosError.ERR_BAD_PARAMS_SCHEMA
252
- | typeof FaxiosError.ERR_BAD_PATH_PARAMS_SCHEMA;
251
+ | typeof FaxiosError.ERR_BAD_PATH_PARAMS_SCHEMA
252
+ | typeof FaxiosError.ERR_BAD_REQUEST_SCHEMA;
253
253
  issues: SchemaIssues;
254
254
  };
255
255