@npy/fetch 0.1.2 → 0.1.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 (142) hide show
  1. package/README.md +143 -50
  2. package/bun.lock +68 -0
  3. package/examples/custom-proxy-client.ts +32 -0
  4. package/examples/http-client.ts +47 -0
  5. package/examples/proxy.ts +16 -0
  6. package/examples/simple.ts +15 -0
  7. package/package.json +25 -30
  8. package/src/_internal/consts.ts +3 -0
  9. package/{_internal/decode-stream-error.d.cts → src/_internal/decode-stream-error.ts} +7 -2
  10. package/src/_internal/error-mapping.ts +160 -0
  11. package/src/_internal/guards.ts +78 -0
  12. package/src/_internal/net.ts +173 -0
  13. package/src/_internal/promises.ts +22 -0
  14. package/src/_internal/streams.ts +52 -0
  15. package/src/_internal/symbols.ts +1 -0
  16. package/src/agent-pool.ts +157 -0
  17. package/src/agent.ts +408 -0
  18. package/src/body.ts +179 -0
  19. package/src/dialers/index.ts +3 -0
  20. package/src/dialers/proxy.ts +102 -0
  21. package/src/dialers/tcp.ts +162 -0
  22. package/src/encoding.ts +222 -0
  23. package/src/errors.ts +357 -0
  24. package/src/fetch.ts +626 -0
  25. package/src/http-client.ts +111 -0
  26. package/src/index.ts +14 -0
  27. package/src/io/_utils.ts +82 -0
  28. package/src/io/buf-writer.ts +183 -0
  29. package/src/io/io.ts +322 -0
  30. package/src/io/readers.ts +576 -0
  31. package/src/io/writers.ts +331 -0
  32. package/{types/agent.d.cts → src/types/agent.ts} +47 -21
  33. package/{types/dialer.d.cts → src/types/dialer.ts} +19 -9
  34. package/src/types/index.ts +2 -0
  35. package/tests/agent-pool.test.ts +111 -0
  36. package/tests/agent.test.ts +134 -0
  37. package/tests/body.test.ts +228 -0
  38. package/tests/errors.test.ts +152 -0
  39. package/tests/fetch.test.ts +421 -0
  40. package/tests/io-options.test.ts +127 -0
  41. package/tests/multipart.test.ts +348 -0
  42. package/tests/test-utils.ts +335 -0
  43. package/tsconfig.json +15 -0
  44. package/LICENSE +0 -21
  45. package/_internal/consts.cjs +0 -4
  46. package/_internal/consts.d.cts +0 -3
  47. package/_internal/consts.d.ts +0 -3
  48. package/_internal/consts.js +0 -4
  49. package/_internal/decode-stream-error.cjs +0 -18
  50. package/_internal/decode-stream-error.d.ts +0 -11
  51. package/_internal/decode-stream-error.js +0 -18
  52. package/_internal/error-mapping.cjs +0 -44
  53. package/_internal/error-mapping.d.cts +0 -15
  54. package/_internal/error-mapping.d.ts +0 -15
  55. package/_internal/error-mapping.js +0 -41
  56. package/_internal/guards.cjs +0 -23
  57. package/_internal/guards.d.cts +0 -15
  58. package/_internal/guards.d.ts +0 -15
  59. package/_internal/guards.js +0 -15
  60. package/_internal/net.cjs +0 -95
  61. package/_internal/net.d.cts +0 -11
  62. package/_internal/net.d.ts +0 -11
  63. package/_internal/net.js +0 -92
  64. package/_internal/promises.cjs +0 -18
  65. package/_internal/promises.d.cts +0 -1
  66. package/_internal/promises.d.ts +0 -1
  67. package/_internal/promises.js +0 -18
  68. package/_internal/streams.cjs +0 -37
  69. package/_internal/streams.d.cts +0 -21
  70. package/_internal/streams.d.ts +0 -21
  71. package/_internal/streams.js +0 -36
  72. package/_internal/symbols.cjs +0 -4
  73. package/_internal/symbols.d.cts +0 -1
  74. package/_internal/symbols.d.ts +0 -1
  75. package/_internal/symbols.js +0 -4
  76. package/_virtual/_rolldown/runtime.cjs +0 -23
  77. package/agent-pool.cjs +0 -96
  78. package/agent-pool.d.cts +0 -2
  79. package/agent-pool.d.ts +0 -2
  80. package/agent-pool.js +0 -95
  81. package/agent.cjs +0 -260
  82. package/agent.d.cts +0 -3
  83. package/agent.d.ts +0 -3
  84. package/agent.js +0 -259
  85. package/body.cjs +0 -105
  86. package/body.d.cts +0 -12
  87. package/body.d.ts +0 -12
  88. package/body.js +0 -102
  89. package/dialers/index.d.cts +0 -3
  90. package/dialers/index.d.ts +0 -3
  91. package/dialers/proxy.cjs +0 -56
  92. package/dialers/proxy.d.cts +0 -27
  93. package/dialers/proxy.d.ts +0 -27
  94. package/dialers/proxy.js +0 -55
  95. package/dialers/tcp.cjs +0 -92
  96. package/dialers/tcp.d.cts +0 -57
  97. package/dialers/tcp.d.ts +0 -57
  98. package/dialers/tcp.js +0 -89
  99. package/encoding.cjs +0 -114
  100. package/encoding.d.cts +0 -35
  101. package/encoding.d.ts +0 -35
  102. package/encoding.js +0 -110
  103. package/errors.cjs +0 -275
  104. package/errors.d.cts +0 -110
  105. package/errors.d.ts +0 -110
  106. package/errors.js +0 -259
  107. package/fetch.cjs +0 -353
  108. package/fetch.d.cts +0 -58
  109. package/fetch.d.ts +0 -58
  110. package/fetch.js +0 -350
  111. package/http-client.cjs +0 -75
  112. package/http-client.d.cts +0 -39
  113. package/http-client.d.ts +0 -39
  114. package/http-client.js +0 -75
  115. package/index.cjs +0 -49
  116. package/index.d.cts +0 -14
  117. package/index.d.ts +0 -14
  118. package/index.js +0 -11
  119. package/io/_utils.cjs +0 -56
  120. package/io/_utils.d.cts +0 -10
  121. package/io/_utils.d.ts +0 -10
  122. package/io/_utils.js +0 -51
  123. package/io/buf-writer.cjs +0 -149
  124. package/io/buf-writer.d.cts +0 -13
  125. package/io/buf-writer.d.ts +0 -13
  126. package/io/buf-writer.js +0 -148
  127. package/io/io.cjs +0 -199
  128. package/io/io.d.cts +0 -5
  129. package/io/io.d.ts +0 -5
  130. package/io/io.js +0 -198
  131. package/io/readers.cjs +0 -337
  132. package/io/readers.d.cts +0 -69
  133. package/io/readers.d.ts +0 -69
  134. package/io/readers.js +0 -333
  135. package/io/writers.cjs +0 -196
  136. package/io/writers.d.cts +0 -22
  137. package/io/writers.d.ts +0 -22
  138. package/io/writers.js +0 -195
  139. package/types/agent.d.ts +0 -72
  140. package/types/dialer.d.ts +0 -30
  141. package/types/index.d.cts +0 -2
  142. package/types/index.d.ts +0 -2
@@ -0,0 +1,421 @@
1
+ import { afterAll, describe, expect, test } from "bun:test";
2
+ import { createFetch, HttpClient, normalizeHeaders } from "../src/fetch";
3
+ import { createTestServer } from "./test-utils";
4
+
5
+ describe("fetch.ts weblike API", () => {
6
+ const testServer = createTestServer();
7
+
8
+ afterAll(async () => {
9
+ await testServer.stop();
10
+ });
11
+
12
+ async function withEnv(
13
+ values: Partial<Record<string, string | undefined>>,
14
+ run: () => Promise<void>,
15
+ ): Promise<void> {
16
+ const previous = new Map<string, string | undefined>();
17
+
18
+ for (const [key, value] of Object.entries(values)) {
19
+ previous.set(key, process.env[key]);
20
+ if (value == null) {
21
+ delete process.env[key];
22
+ } else {
23
+ process.env[key] = value;
24
+ }
25
+ }
26
+
27
+ try {
28
+ await run();
29
+ } finally {
30
+ for (const [key, value] of previous) {
31
+ if (value == null) {
32
+ delete process.env[key];
33
+ } else {
34
+ process.env[key] = value;
35
+ }
36
+ }
37
+ }
38
+ }
39
+
40
+ test("normalizeHeaders preserves tuples, records and Headers", () => {
41
+ const fromRecord = normalizeHeaders({
42
+ "x-one": "1",
43
+ "x-two": "2",
44
+ });
45
+ expect(fromRecord.get("x-one")).toBe("1");
46
+ expect(fromRecord.get("x-two")).toBe("2");
47
+
48
+ const fromTuples = normalizeHeaders([
49
+ ["x-a", "a"],
50
+ ["x-b", "b"],
51
+ ]);
52
+ expect(fromTuples.get("x-a")).toBe("a");
53
+ expect(fromTuples.get("x-b")).toBe("b");
54
+
55
+ const headers = new Headers({ "x-test": "ok" });
56
+ const same = normalizeHeaders(headers);
57
+ expect(same).toBe(headers);
58
+ });
59
+
60
+ test("createFetch performs a basic GET request", async () => {
61
+ const fetchLike = createFetch();
62
+
63
+ try {
64
+ const response = await fetchLike(`${testServer.baseUrl}/text`);
65
+ expect(response.status).toBe(200);
66
+ expect(response.ok).toBe(true);
67
+ expect(await response.text()).toBe("Hello, World!");
68
+ } finally {
69
+ await fetchLike.close();
70
+ }
71
+ });
72
+
73
+ test("HttpClient raw API performs POST JSON requests", async () => {
74
+ const client = new HttpClient();
75
+
76
+ try {
77
+ const response = await client.send({
78
+ url: `${testServer.baseUrl}/echo`,
79
+ method: "POST",
80
+ headers: new Headers({
81
+ "content-type": "application/json",
82
+ }),
83
+ body: JSON.stringify({ test: "data" }),
84
+ });
85
+
86
+ expect(response.status).toBe(200);
87
+
88
+ const echo = await response.json();
89
+ expect(echo.method).toBe("POST");
90
+ expect(echo.headers["content-type"]).toContain("application/json");
91
+ expect(echo.bodyText).toBe(JSON.stringify({ test: "data" }));
92
+ } finally {
93
+ await client.close();
94
+ }
95
+ });
96
+
97
+ test("Request input is accepted and body/method are inherited", async () => {
98
+ const fetchLike = createFetch();
99
+
100
+ try {
101
+ const request = new Request(`${testServer.baseUrl}/echo`, {
102
+ method: "POST",
103
+ headers: {
104
+ "content-type": "text/plain;charset=utf-8",
105
+ },
106
+ body: "from-request-object",
107
+ });
108
+
109
+ const response = await fetchLike(request);
110
+ expect(response.status).toBe(200);
111
+
112
+ const echo = await response.json();
113
+ expect(echo.method).toBe("POST");
114
+ expect(echo.bodyText).toBe("from-request-object");
115
+ } finally {
116
+ await fetchLike.close();
117
+ }
118
+ });
119
+
120
+ test("URLSearchParams bodies are encoded and content-type is set", async () => {
121
+ const fetchLike = createFetch();
122
+
123
+ try {
124
+ const body = new URLSearchParams({
125
+ username: "john",
126
+ password: "secret123",
127
+ });
128
+
129
+ const response = await fetchLike(`${testServer.baseUrl}/echo`, {
130
+ method: "POST",
131
+ body,
132
+ });
133
+
134
+ expect(response.status).toBe(200);
135
+
136
+ const echo = await response.json();
137
+ expect(echo.headers["content-type"]).toContain(
138
+ "application/x-www-form-urlencoded",
139
+ );
140
+ expect(echo.bodyText).toBe("username=john&password=secret123");
141
+ } finally {
142
+ await fetchLike.close();
143
+ }
144
+ });
145
+
146
+ test("compressed responses are transparently decompressed", async () => {
147
+ const fetchLike = createFetch();
148
+
149
+ try {
150
+ const response = await fetchLike(`${testServer.baseUrl}/gzip`);
151
+ expect(response.status).toBe(200);
152
+ expect(await response.text()).toBe("This is compressed content!");
153
+ } finally {
154
+ await fetchLike.close();
155
+ }
156
+ });
157
+
158
+ test("redirects follow by default and annotate the final response", async () => {
159
+ const fetchLike = createFetch();
160
+
161
+ try {
162
+ const response = await fetchLike(`${testServer.baseUrl}/redirect`);
163
+ expect(response.status).toBe(200);
164
+ expect(response.redirected).toBe(true);
165
+ expect(response.url).toBe(
166
+ `${testServer.baseUrl}/redirected-target`,
167
+ );
168
+ expect(await response.text()).toBe("You followed the redirect");
169
+ } finally {
170
+ await fetchLike.close();
171
+ }
172
+ });
173
+
174
+ test("redirect mode manual returns the original redirect response", async () => {
175
+ const fetchLike = createFetch();
176
+
177
+ try {
178
+ const response = await fetchLike(`${testServer.baseUrl}/redirect`, {
179
+ redirect: "manual",
180
+ });
181
+ expect(response.status).toBe(302);
182
+ expect(response.redirected).toBe(false);
183
+ expect(response.url).toBe(`${testServer.baseUrl}/redirect`);
184
+ expect(response.headers.get("location")).toBe("/redirected-target");
185
+ expect(await response.text()).toBe(
186
+ "Redirecting to /redirected-target",
187
+ );
188
+ } finally {
189
+ await fetchLike.close();
190
+ }
191
+ });
192
+
193
+ test("redirect mode error rejects with TypeError", async () => {
194
+ const fetchLike = createFetch();
195
+
196
+ try {
197
+ await expect(
198
+ fetchLike(`${testServer.baseUrl}/redirect`, {
199
+ redirect: "error",
200
+ }),
201
+ ).rejects.toBeInstanceOf(TypeError);
202
+ } finally {
203
+ await fetchLike.close();
204
+ }
205
+ });
206
+
207
+ test("GET and HEAD requests reject explicit bodies", async () => {
208
+ const fetchLike = createFetch();
209
+
210
+ try {
211
+ await expect(
212
+ fetchLike(`${testServer.baseUrl}/echo`, {
213
+ method: "GET",
214
+ body: "invalid",
215
+ }),
216
+ ).rejects.toBeInstanceOf(TypeError);
217
+
218
+ await expect(
219
+ fetchLike(`${testServer.baseUrl}/echo`, {
220
+ method: "HEAD",
221
+ body: "invalid",
222
+ }),
223
+ ).rejects.toBeInstanceOf(TypeError);
224
+ } finally {
225
+ await fetchLike.close();
226
+ }
227
+ });
228
+
229
+ test("fetch rejects URLs with embedded credentials", async () => {
230
+ const fetchLike = createFetch();
231
+
232
+ try {
233
+ await expect(
234
+ fetchLike(
235
+ `http://user:pass@127.0.0.1:${new URL(testServer.baseUrl).port}/text`,
236
+ ),
237
+ ).rejects.toBeInstanceOf(TypeError);
238
+ } finally {
239
+ await fetchLike.close();
240
+ }
241
+ });
242
+
243
+ test("fetch rejects network failures with TypeError", async () => {
244
+ const fetchLike = createFetch();
245
+
246
+ try {
247
+ await expect(
248
+ fetchLike("http://127.0.0.1:1/"),
249
+ ).rejects.toBeInstanceOf(TypeError);
250
+ } finally {
251
+ await fetchLike.close();
252
+ }
253
+ });
254
+
255
+ test("environment proxies are used when no explicit proxy is provided", async () => {
256
+ const fetchLike = createFetch();
257
+
258
+ try {
259
+ await withEnv(
260
+ {
261
+ HTTP_PROXY: "http://127.0.0.1:1",
262
+ HTTPS_PROXY: undefined,
263
+ SOCKS5_PROXY: undefined,
264
+ SOCKS_PROXY: undefined,
265
+ },
266
+ async () => {
267
+ await expect(
268
+ fetchLike(`${testServer.baseUrl}/text`),
269
+ ).rejects.toBeInstanceOf(TypeError);
270
+ },
271
+ );
272
+ } finally {
273
+ await fetchLike.close();
274
+ }
275
+ });
276
+
277
+ test("proxy: null disables environment proxy resolution", async () => {
278
+ const fetchLike = createFetch();
279
+
280
+ try {
281
+ await withEnv(
282
+ {
283
+ HTTP_PROXY: "http://127.0.0.1:1",
284
+ HTTPS_PROXY: undefined,
285
+ SOCKS5_PROXY: undefined,
286
+ SOCKS_PROXY: undefined,
287
+ },
288
+ async () => {
289
+ const response = await fetchLike(
290
+ `${testServer.baseUrl}/text`,
291
+ {
292
+ proxy: null,
293
+ },
294
+ );
295
+ expect(response.status).toBe(200);
296
+ expect(await response.text()).toBe("Hello, World!");
297
+ },
298
+ );
299
+ } finally {
300
+ await fetchLike.close();
301
+ }
302
+ });
303
+
304
+ test("proxy accepts ProxyInfo objects directly", async () => {
305
+ const fetchLike = createFetch();
306
+
307
+ try {
308
+ await expect(
309
+ fetchLike(`${testServer.baseUrl}/text`, {
310
+ proxy: {
311
+ protocol: "http",
312
+ host: "127.0.0.1",
313
+ port: 1,
314
+ },
315
+ }),
316
+ ).rejects.toBeInstanceOf(TypeError);
317
+ } finally {
318
+ await fetchLike.close();
319
+ }
320
+ });
321
+
322
+ test("abort signals cancel in-flight requests with AbortError DOMException", async () => {
323
+ const fetchLike = createFetch();
324
+
325
+ try {
326
+ const controller = new AbortController();
327
+ const promise = fetchLike(`${testServer.baseUrl}/slow`, {
328
+ signal: controller.signal,
329
+ });
330
+
331
+ setTimeout(() => controller.abort(), 50);
332
+
333
+ await expect(promise).rejects.toMatchObject({
334
+ name: "AbortError",
335
+ });
336
+ } finally {
337
+ await fetchLike.close();
338
+ }
339
+ });
340
+
341
+ test("AbortSignal.timeout() is preserved as TimeoutError", async () => {
342
+ const fetchLike = createFetch();
343
+
344
+ try {
345
+ await expect(
346
+ fetchLike(`${testServer.baseUrl}/slow`, {
347
+ signal: AbortSignal.timeout(10),
348
+ }),
349
+ ).rejects.toMatchObject({
350
+ name: "TimeoutError",
351
+ });
352
+ } finally {
353
+ await fetchLike.close();
354
+ }
355
+ });
356
+
357
+ test("body decoding failures become TypeError on the public fetch API", async () => {
358
+ const fetchLike = createFetch();
359
+
360
+ try {
361
+ const response = await fetchLike(`${testServer.baseUrl}/bad-gzip`);
362
+ await expect(response.text()).rejects.toBeInstanceOf(TypeError);
363
+ } finally {
364
+ await fetchLike.close();
365
+ }
366
+ });
367
+
368
+ test("body abort after headers becomes AbortError on the public fetch API", async () => {
369
+ const fetchLike = createFetch();
370
+
371
+ try {
372
+ const controller = new AbortController();
373
+ const response = await fetchLike(
374
+ `${testServer.baseUrl}/slow-body`,
375
+ {
376
+ signal: controller.signal,
377
+ },
378
+ );
379
+
380
+ controller.abort();
381
+
382
+ await expect(response.text()).rejects.toMatchObject({
383
+ name: "AbortError",
384
+ });
385
+ } finally {
386
+ await fetchLike.close();
387
+ }
388
+ });
389
+
390
+ test("second body read rejects with TypeError", async () => {
391
+ const fetchLike = createFetch();
392
+
393
+ try {
394
+ const response = await fetchLike(`${testServer.baseUrl}/text`);
395
+ expect(await response.text()).toBe("Hello, World!");
396
+ await expect(response.text()).rejects.toBeInstanceOf(TypeError);
397
+ } finally {
398
+ await fetchLike.close();
399
+ }
400
+ });
401
+
402
+ test("HttpClient.close() allows fresh pools on later requests", async () => {
403
+ const client = new HttpClient();
404
+
405
+ const first = await client.send({
406
+ url: `${testServer.baseUrl}/text`,
407
+ method: "GET",
408
+ });
409
+ expect(await first.text()).toBe("Hello, World!");
410
+
411
+ await client.close();
412
+
413
+ const second = await client.send({
414
+ url: `${testServer.baseUrl}/text`,
415
+ method: "GET",
416
+ });
417
+ expect(await second.text()).toBe("Hello, World!");
418
+
419
+ await client.close();
420
+ });
421
+ });
@@ -0,0 +1,127 @@
1
+ import { afterAll, describe, expect, test } from "bun:test";
2
+ import {
3
+ ResponseBodyError,
4
+ ResponseDecodeError,
5
+ ResponseHeaderError,
6
+ } from "../src/errors";
7
+ import { HttpClient } from "../src/fetch";
8
+ import { createTestServer } from "./test-utils";
9
+
10
+ describe("high-level I/O options", () => {
11
+ const testServer = createTestServer();
12
+
13
+ afterAll(async () => {
14
+ await testServer.stop();
15
+ });
16
+
17
+ test("reader.maxHeaderSize is enforced through HttpClient -> AgentPool -> Agent", async () => {
18
+ const client = new HttpClient({
19
+ io: {
20
+ reader: {
21
+ maxHeaderSize: 64,
22
+ },
23
+ },
24
+ });
25
+
26
+ try {
27
+ await expect(
28
+ client.send({
29
+ url: `${testServer.baseUrl}/huge-header`,
30
+ method: "GET",
31
+ }),
32
+ ).rejects.toBeInstanceOf(ResponseHeaderError);
33
+ } finally {
34
+ await client.close();
35
+ }
36
+ });
37
+
38
+ test("reader.maxBodySize is enforced while consuming the body stream", async () => {
39
+ const client = new HttpClient({
40
+ io: {
41
+ reader: {
42
+ maxBodySize: 128,
43
+ },
44
+ },
45
+ });
46
+
47
+ try {
48
+ const response = await client.send({
49
+ url: `${testServer.baseUrl}/large-stream`,
50
+ method: "GET",
51
+ });
52
+
53
+ await expect(response.text()).rejects.toBeInstanceOf(
54
+ ResponseBodyError,
55
+ );
56
+ } finally {
57
+ await client.close();
58
+ }
59
+ });
60
+
61
+ test("reader.maxDecodedBodySize errors while consuming the decoded body", async () => {
62
+ const client = new HttpClient({
63
+ io: {
64
+ reader: {
65
+ maxDecodedBodySize: 8,
66
+ },
67
+ },
68
+ });
69
+
70
+ try {
71
+ const response = await client.send({
72
+ url: `${testServer.baseUrl}/gzip`,
73
+ method: "GET",
74
+ });
75
+
76
+ await expect(response.text()).rejects.toBeInstanceOf(
77
+ ResponseBodyError,
78
+ );
79
+ } finally {
80
+ await client.close();
81
+ }
82
+ });
83
+
84
+ test("decoding failures surface as ResponseDecodeError on the advanced API", async () => {
85
+ const client = new HttpClient();
86
+
87
+ try {
88
+ const response = await client.send({
89
+ url: `${testServer.baseUrl}/bad-gzip`,
90
+ method: "GET",
91
+ });
92
+
93
+ await expect(response.text()).rejects.toBeInstanceOf(
94
+ ResponseDecodeError,
95
+ );
96
+ } finally {
97
+ await client.close();
98
+ }
99
+ });
100
+
101
+ test("reader.decompress=false keeps the compressed payload untouched", async () => {
102
+ const client = new HttpClient({
103
+ io: {
104
+ reader: {
105
+ decompress: false,
106
+ },
107
+ },
108
+ });
109
+
110
+ try {
111
+ const response = await client.send({
112
+ url: `${testServer.baseUrl}/gzip`,
113
+ method: "GET",
114
+ });
115
+
116
+ expect(response.headers.get("content-encoding")).toBe("gzip");
117
+
118
+ const body = new Uint8Array(await response.arrayBuffer());
119
+ expect(body.byteLength).toBeGreaterThan(0);
120
+ expect(new TextDecoder().decode(body)).not.toBe(
121
+ "This is compressed content!",
122
+ );
123
+ } finally {
124
+ await client.close();
125
+ }
126
+ });
127
+ });