create-request 1.6.0 → 2.0.0-next.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +58 -0
- package/MIGRATION.md +158 -0
- package/README.md +422 -1292
- package/dist/index.cjs +730 -0
- package/dist/index.d.cts +851 -0
- package/dist/index.d.ts +851 -0
- package/dist/index.js +717 -0
- package/package.json +63 -67
- package/dist/library/BaseRequest.d.ts +0 -870
- package/dist/library/BodyRequest.d.ts +0 -101
- package/dist/library/RequestError.d.ts +0 -171
- package/dist/library/ResponseWrapper.d.ts +0 -182
- package/dist/library/apiBuilder.d.ts +0 -560
- package/dist/library/enums.d.ts +0 -85
- package/dist/library/index.cjs +0 -3160
- package/dist/library/index.cjs.map +0 -1
- package/dist/library/index.d.ts +0 -47
- package/dist/library/index.esm.js +0 -3140
- package/dist/library/index.esm.js.map +0 -1
- package/dist/library/index.esm.min.js +0 -1
- package/dist/library/index.esm.min.js.map +0 -1
- package/dist/library/index.min.cjs +0 -1
- package/dist/library/index.min.cjs.map +0 -1
- package/dist/library/requestFactories.d.ts +0 -91
- package/dist/library/requestMethods.d.ts +0 -91
- package/dist/library/types.d.ts +0 -329
- package/dist/library/utils/Config.d.ts +0 -221
- package/dist/library/utils/CookieUtils.d.ts +0 -9
- package/dist/library/utils/CsrfUtils.d.ts +0 -24
package/CHANGELOG.md
CHANGED
|
@@ -1 +1,59 @@
|
|
|
1
1
|
# Changelog
|
|
2
|
+
|
|
3
|
+
## 2.0.0 — unreleased
|
|
4
|
+
|
|
5
|
+
A ground-up rewrite with the same fluent API, a fifth fewer bytes on the wire, a 70 % smaller package and none of the known bugs. See [MIGRATION.md](MIGRATION.md) for the v1 → v2 map.
|
|
6
|
+
|
|
7
|
+
### Breaking
|
|
8
|
+
|
|
9
|
+
- **Node.js 22+** is required. The package ships `dist/index.js` (ESM), `dist/index.cjs` (CommonJS) and bundled declaration files; the `production` export condition and the `dist/library/*` files are gone.
|
|
10
|
+
- **No global configuration.** `create.config` and the `Config` singleton are removed; defaults live on immutable api instances (`createApi()` / `create.api()`), which now have every request method except body and signal ones, derived from the request type.
|
|
11
|
+
- **String literals instead of enums.** The `.withCache.NO_CACHE()`-style getters and the runtime enums (`CacheMode`, `CredentialsPolicy`, `RequestMode`, `RedirectMode`, `ReferrerPolicy`, `RequestPriority`, `SameSitePolicy`, `HttpMethod`) are removed; the setters take the DOM string unions.
|
|
12
|
+
- **CSRF is explicit.** Nothing is sent automatically: `withCsrf()` enables the XSRF-cookie → header copy (same-origin only), `withCsrf({ token })` sends a token you hold; `withoutCsrfProtection()`, `withAntiCsrfHeaders()` and `X-Requested-With` by default are gone.
|
|
13
|
+
- **One request class.** `GetRequest`, `PostRequest`, … are now type aliases of `HttpRequest<M, T>`; construct requests with the factories.
|
|
14
|
+
- `getJson<T>()` returns `Promise<T>` (an empty body still yields `null` at runtime); `getData`'s selector receives `T`, not `T | null`.
|
|
15
|
+
- `RequestError` takes an options object (`{ code, url, method, … }`), `error.getJson()` becomes `error.data`, and the static factories are removed. Messages are readable (`HTTP 404 Not Found`, `Request timed out after 5000ms`, …) and every rejection carries a `code`.
|
|
16
|
+
- Retries only retry retriable failures (network errors, timeouts, 408/425/429/500/502/503/504), with exponential backoff by default and `Retry-After` support; validation errors are not retried by default, aborts and stream bodies never; error interceptors run once, after the last attempt.
|
|
17
|
+
- `withQueryParams` replaces a key that is already present instead of appending; `null` removes it.
|
|
18
|
+
- Interceptors run in registration order (api-level first); a request interceptor may return nothing; `config.headers` is a lower-case-keyed copy; short-circuit responses go through the status check.
|
|
19
|
+
- `withCookie` / `withCookies` take string values sent verbatim; `CookieOptions` and `CookieUtils` are gone.
|
|
20
|
+
- `error.response`'s body is consumed when `error.body` is captured; `body` is capped at 1 MB (a larger declared `Content-Length` leaves the body unread, a longer chunked or compressed body is cut off and `body` is `undefined`).
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
- `withSignal(signal)` (combinable), `getResult()` (`{ data, error }`), `getFormData()`, `clone()`, `create.delete()` / `api.delete()`, `withCsrf(options)`, `isRequestError()`.
|
|
25
|
+
- Standard Schema validation: `getJson(schema)`, `getData(schema, selector?)`, `getResult(schema)` with zod, valibot, arktype and any other implementation — typed from the schema, failures have `code: "VALIDATION"` and `error.issues`.
|
|
26
|
+
- Response types at the request: `api.get<User>("/me").getJson()`.
|
|
27
|
+
- `RetryConfig.statuses`, `methods`, `maxDelay`, `shouldRetry`, `onRetry`; the `delay` callback receives `{ attempt, error }`, `onRetry` receives `{ attempt, error, delay }`.
|
|
28
|
+
- Query values accept numbers, booleans, `Date`s, arrays and `URLSearchParams`; header values accept numbers and `null` (unset).
|
|
29
|
+
- `DELETE` requests can carry a body; `ReadableStream` bodies are sent with `duplex: "half"`.
|
|
30
|
+
- `withRedirect("manual")` and `withMode("no-cors")` resolve with the redirect / opaque response.
|
|
31
|
+
- `withTimeout()` covers the whole exchange (response and body read); `0` / `Infinity` remove a timeout; a signal from `AbortSignal.timeout()` is reported as `"TIMEOUT"`.
|
|
32
|
+
- Response and error interceptors receive the `HttpRequest` as a second argument, so a failed request can be replayed with `request.clone()`.
|
|
33
|
+
- `withQueryParams`, `withHeaders`, `withCookies` and `withGraphQL` variables accept interface-typed objects.
|
|
34
|
+
- A `FormData` body drops any `Content-Type` (fetch sets the multipart boundary); an already-aborted signal fails the request before interceptors run; api defaults are validated when set; `Retry-After` accepts decimals.
|
|
35
|
+
|
|
36
|
+
### Fixed
|
|
37
|
+
|
|
38
|
+
- Reading a body in two formats (`getText()` then `getJson()`, …) no longer throws "Body used".
|
|
39
|
+
- Aborts with a custom reason are classified as aborted; network errors carry the underlying cause's message (or code) in their own message and keep `cause`; timeouts are detected from the signal, not from message text.
|
|
40
|
+
- Query strings are inserted before a `#fragment`; `./users` joins onto a base URL cleanly.
|
|
41
|
+
- Case-insensitive header merging; basic auth handles UTF-8 credentials.
|
|
42
|
+
- Library options (`timeout`, `retries`, …) no longer leak into `fetch`'s `RequestInit`.
|
|
43
|
+
- The timeout starts after request interceptors ran.
|
|
44
|
+
- An interceptor's header mutations no longer persist on the request across executions.
|
|
45
|
+
- A `URIError` from a malformed cookie can no longer escape `getResponse()`.
|
|
46
|
+
- `import { RequestError } from "create-request"` tree-shakes to ≈ 0.2 KB.
|
|
47
|
+
|
|
48
|
+
### Security
|
|
49
|
+
|
|
50
|
+
- CSRF/XSRF tokens are attached only to same-origin requests, judged on the final URL after interceptors and before any redirect, unless `crossOrigin: true` is passed (v1 leaked them to every host, cf. axios CVE-2023-45857). Outside a browser every URL counts as same-origin; `fetch` forwards custom headers across redirects, so pair `withCsrf()` with `withRedirect("error")` on endpoints that may redirect elsewhere.
|
|
51
|
+
- Error bodies are capped at 1 MB however they are encoded (a compressed or chunked 5xx can no longer inflate into memory), header values `fetch` would reject fail before the request is sent instead of being retried, and header/cookie objects are read by own keys only.
|
|
52
|
+
|
|
53
|
+
### Internal
|
|
54
|
+
|
|
55
|
+
- Build with `tsdown`; ESLint (typescript-eslint strict), Prettier, lint-staged and commitlint; CI on Node 22/24 with lint, type-check, type tests (including every README code block), 100% test coverage, package linting (`attw`, `publint`) and a `size-limit` gate; npm publishing with provenance.
|
|
56
|
+
|
|
57
|
+
## 1.6.1 and earlier
|
|
58
|
+
|
|
59
|
+
See the [GitHub releases](https://github.com/DanielAmenou/create-request/releases).
|
package/MIGRATION.md
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# Migrating from v1 to v2
|
|
2
|
+
|
|
3
|
+
v2 is a redesign of the internals with a smaller, more correct and fully typed surface. The
|
|
4
|
+
fluent style and most method names are unchanged, so for most code bases the migration is
|
|
5
|
+
mechanical: replace the enum getters with string literals, move global configuration to an api
|
|
6
|
+
instance, and drop a few options that never did anything.
|
|
7
|
+
|
|
8
|
+
Node.js 22+ is required. The published files are `dist/index.js` (ESM),
|
|
9
|
+
`dist/index.cjs` (CommonJS) and their declaration files; deep imports of `dist/library/*` no
|
|
10
|
+
longer exist.
|
|
11
|
+
|
|
12
|
+
## Checklist
|
|
13
|
+
|
|
14
|
+
1. Replace `.withX.VALUE()` getters and enum arguments with string literals.
|
|
15
|
+
2. Replace `create.config.*` with an api instance you export from one module.
|
|
16
|
+
3. Replace `withoutCsrfProtection()` / `withAntiCsrfHeaders()` / automatic XSRF handling with
|
|
17
|
+
`withCsrf()` where you need it.
|
|
18
|
+
4. Rename `error.getJson()` to `error.data`, and check `error.code` instead of message prefixes.
|
|
19
|
+
5. Remove `| null` handling around `getJson()` unless the endpoint really answers `204`.
|
|
20
|
+
6. Review retries: they now retry only retriable failures, with backoff, and honour `Retry-After`.
|
|
21
|
+
7. Check `withQueryParams` calls that relied on repeated keys — pass an array instead.
|
|
22
|
+
8. Cookie values are sent verbatim; `CookieOptions` objects are gone.
|
|
23
|
+
|
|
24
|
+
## Method by method
|
|
25
|
+
|
|
26
|
+
| v1 | v2 |
|
|
27
|
+
| ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
28
|
+
| `.withCache.NO_CACHE()`, `.withCredentials.INCLUDE()`, `.withMode.CORS()`, `.withRedirect.FOLLOW()`, `.withReferrerPolicy.NO_REFERRER()`, `.withPriority.HIGH()` | `.withCache("no-cache")`, `.withCredentials("include")`, `.withMode("cors")`, `.withRedirect("follow")`, `.withReferrerPolicy("no-referrer")`, `.withPriority("high")` — the DOM string unions, autocompleted |
|
|
29
|
+
| `withCache(CacheMode.NO_CACHE)` and the other enum arguments | the string literal |
|
|
30
|
+
| `import { CacheMode, CredentialsPolicy, RequestMode, RedirectMode, ReferrerPolicy, RequestPriority, SameSitePolicy, HttpMethod }` | removed — use string literals; the `Method` type replaces `HttpMethod` |
|
|
31
|
+
| `withKeepAlive(true)` | `withKeepAlive()` (`true` is the default; `withKeepAlive(false)` still works) |
|
|
32
|
+
| `withTimeout(ms)` (headers only; `0` threw) | covers the whole exchange including the body read; `0` / `Infinity` remove the timeout |
|
|
33
|
+
| `withResponseInterceptor(response => …)`, `withErrorInterceptor(error => …)` | unchanged, and both also receive the `HttpRequest` as a second argument (`(error, request) => request.clone()…`) |
|
|
34
|
+
| `withQueryParams({ page: 2 })` called twice with the same key → `page=1&page=2` | the last call wins (`page=2`); pass an array for repeated keys |
|
|
35
|
+
| `withQueryParam("ids", ["1", "2"])` (strings only) | numbers, booleans, `Date`s and arrays are accepted too (`withQueryParams` also takes a `URLSearchParams`); `null` removes a key |
|
|
36
|
+
| `withHeaders({ a: undefined })` (type error, ignored at runtime) | allowed: `null` / `undefined` remove the header (useful to drop an api default) |
|
|
37
|
+
| `withCookie("t", { value: "x", secure: true })`, `CookieOptions`, `SameSitePolicy` | `withCookie("t", "x")` — values are sent verbatim, options were never sent anyway |
|
|
38
|
+
| `withAbortController(controller)` | unchanged; `withSignal(signal)` added (call several times to combine signals) |
|
|
39
|
+
| `withoutCsrfProtection()` | removed — nothing is automatic any more |
|
|
40
|
+
| `withAntiCsrfHeaders()` | `withHeader("X-Requested-With", "XMLHttpRequest")` if a framework still checks it |
|
|
41
|
+
| `withCsrfToken(token, header?)` | unchanged |
|
|
42
|
+
| `withBody(...)` on `DeleteRequest` | now allowed (`DELETE` may carry a body); still a compile error on `GET`, `HEAD`, `OPTIONS` |
|
|
43
|
+
| `withGraphQL(query, variables, { throwOnError })` | unchanged; GraphQL errors now have `code: "GRAPHQL"` |
|
|
44
|
+
| `withRetries({ attempts, delay })` | unchanged shape, plus `statuses`, `methods`, `maxDelay`, `shouldRetry`, `onRetry` |
|
|
45
|
+
| `onRetry(({ attempt, error }) => …)` | unchanged; the callback also receives `delay` |
|
|
46
|
+
| `getJson<T>(): Promise<T \| null>` | `getJson<T>(): Promise<T>` — write `getJson<T \| null>()` where an empty body is possible |
|
|
47
|
+
| `getData<T, R>(d => d!.x)` | `getData<T, R>(d => d.x)` — or `api.get<T>(url).getData(d => d.x)`; the selector receives `T`, not `T \| null` |
|
|
48
|
+
| `getResponse()` throwing on every non-2xx | unchanged; `getResult()` added as the path that resolves with the error; `redirect: "manual"` 3xx and opaque responses no longer throw |
|
|
49
|
+
| `getBlob()` after `getText()` → "Body used" | works: every reader shares one buffer |
|
|
50
|
+
| — | added: `getFormData()`, `getResult()`, `clone()`, `withSignal()`, `withCsrf()`, `create.delete()`, `getJson(schema)`, `isRequestError()` |
|
|
51
|
+
|
|
52
|
+
## Global configuration → api instances
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
// v1
|
|
56
|
+
create.config.addRequestInterceptor(addTraceId);
|
|
57
|
+
create.config.setCsrfToken(token);
|
|
58
|
+
create.config.setEnableAntiCsrf(false);
|
|
59
|
+
const users = await create.get("https://api.example.com/users").getJson();
|
|
60
|
+
|
|
61
|
+
// v2 — src/lib/api.ts
|
|
62
|
+
export const api = createApi().withBaseURL("https://api.example.com").withRequestInterceptor(addTraceId).withCsrf({ token }); // or withCsrf() for the XSRF-TOKEN cookie, or nothing at all
|
|
63
|
+
const users = await api.get("/users").getJson();
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
| v1 `create.config.*` | v2 |
|
|
67
|
+
| ------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
|
|
68
|
+
| `addRequestInterceptor`, `addResponseInterceptor`, `addErrorInterceptor` | `api.withRequestInterceptor(fn)`, … — interceptors are part of the instance |
|
|
69
|
+
| `removeRequestInterceptor(id)`, `clearInterceptors()` | create another instance without them (`const anonymous = createApi().withBaseURL(base)`) |
|
|
70
|
+
| `setCsrfToken(token)`, `setCsrfHeaderName(name)` | `api.withCsrf({ token, header })` |
|
|
71
|
+
| `setXsrfCookieName(name)`, `setXsrfHeaderName(name)`, `setEnableAutoXsrf(bool)` | `api.withCsrf({ cookie, header })` — off unless you call it |
|
|
72
|
+
| `setEnableAntiCsrf(bool)` | `api.withHeader("X-Requested-With", "XMLHttpRequest")` — off unless you add it |
|
|
73
|
+
| `reset()` | not needed: there is no global state to reset between tests |
|
|
74
|
+
|
|
75
|
+
There is no ambient configuration in v2 on purpose: state that leaked across tests, tenants and
|
|
76
|
+
SSR requests was the source of several v1 bugs. Code you do not control that calls `create.get`
|
|
77
|
+
directly is unaffected by your api instance — hand it the instance instead.
|
|
78
|
+
|
|
79
|
+
## Errors
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
// v1
|
|
83
|
+
if (error instanceof RequestError) {
|
|
84
|
+
if (error.isTimeout) retryLater();
|
|
85
|
+
else if (error.message.startsWith("HTTP")) console.log(error.status, error.getJson());
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// v2
|
|
89
|
+
if (isRequestError(error)) {
|
|
90
|
+
switch (error.code) {
|
|
91
|
+
case "TIMEOUT":
|
|
92
|
+
retryLater();
|
|
93
|
+
break;
|
|
94
|
+
case "HTTP":
|
|
95
|
+
console.log(error.status, error.data);
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
| v1 | v2 |
|
|
102
|
+
| ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
103
|
+
| `error.getJson()` | `error.data` (a typed getter; never throws) |
|
|
104
|
+
| `error.isTimeout`, `error.isAborted` | unchanged (getters over `code`) |
|
|
105
|
+
| message prefixes `"HTTP 404"`, `"Timeout:5000"`, `"Net:…"`, `"ReqI: …"` | `error.code` plus readable messages (`HTTP 404 Not Found`, `Request timed out after 5000ms`, `Network error: fetch failed (getaddrinfo ENOTFOUND api.example)`, `Request interceptor failed: …`) |
|
|
106
|
+
| `new RequestError(message, url, method, options)` | `new RequestError(message, { code, url, method, ...options })` |
|
|
107
|
+
| `RequestError.timeout()`, `.fromResponse()`, `.networkError()`, `.abortError()`, `.captureBody()` | removed (they were implementation details) |
|
|
108
|
+
| `error.name === "AbortError"` (never true in v1 either) | `error.code === "ABORTED"` |
|
|
109
|
+
| a `URIError` or `TypeError` escaping `getResponse()` | cannot happen: every rejection is a `RequestError` (even a broken `withFetch` stub, a throwing schema or a hostile GraphQL body is wrapped) |
|
|
110
|
+
|
|
111
|
+
## Interceptors
|
|
112
|
+
|
|
113
|
+
- Request interceptors may return nothing: in-place changes to `config` are kept. In v1 that
|
|
114
|
+
crashed the request.
|
|
115
|
+
- `config.headers` is a copy with lower-case keys; mutating it no longer changes the request
|
|
116
|
+
object. `config.body` is already serialised (a string for JSON bodies).
|
|
117
|
+
- Api-level interceptors run before request-level ones, all in registration order (v1 ran global
|
|
118
|
+
response/error interceptors in reverse).
|
|
119
|
+
- Error interceptors run once per request, after retries (v1 ran them on every attempt).
|
|
120
|
+
- A `Response` returned by a request interceptor goes through the status check and the response
|
|
121
|
+
interceptors like a fetched response (`withTimeout()` does not apply to it — nothing was fetched).
|
|
122
|
+
|
|
123
|
+
## Retries
|
|
124
|
+
|
|
125
|
+
- Only network errors, timeouts and 408/425/429/500/502/503/504 are retried by default. v1
|
|
126
|
+
retried everything, including 404s, invalid URLs and aborted requests.
|
|
127
|
+
- The default delay is exponential backoff with jitter (v1: none). `Retry-After` is honoured
|
|
128
|
+
unless you set `delay`; one longer than `maxDelay` (30 s) cancels the retry.
|
|
129
|
+
- Requests with a `ReadableStream` body are never retried.
|
|
130
|
+
|
|
131
|
+
## Types
|
|
132
|
+
|
|
133
|
+
| v1 | v2 |
|
|
134
|
+
| -------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
135
|
+
| `BaseRequest`, `BodyRequest` (classes) | `HttpRequest<M, T>` class; `BaseRequest<T>` / `BodyRequest<T>` kept as type aliases |
|
|
136
|
+
| `GetRequest`, `PostRequest`, … (classes) | type aliases of `HttpRequest<"GET", T>`, `HttpRequest<"POST", T>`, …; `new GetRequest(url)` → `createGet(url)` |
|
|
137
|
+
| `ApiBuilder` (hand-written) | `ApiBuilder`, derived from `HttpRequest` |
|
|
138
|
+
| `RequestOptions`, `RetryCallback`, `RetryDelayFunction`, `CookieOptions`, `CookiesRecord` (with options) | removed / `RetryConfig["onRetry"]`, `RetryConfig["delay"]`, `CookiesRecord` is `Record<string, string>` |
|
|
139
|
+
| `RequestConfig.method: string` | `Method` |
|
|
140
|
+
| `withQueryParams(record)`, `withHeaders(record)` requiring an index signature | interface-typed objects are accepted |
|
|
141
|
+
| `FetchFunction = (input: string \| URL \| Request, init?) => …` | `(url: string, init: RequestInit) => Promise<Response>` — every v1 implementation still fits |
|
|
142
|
+
| `CookieUtils` | removed (internal) |
|
|
143
|
+
| — | added: `Method`, `BodyMethod`, `RequestErrorCode`, `RetryContext`, `QueryValue`, `QueryParams`, `HeadersRecord`, `CsrfOptions`, `CookiesRecord`, `RequestResult`, `RequestErrorOptions`, `StandardSchemaV1` |
|
|
144
|
+
|
|
145
|
+
## Behaviour changes you might notice
|
|
146
|
+
|
|
147
|
+
- Header names are stored lower-case; `withHeader("Content-Type", …)` and
|
|
148
|
+
`withHeader("content-type", …)` are the same header.
|
|
149
|
+
- Query strings are inserted before a `#fragment`.
|
|
150
|
+
- `./users` joins onto a base URL cleanly (`https://e.com/users`).
|
|
151
|
+
- Basic auth encodes UTF-8 credentials correctly.
|
|
152
|
+
- `withTimeout` starts counting after request interceptors ran, and applies per attempt.
|
|
153
|
+
- `withMode("no-cors")` and `withRedirect("manual")` resolve instead of throwing.
|
|
154
|
+
- `error.body` is capped at 1 MB: a response declaring more is left unread on `error.response`, a longer
|
|
155
|
+
chunked or compressed one is cut off (`body` is `undefined`).
|
|
156
|
+
- `error.response`'s body has been read when `error.body` is set; use `error.body` / `error.data`.
|
|
157
|
+
- A `FormData` body removes any `Content-Type` header (including an api-level default).
|
|
158
|
+
- A request whose signal is already aborted fails before interceptors run and before `fetch` is called.
|