@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.
Files changed (131) 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 +4 -1
  8. package/dist/lib/adapters/fetch.d.ts.map +1 -1
  9. package/dist/lib/adapters/fetch.js +210 -92
  10. package/dist/lib/adapters/fetch.js.map +1 -1
  11. package/dist/lib/cancel/CanceledError.d.ts +1 -1
  12. package/dist/lib/cancel/CanceledError.d.ts.map +1 -1
  13. package/dist/lib/core/Faxios.d.ts +3 -3
  14. package/dist/lib/core/Faxios.d.ts.map +1 -1
  15. package/dist/lib/core/Faxios.js +8 -4
  16. package/dist/lib/core/Faxios.js.map +1 -1
  17. package/dist/lib/core/FaxiosError.d.ts +2 -2
  18. package/dist/lib/core/FaxiosError.d.ts.map +1 -1
  19. package/dist/lib/core/FaxiosHeaders.d.ts +8 -8
  20. package/dist/lib/core/FaxiosHeaders.d.ts.map +1 -1
  21. package/dist/lib/core/FaxiosHeaders.js +31 -7
  22. package/dist/lib/core/FaxiosHeaders.js.map +1 -1
  23. package/dist/lib/core/define.d.ts +3 -3
  24. package/dist/lib/core/define.d.ts.map +1 -1
  25. package/dist/lib/core/dispatchRequest.d.ts +1 -1
  26. package/dist/lib/core/dispatchRequest.d.ts.map +1 -1
  27. package/dist/lib/core/dispatchRequest.js +6 -1
  28. package/dist/lib/core/dispatchRequest.js.map +1 -1
  29. package/dist/lib/core/mergeConfig.d.ts +2 -2
  30. package/dist/lib/core/mergeConfig.d.ts.map +1 -1
  31. package/dist/lib/core/mergeConfig.js +40 -2
  32. package/dist/lib/core/mergeConfig.js.map +1 -1
  33. package/dist/lib/core/route.d.ts +1 -1
  34. package/dist/lib/core/route.d.ts.map +1 -1
  35. package/dist/lib/core/route.js +2 -5
  36. package/dist/lib/core/route.js.map +1 -1
  37. package/dist/lib/core/transformData.d.ts +2 -2
  38. package/dist/lib/core/transformData.d.ts.map +1 -1
  39. package/dist/lib/core/validateSchema.d.ts +2 -2
  40. package/dist/lib/core/validateSchema.d.ts.map +1 -1
  41. package/dist/lib/env/data.d.ts +1 -1
  42. package/dist/lib/env/data.js +1 -1
  43. package/dist/lib/faxios.d.ts +50 -50
  44. package/dist/lib/faxios.d.ts.map +1 -1
  45. package/dist/lib/helpers/cloneConfig.d.ts +0 -1
  46. package/dist/lib/helpers/cloneConfig.d.ts.map +1 -1
  47. package/dist/lib/helpers/cloneConfig.js +2 -2
  48. package/dist/lib/helpers/cloneConfig.js.map +1 -1
  49. package/dist/lib/helpers/cookies.d.ts +1 -1
  50. package/dist/lib/helpers/cookies.d.ts.map +1 -1
  51. package/dist/lib/helpers/formDataToJSON.d.ts +1 -1
  52. package/dist/lib/helpers/formDataToJSON.d.ts.map +1 -1
  53. package/dist/lib/helpers/formDataToJSON.js +1 -1
  54. package/dist/lib/helpers/formDataToJSON.js.map +1 -1
  55. package/dist/lib/helpers/parseHeaders.d.ts +1 -1
  56. package/dist/lib/helpers/parseHeaders.d.ts.map +1 -1
  57. package/dist/lib/helpers/progressEventReducer.d.ts.map +1 -1
  58. package/dist/lib/helpers/progressEventReducer.js +14 -1
  59. package/dist/lib/helpers/progressEventReducer.js.map +1 -1
  60. package/dist/lib/helpers/substitutePathParams.d.ts.map +1 -1
  61. package/dist/lib/helpers/substitutePathParams.js +11 -3
  62. package/dist/lib/helpers/substitutePathParams.js.map +1 -1
  63. package/dist/lib/helpers/toFormData.d.ts.map +1 -1
  64. package/dist/lib/helpers/toFormData.js +16 -20
  65. package/dist/lib/helpers/toFormData.js.map +1 -1
  66. package/dist/lib/helpers/trackStream.d.ts.map +1 -1
  67. package/dist/lib/helpers/trackStream.js +27 -3
  68. package/dist/lib/helpers/trackStream.js.map +1 -1
  69. package/dist/lib/helpers/validator.d.ts +1 -1
  70. package/dist/lib/helpers/validator.d.ts.map +1 -1
  71. package/dist/lib/platform.d.ts.map +1 -1
  72. package/dist/lib/plugins/authBearer.d.ts +9 -5
  73. package/dist/lib/plugins/authBearer.d.ts.map +1 -1
  74. package/dist/lib/plugins/authBearer.js +6 -0
  75. package/dist/lib/plugins/authBearer.js.map +1 -1
  76. package/dist/lib/plugins/definePlugin.d.ts +3 -3
  77. package/dist/lib/plugins/definePlugin.d.ts.map +1 -1
  78. package/dist/lib/plugins/retry.d.ts +23 -23
  79. package/dist/lib/plugins/retry.d.ts.map +1 -1
  80. package/dist/lib/plugins/timing.d.ts +8 -8
  81. package/dist/lib/plugins/timing.d.ts.map +1 -1
  82. package/dist/lib/types/standard-schema.d.ts +6 -6
  83. package/dist/lib/types/standard-schema.d.ts.map +1 -1
  84. package/dist/lib/types.d.ts +106 -92
  85. package/dist/lib/types.d.ts.map +1 -1
  86. package/dist/lib/utils.d.ts +4 -3
  87. package/dist/lib/utils.d.ts.map +1 -1
  88. package/dist/lib/utils.js +18 -5
  89. package/dist/lib/utils.js.map +1 -1
  90. package/package.json +7 -4
  91. package/skills/configure-requests/SKILL.md +14 -8
  92. package/skills/configure-requests/references/axios-differences.md +1 -1
  93. package/skills/configure-requests/references/plugins.md +37 -6
  94. package/src/index.ts +15 -15
  95. package/src/lib/adapters/fetch-helpers.ts +3 -3
  96. package/src/lib/adapters/fetch.ts +263 -144
  97. package/src/lib/cancel/CanceledError.ts +1 -1
  98. package/src/lib/core/Faxios.ts +17 -13
  99. package/src/lib/core/FaxiosError.ts +3 -3
  100. package/src/lib/core/FaxiosHeaders.ts +50 -23
  101. package/src/lib/core/define.ts +7 -7
  102. package/src/lib/core/dispatchRequest.ts +8 -3
  103. package/src/lib/core/mergeConfig.ts +42 -4
  104. package/src/lib/core/prepareRequest.ts +1 -1
  105. package/src/lib/core/route.ts +3 -6
  106. package/src/lib/core/transformData.ts +2 -2
  107. package/src/lib/core/validateSchema.ts +4 -4
  108. package/src/lib/defaults/index.ts +1 -1
  109. package/src/lib/env/data.ts +1 -1
  110. package/src/lib/faxios.ts +55 -55
  111. package/src/lib/helpers/HttpStatusCode.ts +1 -1
  112. package/src/lib/helpers/cloneConfig.ts +2 -3
  113. package/src/lib/helpers/composeSignals.ts +4 -4
  114. package/src/lib/helpers/cookies.ts +3 -3
  115. package/src/lib/helpers/formDataToJSON.ts +4 -4
  116. package/src/lib/helpers/parseHeaders.ts +2 -2
  117. package/src/lib/helpers/progressEventReducer.ts +15 -2
  118. package/src/lib/helpers/sanitizeHeaderValue.ts +1 -1
  119. package/src/lib/helpers/substitutePathParams.ts +13 -3
  120. package/src/lib/helpers/throttle.ts +1 -1
  121. package/src/lib/helpers/toFormData.ts +26 -28
  122. package/src/lib/helpers/trackStream.ts +40 -12
  123. package/src/lib/helpers/validator.ts +3 -3
  124. package/src/lib/platform.ts +4 -4
  125. package/src/lib/plugins/authBearer.ts +14 -9
  126. package/src/lib/plugins/definePlugin.ts +3 -3
  127. package/src/lib/plugins/retry.ts +27 -27
  128. package/src/lib/plugins/timing.ts +5 -5
  129. package/src/lib/types/standard-schema.ts +6 -6
  130. package/src/lib/types.ts +139 -125
  131. package/src/lib/utils.ts +30 -15
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gcmdev/faxios",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "Promise based, web-standard fetch HTTP client for the browser, Node.js, Deno and Bun",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",
@@ -52,7 +52,7 @@
52
52
  "bp": "pnpm build && pnpm pack",
53
53
  "version": "pnpm run build && git add package.json",
54
54
  "docs:dev": "cd docs && npm run docs:dev",
55
- "lint": "pnpm lint:ts && pnpm lint:esl && pnpm lint:fix",
55
+ "lint": "pnpm lint:ts && pnpm lint:esl && pnpm lint:s",
56
56
  "lint:ts": "tsc --noEmit -p tsconfig.check.json",
57
57
  "lint:esl": "eslint --config eslint.config.ts \"**/*.{j,t}s{,x}\" --cache --cache-strategy content --max-warnings=0",
58
58
  "lint:s": "eslint --config eslint.config.style.ts \"**/*.{j,t}s{,x}\" --cache --cache-strategy content --max-warnings=0",
@@ -60,8 +60,8 @@
60
60
  "lint:e18e": "pnpm dlx @e18e/cli analyze"
61
61
  },
62
62
  "devDependencies": {
63
- "@gingacodemonkey/config": "^0.0.38",
64
- "eslint": "^9.39.4",
63
+ "@gingacodemonkey/config": "^0.1.0",
64
+ "eslint": "^9.39.5",
65
65
  "typescript": "^6.0.3",
66
66
  "typescript-eslint": "^8.71.0",
67
67
  "zshy": "latest"
@@ -129,5 +129,8 @@
129
129
  "default": "./dist/lib/utils.js"
130
130
  },
131
131
  "./package.json": "./package.json"
132
+ },
133
+ "imports": {
134
+ "#src/*.ts": "./src/*.ts"
132
135
  }
133
136
  }
@@ -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
 
@@ -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
 
@@ -273,6 +276,7 @@ plugin's request options (such as `retry: false`). Before writing plugin code, r
273
276
  `baseURL` origin when `baseURL` is absolute; otherwise only to relative request URLs. Use
274
277
  `origins` to allow more. It leaves an explicit `Authorization` header alone unless
275
278
  `overwrite: true`. Install `retry` before it if each attempt should call `getToken` again.
279
+ A `getToken` result that isn't a non-empty string sends the request without the header.
276
280
  - `retry` defaults: 3 attempts, statuses 408/429/500/502/503/504 plus `ERR_NETWORK` and
277
281
  `ETIMEDOUT`, exponential backoff from 100 ms with full jitter. A 429/503 `Retry-After` sets the
278
282
  wait unless `respectRetryAfter: false`. Turn that off when something else, such as a queue
@@ -304,9 +308,11 @@ const { data } = await testApi.get("https://api.example.com/feed"); // { ok: tru
304
308
  await faxios.get("https://api.example.com/feed", { fetchOptions: { cache: "no-store" } });
305
309
  ```
306
310
 
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`.
311
+ - Every own enumerable key of `fetchOptions` is copied into a null-prototype init passed to
312
+ `fetch`, including runtime extensions (Next.js `next`, Workers `cf`, undici `dispatcher`).
313
+ Inherited keys are ignored. faxios sets `signal`, `method`, `headers`, `body`, `duplex` and
314
+ `credentials` itself and ignores those keys in `fetchOptions`.
315
+ - `env.fetch`, `env.Request` and `env.Response` count only as own properties of `env`.
310
316
  - Plain-object values inside `fetchOptions` are copied when configs merge; class instances are
311
317
  passed by reference.
312
318
  - Proxies are runtime configuration, not a faxios option. In Node, the project docs route a
@@ -353,9 +359,9 @@ export only. Instances from `create()` do not have them; import them by name ins
353
359
  `faxios.plugins` is undefined. Import from `@gcmdev/faxios/plugins/<name>`.
354
360
  7. **MEDIUM: Calling statics on an instance.** `api.isCancel(e)` is undefined on a `create()`
355
361
  instance. Use the named import.
356
- 8. **MEDIUM: Expecting `onUploadProgress` everywhere.** It fires only where `fetch` can stream a
357
- request body (`duplex: "half"`, for example Node). Browsers generally do not report upload
358
- progress through fetch; do not promise it there.
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`).
359
365
  9. **MEDIUM: Retrying POST by accident or by assumption.** The retry plugin skips POST and PATCH
360
366
  unless they are listed in `methods`; a custom `retryOn` cannot override that.
361
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); not a browser guarantee |
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 |
@@ -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) &&