@npy/fetch 0.1.3 → 0.1.4

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 (141) hide show
  1. package/LICENSE +21 -0
  2. package/_internal/consts.cjs +4 -0
  3. package/_internal/consts.d.cts +3 -0
  4. package/_internal/consts.d.ts +3 -0
  5. package/_internal/consts.js +4 -0
  6. package/_internal/decode-stream-error.cjs +18 -0
  7. package/{src/_internal/decode-stream-error.ts → _internal/decode-stream-error.d.cts} +2 -7
  8. package/_internal/decode-stream-error.d.ts +11 -0
  9. package/_internal/decode-stream-error.js +18 -0
  10. package/_internal/error-mapping.cjs +44 -0
  11. package/_internal/error-mapping.d.cts +15 -0
  12. package/_internal/error-mapping.d.ts +15 -0
  13. package/_internal/error-mapping.js +41 -0
  14. package/_internal/guards.cjs +23 -0
  15. package/_internal/guards.d.cts +15 -0
  16. package/_internal/guards.d.ts +15 -0
  17. package/_internal/guards.js +15 -0
  18. package/_internal/net.cjs +95 -0
  19. package/_internal/net.d.cts +11 -0
  20. package/_internal/net.d.ts +11 -0
  21. package/_internal/net.js +92 -0
  22. package/_internal/promises.cjs +18 -0
  23. package/_internal/promises.d.cts +1 -0
  24. package/_internal/promises.d.ts +1 -0
  25. package/_internal/promises.js +18 -0
  26. package/_internal/streams.cjs +37 -0
  27. package/_internal/streams.d.cts +21 -0
  28. package/_internal/streams.d.ts +21 -0
  29. package/_internal/streams.js +36 -0
  30. package/_internal/symbols.cjs +4 -0
  31. package/_internal/symbols.d.cts +1 -0
  32. package/_internal/symbols.d.ts +1 -0
  33. package/_internal/symbols.js +4 -0
  34. package/_virtual/_rolldown/runtime.cjs +23 -0
  35. package/agent-pool.cjs +96 -0
  36. package/agent-pool.d.cts +2 -0
  37. package/agent-pool.d.ts +2 -0
  38. package/agent-pool.js +95 -0
  39. package/agent.cjs +260 -0
  40. package/agent.d.cts +3 -0
  41. package/agent.d.ts +3 -0
  42. package/agent.js +259 -0
  43. package/body.cjs +105 -0
  44. package/body.d.cts +12 -0
  45. package/body.d.ts +12 -0
  46. package/body.js +102 -0
  47. package/dialers/index.d.cts +3 -0
  48. package/dialers/index.d.ts +3 -0
  49. package/dialers/proxy.cjs +56 -0
  50. package/dialers/proxy.d.cts +27 -0
  51. package/dialers/proxy.d.ts +27 -0
  52. package/dialers/proxy.js +55 -0
  53. package/dialers/tcp.cjs +92 -0
  54. package/dialers/tcp.d.cts +57 -0
  55. package/dialers/tcp.d.ts +57 -0
  56. package/dialers/tcp.js +89 -0
  57. package/encoding.cjs +114 -0
  58. package/encoding.d.cts +35 -0
  59. package/encoding.d.ts +35 -0
  60. package/encoding.js +110 -0
  61. package/errors.cjs +275 -0
  62. package/errors.d.cts +110 -0
  63. package/errors.d.ts +110 -0
  64. package/errors.js +259 -0
  65. package/fetch.cjs +353 -0
  66. package/fetch.d.cts +58 -0
  67. package/fetch.d.ts +58 -0
  68. package/fetch.js +350 -0
  69. package/http-client.cjs +75 -0
  70. package/http-client.d.cts +39 -0
  71. package/http-client.d.ts +39 -0
  72. package/http-client.js +75 -0
  73. package/index.cjs +49 -0
  74. package/index.d.cts +14 -0
  75. package/index.d.ts +14 -0
  76. package/index.js +11 -0
  77. package/io/_utils.cjs +56 -0
  78. package/io/_utils.d.cts +10 -0
  79. package/io/_utils.d.ts +10 -0
  80. package/io/_utils.js +51 -0
  81. package/io/buf-writer.cjs +149 -0
  82. package/io/buf-writer.d.cts +13 -0
  83. package/io/buf-writer.d.ts +13 -0
  84. package/io/buf-writer.js +148 -0
  85. package/io/io.cjs +199 -0
  86. package/io/io.d.cts +5 -0
  87. package/io/io.d.ts +5 -0
  88. package/io/io.js +198 -0
  89. package/io/readers.cjs +337 -0
  90. package/io/readers.d.cts +69 -0
  91. package/io/readers.d.ts +69 -0
  92. package/io/readers.js +333 -0
  93. package/io/writers.cjs +196 -0
  94. package/io/writers.d.cts +22 -0
  95. package/io/writers.d.ts +22 -0
  96. package/io/writers.js +195 -0
  97. package/package.json +30 -25
  98. package/{src/types/agent.ts → types/agent.d.cts} +21 -47
  99. package/types/agent.d.ts +72 -0
  100. package/{src/types/dialer.ts → types/dialer.d.cts} +9 -19
  101. package/types/dialer.d.ts +30 -0
  102. package/types/index.d.cts +2 -0
  103. package/types/index.d.ts +2 -0
  104. package/bun.lock +0 -68
  105. package/examples/custom-proxy-client.ts +0 -32
  106. package/examples/http-client.ts +0 -47
  107. package/examples/proxy.ts +0 -16
  108. package/examples/simple.ts +0 -15
  109. package/src/_internal/consts.ts +0 -3
  110. package/src/_internal/error-mapping.ts +0 -160
  111. package/src/_internal/guards.ts +0 -78
  112. package/src/_internal/net.ts +0 -173
  113. package/src/_internal/promises.ts +0 -22
  114. package/src/_internal/streams.ts +0 -52
  115. package/src/_internal/symbols.ts +0 -1
  116. package/src/agent-pool.ts +0 -157
  117. package/src/agent.ts +0 -408
  118. package/src/body.ts +0 -179
  119. package/src/dialers/index.ts +0 -3
  120. package/src/dialers/proxy.ts +0 -102
  121. package/src/dialers/tcp.ts +0 -162
  122. package/src/encoding.ts +0 -222
  123. package/src/errors.ts +0 -357
  124. package/src/fetch.ts +0 -626
  125. package/src/http-client.ts +0 -111
  126. package/src/index.ts +0 -14
  127. package/src/io/_utils.ts +0 -82
  128. package/src/io/buf-writer.ts +0 -183
  129. package/src/io/io.ts +0 -322
  130. package/src/io/readers.ts +0 -576
  131. package/src/io/writers.ts +0 -331
  132. package/src/types/index.ts +0 -2
  133. package/tests/agent-pool.test.ts +0 -111
  134. package/tests/agent.test.ts +0 -134
  135. package/tests/body.test.ts +0 -228
  136. package/tests/errors.test.ts +0 -152
  137. package/tests/fetch.test.ts +0 -421
  138. package/tests/io-options.test.ts +0 -127
  139. package/tests/multipart.test.ts +0 -348
  140. package/tests/test-utils.ts +0 -335
  141. package/tsconfig.json +0 -15
package/src/fetch.ts DELETED
@@ -1,626 +0,0 @@
1
- import {
2
- type ProxyInfo,
3
- parse as parseProxy,
4
- stringify as stringifyProxy,
5
- } from "@npy/proxy-kit";
6
- import { toWebBodyReadError, toWebFetchError } from "./_internal/error-mapping";
7
- import {
8
- isBlob,
9
- isFormData,
10
- isIterable,
11
- isMultipartFormDataStream,
12
- isReadable,
13
- isReadableStream,
14
- isURLSearchParameters,
15
- } from "./_internal/guards";
16
- import { bodyErrorMapperSymbol } from "./_internal/symbols";
17
- import { type BodyInit as FetchBodyInit, fromRequestBody } from "./body";
18
- import { AutoDialer, ProxyDialer } from "./dialers";
19
- import type { HttpClientOptions } from "./http-client";
20
- import { HttpClient } from "./http-client";
21
-
22
- const MAX_REDIRECTS = 20;
23
- const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
24
- const SENSITIVE_REDIRECT_HEADERS = [
25
- "authorization",
26
- "cookie",
27
- "proxy-authorization",
28
- ] as const;
29
- const BODY_HEADERS = [
30
- "content-encoding",
31
- "content-language",
32
- "content-length",
33
- "content-location",
34
- "content-type",
35
- "transfer-encoding",
36
- ] as const;
37
-
38
- export type FetchProxyInput = string | ProxyInfo | null;
39
-
40
- export interface RequestInit
41
- extends Omit<globalThis.RequestInit, "body" | "headers"> {
42
- body?: FetchBodyInit | null;
43
- headers?: HeadersInit;
44
- client?: HttpClient;
45
- proxy?: FetchProxyInput;
46
- }
47
-
48
- export type FetchRequestInit = RequestInit;
49
- export interface FetchOptions extends HttpClientOptions {}
50
-
51
- export interface FetchLike {
52
- (input: RequestInfo | URL, init?: RequestInit): Promise<Response>;
53
- close(): Promise<void>;
54
- [Symbol.asyncDispose](): Promise<void>;
55
- readonly client: HttpClient;
56
- }
57
-
58
- interface PreparedRequest {
59
- url: URL;
60
- method: string;
61
- headers: Headers;
62
- body: FetchBodyInit | null | undefined;
63
- signal?: AbortSignal;
64
- redirect: RequestRedirect;
65
- baseClient: HttpClient;
66
- proxy: FetchProxyInput | undefined;
67
- replayBodyRequest?: Request;
68
- }
69
-
70
- interface NormalizedProxyConfig {
71
- key: string;
72
- proxy: ProxyInfo;
73
- }
74
-
75
- function createDefaultHttpClient(options: FetchOptions = {}): HttpClient {
76
- return new HttpClient({
77
- ...options,
78
- dialer: options.dialer ?? new AutoDialer(),
79
- });
80
- }
81
-
82
- /**
83
- * Normalizes any supported header input into a {@link Headers} instance.
84
- *
85
- * @remarks
86
- * If the input is already a {@link Headers} object, the same instance is returned.
87
- * Tuple arrays and plain records are copied into a new {@link Headers}.
88
- */
89
- export function normalizeHeaders(headers?: HeadersInit): Headers {
90
- if (headers instanceof Headers) return headers;
91
-
92
- const result = new Headers();
93
- if (!headers) return result;
94
-
95
- if (Array.isArray(headers)) {
96
- for (const [key, value] of headers) result.append(key, value);
97
- return result;
98
- }
99
-
100
- for (const [key, value] of Object.entries(headers)) {
101
- if (Array.isArray(value)) {
102
- for (const entry of value) result.append(key, entry);
103
- } else if (value !== undefined) {
104
- result.append(key, value);
105
- }
106
- }
107
-
108
- return result;
109
- }
110
-
111
- function resolveUrl(input: RequestInfo | URL): URL {
112
- if (input instanceof URL) return input;
113
- return new URL(input instanceof Request ? input.url : String(input));
114
- }
115
-
116
- function resolveMethod(input: RequestInfo | URL, init: RequestInit): string {
117
- const raw =
118
- init.method ?? (input instanceof Request ? input.method : undefined);
119
- return raw?.toUpperCase().trim() ?? "GET";
120
- }
121
-
122
- function resolveHeaders(input: RequestInfo | URL, init: RequestInit): Headers {
123
- const headersSource =
124
- init.headers !== undefined
125
- ? init.headers
126
- : input instanceof Request
127
- ? input.headers
128
- : undefined;
129
- return normalizeHeaders(headersSource);
130
- }
131
-
132
- function resolveSignal(
133
- input: RequestInfo | URL,
134
- init: RequestInit,
135
- ): AbortSignal | undefined {
136
- return init.signal ?? (input instanceof Request ? input.signal : undefined);
137
- }
138
-
139
- function resolveBody(
140
- input: RequestInfo | URL,
141
- init: RequestInit,
142
- ): FetchBodyInit | null | undefined {
143
- if (init.body !== undefined) return init.body;
144
- return input instanceof Request ? fromRequestBody(input) : undefined;
145
- }
146
-
147
- function resolveRedirectMode(
148
- input: RequestInfo | URL,
149
- init: RequestInit,
150
- ): RequestRedirect {
151
- return (
152
- init.redirect ?? (input instanceof Request ? input.redirect : "follow")
153
- );
154
- }
155
-
156
- function getBodyReplaySource(
157
- input: RequestInfo | URL,
158
- init: RequestInit,
159
- method: string,
160
- body: FetchBodyInit | null | undefined,
161
- redirect: RequestRedirect,
162
- ): Request | undefined {
163
- if (
164
- redirect !== "follow" ||
165
- init.body !== undefined ||
166
- body == null ||
167
- method === "GET" ||
168
- method === "HEAD" ||
169
- !(input instanceof Request)
170
- ) {
171
- return undefined;
172
- }
173
- return input.clone();
174
- }
175
-
176
- function assertValidFetchUrl(url: URL): void {
177
- if (url.username || url.password) {
178
- throw new TypeError(
179
- "Request URL must not include embedded credentials",
180
- );
181
- }
182
- if (url.protocol !== "http:" && url.protocol !== "https:") {
183
- throw new TypeError(`fetch failed: unsupported scheme ${url.protocol}`);
184
- }
185
- }
186
-
187
- function assertValidFetchBody(
188
- method: string,
189
- body: FetchBodyInit | null | undefined,
190
- ): void {
191
- if (body == null) return;
192
- if (method === "GET" || method === "HEAD") {
193
- throw new TypeError(`Request with ${method} method cannot have a body`);
194
- }
195
- }
196
-
197
- function lookupEnvProxy(...names: string[]): string | undefined {
198
- for (const name of names) {
199
- const value = process.env[name] ?? process.env[name.toLowerCase()];
200
- const normalized = value?.trim();
201
- if (normalized) return normalized;
202
- }
203
- return undefined;
204
- }
205
-
206
- function resolveProxyFromEnv(url: URL): string | undefined {
207
- const socksProxy = lookupEnvProxy("SOCKS5_PROXY", "SOCKS_PROXY");
208
- if (socksProxy) return socksProxy;
209
- if (url.protocol === "https:")
210
- return lookupEnvProxy("HTTPS_PROXY", "HTTP_PROXY");
211
- return lookupEnvProxy("HTTP_PROXY");
212
- }
213
-
214
- function normalizeProxyConfig(
215
- proxy: Exclude<FetchProxyInput, null>,
216
- ): NormalizedProxyConfig {
217
- const parsed =
218
- typeof proxy === "string" ? parseProxy(proxy, { strict: true }) : proxy;
219
- if (parsed == null)
220
- throw new TypeError(`Invalid proxy string: ${String(proxy)}`);
221
-
222
- const key = stringifyProxy(parsed, {
223
- strict: true,
224
- format: "user:pass@ip:port",
225
- });
226
- if (!key) throw new TypeError("Failed to normalize proxy configuration");
227
-
228
- return { key, proxy: parsed };
229
- }
230
-
231
- function resolveNormalizedProxy(
232
- url: URL,
233
- proxy: FetchProxyInput | undefined,
234
- ): NormalizedProxyConfig | null {
235
- if (proxy === null) return null;
236
- if (proxy !== undefined) return normalizeProxyConfig(proxy);
237
- const envProxy = resolveProxyFromEnv(url);
238
- return envProxy ? normalizeProxyConfig(envProxy) : null;
239
- }
240
-
241
- function annotateResponse(
242
- response: Response,
243
- url: string,
244
- redirected: boolean,
245
- ): Response {
246
- try {
247
- if (redirected) {
248
- Object.defineProperties(response, {
249
- redirected: {
250
- configurable: true,
251
- enumerable: true,
252
- value: true,
253
- writable: false,
254
- },
255
- url: {
256
- configurable: true,
257
- enumerable: true,
258
- value: url,
259
- writable: false,
260
- },
261
- });
262
- } else {
263
- Object.defineProperty(response, "url", {
264
- configurable: true,
265
- enumerable: true,
266
- value: url,
267
- writable: false,
268
- });
269
- }
270
- } catch {}
271
- return response;
272
- }
273
-
274
- async function discardResponse(response: Response): Promise<void> {
275
- try {
276
- await response.body?.cancel();
277
- } catch {}
278
- }
279
-
280
- function isRedirectResponse(response: Response): boolean {
281
- return (
282
- REDIRECT_STATUSES.has(response.status) &&
283
- response.headers.get("location") != null
284
- );
285
- }
286
-
287
- function isReplayableBody(body: FetchBodyInit | null | undefined): boolean {
288
- if (
289
- body == null ||
290
- typeof body === "string" ||
291
- body instanceof Uint8Array ||
292
- isBlob(body) ||
293
- isFormData(body) ||
294
- isURLSearchParameters(body)
295
- ) {
296
- return true;
297
- }
298
- if (
299
- isReadableStream(body) ||
300
- isReadable(body) ||
301
- isMultipartFormDataStream(body) ||
302
- isIterable(body)
303
- ) {
304
- return false;
305
- }
306
- return true;
307
- }
308
-
309
- async function resolveReplayableBody(
310
- current: PreparedRequest,
311
- ): Promise<FetchBodyInit | null | undefined> {
312
- if (isReplayableBody(current.body)) return current.body;
313
- if (current.replayBodyRequest) {
314
- const buffer = await current.replayBodyRequest.arrayBuffer();
315
- return buffer.byteLength > 0 ? new Uint8Array(buffer) : null;
316
- }
317
- throw new TypeError(
318
- "Cannot follow redirect with a non-replayable request body",
319
- );
320
- }
321
-
322
- function shouldDropBodyOnRedirect(status: number, method: string): boolean {
323
- if (status === 303) return method !== "HEAD";
324
- return (status === 301 || status === 302) && method === "POST";
325
- }
326
-
327
- function resolveRedirectMethod(status: number, method: string): string {
328
- if (
329
- (status === 303 && method !== "HEAD") ||
330
- ((status === 301 || status === 302) && method === "POST")
331
- ) {
332
- return "GET";
333
- }
334
- return method;
335
- }
336
-
337
- function createRedirectHeaders(
338
- current: PreparedRequest,
339
- nextUrl: URL,
340
- dropBody: boolean,
341
- ): Headers {
342
- const headers = new Headers(current.headers);
343
- const isCrossOrigin = current.url.origin !== nextUrl.origin;
344
- const isDowngrade =
345
- current.url.protocol === "https:" && nextUrl.protocol === "http:";
346
-
347
- headers.delete("host");
348
- if (isCrossOrigin) {
349
- for (const name of SENSITIVE_REDIRECT_HEADERS) headers.delete(name);
350
- }
351
- if (dropBody) {
352
- for (const name of BODY_HEADERS) headers.delete(name);
353
- }
354
- if (!headers.has("referer") && !isDowngrade) {
355
- headers.set("referer", current.url.toString());
356
- }
357
- return headers;
358
- }
359
-
360
- async function buildRedirectRequest(
361
- current: PreparedRequest,
362
- response: Response,
363
- ): Promise<PreparedRequest> {
364
- const location = response.headers.get("location");
365
- if (!location) return current;
366
-
367
- const nextUrl = new URL(location, current.url);
368
- const nextMethod = resolveRedirectMethod(response.status, current.method);
369
- const dropBody = shouldDropBodyOnRedirect(response.status, current.method);
370
- const nextHeaders = createRedirectHeaders(current, nextUrl, dropBody);
371
- const nextBody = dropBody
372
- ? undefined
373
- : await resolveReplayableBody(current);
374
-
375
- return {
376
- ...current,
377
- url: nextUrl,
378
- method: nextMethod,
379
- headers: nextHeaders,
380
- body: nextBody,
381
- replayBodyRequest: undefined,
382
- };
383
- }
384
-
385
- async function sendPreparedRequest(
386
- prepared: PreparedRequest,
387
- getProxyClient: (
388
- baseClient: HttpClient,
389
- proxy: NormalizedProxyConfig,
390
- ) => HttpClient,
391
- ): Promise<Response> {
392
- assertValidFetchUrl(prepared.url);
393
- assertValidFetchBody(prepared.method, prepared.body);
394
-
395
- const normalizedProxy = resolveNormalizedProxy(
396
- prepared.url,
397
- prepared.proxy,
398
- );
399
- const client = normalizedProxy
400
- ? getProxyClient(prepared.baseClient, normalizedProxy)
401
- : prepared.baseClient;
402
-
403
- try {
404
- const sendOptions = Object.defineProperty(
405
- {
406
- url: prepared.url,
407
- method: prepared.method,
408
- headers: prepared.headers,
409
- body: prepared.body ?? null,
410
- signal: prepared.signal,
411
- },
412
- bodyErrorMapperSymbol,
413
- {
414
- configurable: false,
415
- enumerable: false,
416
- writable: false,
417
- value: (error: unknown) =>
418
- toWebBodyReadError(error, prepared.signal),
419
- },
420
- );
421
-
422
- return await client.send(sendOptions);
423
- } catch (error) {
424
- throw toWebFetchError(error, prepared.signal);
425
- }
426
- }
427
-
428
- function prepareRequest(
429
- input: RequestInfo | URL,
430
- init: RequestInit,
431
- defaultHttpClient: HttpClient,
432
- ): PreparedRequest {
433
- const url = resolveUrl(input);
434
- const method = resolveMethod(input, init);
435
- const headers = resolveHeaders(input, init);
436
- const body = resolveBody(input, init);
437
- const signal = resolveSignal(input, init);
438
- const redirect = resolveRedirectMode(input, init);
439
-
440
- return {
441
- url,
442
- method,
443
- headers,
444
- body,
445
- signal,
446
- redirect,
447
- baseClient: init.client ?? defaultHttpClient,
448
- proxy: init.proxy,
449
- replayBodyRequest: getBodyReplaySource(
450
- input,
451
- init,
452
- method,
453
- body,
454
- redirect,
455
- ),
456
- };
457
- }
458
-
459
- /**
460
- * Creates a fetch-compatible client backed by {@link HttpClient}.
461
- *
462
- * @remarks
463
- * The returned function follows the standard fetch shape, but uses this library's
464
- * connection pooling, proxy support and body/error mapping rules.
465
- *
466
- * When no client is provided, an internal {@link HttpClient} is created and owned
467
- * by the returned fetch-like function. Calling {@link FetchLike.close} closes that
468
- * internal client and any proxy-specific clients created on demand.
469
- *
470
- * @example
471
- * ```ts
472
- * const fetchLike = createFetch();
473
- * const response = await fetchLike("https://httpbin.org/anything");
474
- * const data = await response.json();
475
- * await fetchLike.close();
476
- * ```
477
- */
478
- export function createFetch(client?: HttpClient): FetchLike {
479
- const defaultHttpClient = client ?? createDefaultHttpClient();
480
- const proxyClientsByBase = new WeakMap<
481
- HttpClient,
482
- Map<string, HttpClient>
483
- >();
484
- const ownedProxyClients = new Set<HttpClient>();
485
-
486
- let closePromise: Promise<void> | undefined;
487
-
488
- const getProxyClient = (
489
- baseClient: HttpClient,
490
- proxy: NormalizedProxyConfig,
491
- ): HttpClient => {
492
- let clients = proxyClientsByBase.get(baseClient);
493
- if (!clients) {
494
- clients = new Map();
495
- proxyClientsByBase.set(baseClient, clients);
496
- }
497
-
498
- const existing = clients.get(proxy.key);
499
- if (existing) return existing;
500
-
501
- const proxyClient = new HttpClient({
502
- ...baseClient.options,
503
- dialer: new ProxyDialer(proxy.proxy),
504
- });
505
-
506
- clients.set(proxy.key, proxyClient);
507
- ownedProxyClients.add(proxyClient);
508
- return proxyClient;
509
- };
510
-
511
- const fetchLike = (async (
512
- input: RequestInfo | URL,
513
- init: RequestInit = {},
514
- ): Promise<Response> => {
515
- let prepared = prepareRequest(input, init, defaultHttpClient);
516
- let redirected = false;
517
- let redirects = 0;
518
-
519
- for (;;) {
520
- const response = await sendPreparedRequest(
521
- prepared,
522
- getProxyClient,
523
- );
524
-
525
- if (
526
- !isRedirectResponse(response) ||
527
- prepared.redirect === "manual"
528
- ) {
529
- return annotateResponse(
530
- response,
531
- prepared.url.toString(),
532
- redirected,
533
- );
534
- }
535
-
536
- if (prepared.redirect === "error") {
537
- await discardResponse(response);
538
- throw new TypeError(
539
- `fetch failed: redirect mode is set to "error"`,
540
- );
541
- }
542
-
543
- if (redirects >= MAX_REDIRECTS) {
544
- await discardResponse(response);
545
- throw new TypeError(
546
- `fetch failed: maximum redirect count exceeded`,
547
- );
548
- }
549
-
550
- prepared = await buildRedirectRequest(prepared, response);
551
- await discardResponse(response);
552
- redirected = true;
553
- redirects += 1;
554
- }
555
- }) as FetchLike;
556
-
557
- const close = async (): Promise<void> => {
558
- if (closePromise) return closePromise;
559
-
560
- const promise = (async () => {
561
- const proxyClients = Array.from(ownedProxyClients);
562
- ownedProxyClients.clear();
563
-
564
- const results = await Promise.allSettled([
565
- ...proxyClients.map((c) => c.close()),
566
- ...(client == null ? [defaultHttpClient.close()] : []),
567
- ]);
568
-
569
- const errors = results.flatMap((result) =>
570
- result.status === "rejected" ? [result.reason] : [],
571
- );
572
-
573
- if (errors.length === 1) throw errors[0];
574
- if (errors.length > 1) {
575
- throw new AggregateError(
576
- errors,
577
- "Failed to close one or more fetch clients",
578
- );
579
- }
580
- })();
581
-
582
- closePromise = promise;
583
-
584
- try {
585
- await promise;
586
- } finally {
587
- if (closePromise === promise) closePromise = undefined;
588
- }
589
- };
590
-
591
- Object.defineProperties(fetchLike, {
592
- client: {
593
- configurable: false,
594
- enumerable: false,
595
- value: defaultHttpClient,
596
- writable: false,
597
- },
598
- close: {
599
- configurable: false,
600
- enumerable: false,
601
- value: close,
602
- writable: false,
603
- },
604
- [Symbol.asyncDispose]: {
605
- configurable: false,
606
- enumerable: false,
607
- value: close,
608
- writable: false,
609
- },
610
- });
611
-
612
- return fetchLike;
613
- }
614
-
615
- export type { HttpClientOptions, ProxyInfo };
616
- export { HttpClient };
617
-
618
- /**
619
- * Default fetch-compatible client created with {@link createFetch}.
620
- *
621
- * @remarks
622
- * This singleton owns its internal {@link HttpClient}. Call {@link FetchLike.close}
623
- * when you want to release pooled connections explicitly.
624
- */
625
- export const fetch: FetchLike = createFetch();
626
- export default fetch;