Skip to content

Commit adaa330

Browse files
authored
feat: Add abortable request support, getHeader, hasHeader (#26)
1 parent 95e7422 commit adaa330

File tree

8 files changed

+583
-84
lines changed

8 files changed

+583
-84
lines changed

README.md

Lines changed: 146 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ This package:
88
+ Uses the modern, standardized `fetch` function.
99
+ Does **not** throw on non-OK HTTP responses.
1010
+ **Can fully type all possible HTTP responses depending on the HTTP status code, even non-standard ones like 499.**
11+
+ **Supports abortable HTTP requests; no boilerplate.**
1112
+ Works in any runtime that implements `fetch()` (browsers, NodeJS, etc.).
1213
+ Probably the tiniest fetch wrapper you'll ever need.
1314

@@ -105,7 +106,7 @@ Now the fetcher object is ready for use.
105106
This is the fun part where we can enumerate the various shapes of the body depending on the HTTP status code:
106107

107108
```typescript
108-
import type { MyData } from "./my-datatypes.js";
109+
import type { MyData } from "./my-types.js";
109110
import fetcher from "./fetcher.js";
110111

111112
const response = await fetcher
@@ -117,6 +118,7 @@ const response = await fetcher
117118

118119
The object stored in the `response` variable will contain the following properties:
119120

121+
+ `aborted`: Will be `false` (since **v0.8.0**)
120122
+ `ok`: Same as `Response.ok`.
121123
+ `status`: Same as `Response.status`.
122124
+ `statusText`: Same as `Response.statusText`.
@@ -154,6 +156,69 @@ export default new DrFetch<MyStatusCode>();
154156

155157
You will now be able to use non-standardized status code `499` to type the response body with `DrFetch.for<>()`.
156158

159+
## Abortable HTTP Requests
160+
161+
> Since **v0.8.0**
162+
163+
To create abortable HTTP requests, as per the standard, use an `AbortController`. The following is how you would have
164+
to write your code *without* `dr-fetch`:
165+
166+
```typescript
167+
const ac = new AbortController();
168+
let aborted = false;
169+
let response: Response;
170+
171+
try {
172+
response = await fetch('/url', { signal: ac.signal });
173+
}
174+
catch (err) {
175+
if (err instanceof DOMException && err.name === 'AbortError') {
176+
aborted = true;
177+
}
178+
// Other stuff for non-aborted scenarios.
179+
}
180+
if (!aborted) {
181+
const body = await response.json();
182+
...
183+
}
184+
```
185+
186+
In contrast, using an abortable fetcher from `dr-fetch`, you reduce your code to:
187+
188+
```typescript
189+
// abortable-fetcher.ts
190+
import { DrFetch } from "dr-fetch";
191+
192+
export const abortableFetcher = new DrFetch()
193+
.abortable();
194+
```
195+
196+
```typescript
197+
// some-component.ts
198+
import { abortableFetcher } from "./abortable-fetcher.js";
199+
200+
const ac = new AbortController();
201+
202+
const response = await abortableFetcher
203+
.for<200, MyData[]>(),
204+
.for<400, ValidationError[]>()
205+
.get('/url', { signal: ac.signal });
206+
if (!response.aborted) {
207+
...
208+
}
209+
```
210+
211+
In short: All boilerplate is gone. Your only job is to create the abort controller, pass the signal and after
212+
awaiting for the response, you check the value of the `aborted` property.
213+
214+
TypeScript and Intellisense will be fully accurate: If `response.aborted` is true, then the `response.error` property
215+
is available; otherwise the usual `ok`, `status`, `statusText` and `body` properties will be the ones available.
216+
217+
For full details and feedback on this feature, see [this discussion](https://github.com/WJSoftware/dr-fetch/discussions/25).
218+
219+
> [!IMPORTANT]
220+
> Calling `DrFetch.abortable()` permanently changes the fetcher object's configuration.
221+
157222
## Smarter Uses
158223

159224
It is smart to create just one fetcher, configure it, then use it for every fetch call. Because generally speaking,
@@ -164,7 +229,7 @@ if your API is standardized so all status `400` bodies look the same? Then conf
164229
// root-fetcher.ts
165230
import { DrFetch } from "dr-fetch";
166231
import { myFetch } from "./my-fetch.js";
167-
import type { BadRequestBody } from "my-datatypes.js";
232+
import type { BadRequestBody } from "my-types.js";
168233

169234
export default new DrFetch(myFetch)
170235
.withProcessor(...) // Optional processors
@@ -175,14 +240,39 @@ export default new DrFetch(myFetch)
175240

176241
You can now consume this root fetcher object and it will be pre-typed for the `400` status code.
177242

243+
### About Abortable Fetchers
244+
245+
> Since **v0.8.0**
246+
247+
If your project has a need for abortable and non-abortable fetcher objects, the smarter option would be to create and
248+
export 2 fetcher objects, instead of one root fetcher:
249+
250+
```typescript
251+
// root-fetchers.ts
252+
import { DrFetch } from "dr-fetch";
253+
import { myFetch } from "./my-fetch.js";
254+
import type { BadRequestBody } from "my-types.js";
255+
256+
export const rootFetcher new DrFetch(myFetch)
257+
.withProcessor(...) // Optional processors
258+
.withProcessor(...)
259+
.for<400, BadRequestBody>()
260+
;
261+
262+
export const abortableRootFetcher = rootFetcher.clone().abortable();
263+
```
264+
265+
We clone it because `abortable()` has permanent side effects on the object's state. Cloning also helps with other
266+
scenarios, as explained next.
267+
178268
### Specializing the Root Fetcher
179269

180-
Ok, nice, but what if we needed a custom processor for just one particular URL? It makes no sense to add it to the
181-
root fetcher, and maybe it is even harmful to do so. In that case, clone the fetcher.
270+
What if we needed a custom processor for just one particular URL? It makes no sense to add it to the root fetcher, and
271+
maybe it is even harmful to do so. In cases like this one, clone the fetcher.
182272

183-
Cloning a fetcher produces a new fetcher with the same data-fetching function, the same body processors and the same
184-
body typings, **unless** we specify we want something different, like not cloning the body types, or specifying a new
185-
data-fetching function.
273+
Cloning a fetcher produces a new fetcher with the same data-fetching function, the same body processors, the same
274+
support for abortable HTTP requests and the same body typings, **unless** we specify we want something different, like
275+
not cloning the body types, or specifying a new data-fetching function.
186276

187277
```typescript
188278
import rootFetcher from "./root-fetcher.js";
@@ -192,31 +282,38 @@ function specialFetch(url: FetchFnUrl, init?: FetchFnInit) {
192282
...
193283
}
194284

195-
// Same data-fetching function, body processors and body typing.
196-
const specialFetcher = rootFetcher.clone(true);
197-
// Same data-fetching function and body processors. No body typing.
198-
const specialFetcher = rootFetcher.clone(false);
199-
// Different data-fetching function.
200-
const specialFetcher = rootFetcher.clone(true, { fetchFn: specialFetch });
201-
// No custom body processors.
202-
const specialFetcher = rootFetcher.clone(true, { includeProcessors: false });
203-
// Identical processors and body typing, stock fetch().
204-
const specialFetcher = rootFetcher.clone(true, { fetchFn: false });
285+
// Same data-fetching function, body processors, abortable support and body typing.
286+
const specialFetcher = rootFetcher.clone();
287+
// Same data-fetching function, abortable support and body processors; no body typing.
288+
const specialFetcher = rootFetcher.clone({ preserveTyping: false });
289+
// Same everything; different data-fetching function.
290+
const specialFetcher = rootFetcher.clone({ fetchFn: specialFetch });
291+
// Same everything; no custom body processors.
292+
const specialFetcher = rootFetcher.clone({ includeProcessors: false });
293+
// Identical processors, abortable support and body typing; stock fetch().
294+
const specialFetcher = rootFetcher.clone({ fetchFn: false });
295+
// Identical processors, body typing and fetch function; no abortable support (the default when constructing).
296+
const specialFetcher = rootFetcher.clone({ preserveAbortable: false });
205297
```
206298

207299
> [!IMPORTANT]
208-
> The first parameter to the `clone` function cannot be a variable. It is just used as a TypeScript trick to reset the
209-
> body typing. The value itself means nothing in runtime because types are not a runtime thing.
300+
> `preserveTyping` is a TypeScript trick and cannot be a variable of type `boolean`. Its value doesn't matter in
301+
> runtime because types are not a runtime thing, and TypeScript depends on knowing if the value is `true` or `false`.
302+
>
303+
> On the other hand, `preserveAbortable` is a hybrid: It uses the same TypeScript trick, but its value does matter in
304+
> runtime because an abortable fetcher object has different inner state than a stock fetcher object. In this sense,
305+
> supporting a variable would be ideal, but there's just no way to properly reconcile the TypeScript side with a
306+
> variable of type `boolean`. Therefore, try to always use constant values.
210307
211308
## Shortcut Functions
212309

213310
> Since **v0.3.0**
214311
215312
`DrFetch` objects now provide the shortcut functions `get`, `head`, `post`, `patch`, `put` and `delete`. Except for
216-
`get` and `head`, all these accept a body parameter. When this body is a POJO or an array, the body is stringified and
217-
the `Content-Type` header is given the value `application/json`. If a body of any other type is given (that the
218-
`fetch()` function accepts, such as `FormData`), no headers are explicitly specified and therefore it is up to what
219-
`fetch()` (or the custom data-fetching function you provide) does in these cases.
313+
`get` and `head`, all these accept a body parameter. When this body is a POJO or an array, the body is stringified
314+
and, if no explicit `Content-Type` header is set, the `Content-Type` header is given the value `application/json`. If
315+
a body of any other type is given (that the `fetch()` function accepts, such as `FormData`), no headers are explicitly
316+
added and therefore it is up to what `fetch()` (or the custom data-fetching function you provide) does in these cases.
220317

221318
```typescript
222319
import type { Todo } from "./myTypes.js";
@@ -237,6 +334,20 @@ const response = await fetcher
237334
As stated, your custom fetch can be used to further customize the request because these shortcut functions will, in the
238335
end, call it.
239336

337+
### Parameters
338+
339+
> Since **v0.8.0**
340+
341+
The `get` and `head` shortcut functions' parameters are:
342+
343+
`(url: URL | string, init?: RequestInit)`
344+
345+
The other shortcut functions' parameters are:
346+
347+
`(url: URL | string, body?: BodyInit | null | Record<string, any>, init?: RequestInit)`
348+
349+
Just note that `init` won't accept the `method` or `body` properties (the above is a simplification).
350+
240351
## setHeaders and makeIterableHeaders
241352

242353
> Since **v0.4.0**
@@ -347,6 +458,18 @@ for (let [key, value] of makeIterableHeaders(myHeaders)) { ... }
347458
expect([...makeIterableHeaders(myHeaders)].length).to.equal(2);
348459
```
349460

461+
## hasHeader and getHeader
462+
463+
These are two helper functions that do exactly what the names imply: `hasHeader` checks for the existence of a
464+
particular HTTP header; `getHeader` obtains the value of a particular HTTP header.
465+
466+
These functions perform a sequential search with the help of `makeIterableHeaders`.
467+
468+
> [!NOTE]
469+
> Try not to use `getHeader` to determine the existence of a header **without** having the following in mind: The
470+
> function returns `undefined` if the value is not found, but it could return `undefined` if the header is found *and*
471+
> its value is `undefined`.
472+
350473
## Usage Without TypeScript (JavaScript Projects)
351474

352475
Why are you a weird fellow/gal? Anyway, prejudice aside, body typing will mean nothing to you, so forget about `for()`

0 commit comments

Comments
 (0)