@npy/fetch 0.1.1 → 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 (140) 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 +36 -25
  8. package/src/_internal/consts.ts +3 -0
  9. package/src/_internal/decode-stream-error.ts +16 -0
  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/src/types/agent.ts +98 -0
  33. package/src/types/{dialer.d.cts → dialer.ts} +22 -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.js +0 -4
  47. package/_internal/error-adapters.cjs +0 -146
  48. package/_internal/error-adapters.js +0 -142
  49. package/_internal/guards.cjs +0 -24
  50. package/_internal/guards.js +0 -17
  51. package/_internal/net.cjs +0 -95
  52. package/_internal/net.js +0 -92
  53. package/_internal/promises.cjs +0 -18
  54. package/_internal/promises.js +0 -18
  55. package/_internal/streams.cjs +0 -37
  56. package/_internal/streams.js +0 -36
  57. package/_virtual/_rolldown/runtime.cjs +0 -23
  58. package/agent-pool.cjs +0 -78
  59. package/agent-pool.js +0 -77
  60. package/agent.cjs +0 -257
  61. package/agent.js +0 -256
  62. package/body.cjs +0 -154
  63. package/body.js +0 -151
  64. package/dialers/proxy.cjs +0 -49
  65. package/dialers/proxy.js +0 -48
  66. package/dialers/tcp.cjs +0 -70
  67. package/dialers/tcp.js +0 -67
  68. package/encoding.cjs +0 -95
  69. package/encoding.js +0 -91
  70. package/errors.cjs +0 -275
  71. package/errors.js +0 -259
  72. package/fetch.cjs +0 -117
  73. package/fetch.js +0 -115
  74. package/http-client.cjs +0 -33
  75. package/http-client.js +0 -33
  76. package/index.cjs +0 -45
  77. package/index.d.cts +0 -1
  78. package/index.d.ts +0 -1
  79. package/index.js +0 -9
  80. package/io/_utils.cjs +0 -56
  81. package/io/_utils.js +0 -51
  82. package/io/buf-writer.cjs +0 -149
  83. package/io/buf-writer.js +0 -148
  84. package/io/io.cjs +0 -135
  85. package/io/io.js +0 -134
  86. package/io/readers.cjs +0 -377
  87. package/io/readers.js +0 -373
  88. package/io/writers.cjs +0 -191
  89. package/io/writers.js +0 -190
  90. package/src/_internal/consts.d.cts +0 -3
  91. package/src/_internal/consts.d.ts +0 -3
  92. package/src/_internal/error-adapters.d.cts +0 -22
  93. package/src/_internal/error-adapters.d.ts +0 -22
  94. package/src/_internal/guards.d.cts +0 -13
  95. package/src/_internal/guards.d.ts +0 -13
  96. package/src/_internal/net.d.cts +0 -12
  97. package/src/_internal/net.d.ts +0 -12
  98. package/src/_internal/promises.d.cts +0 -1
  99. package/src/_internal/promises.d.ts +0 -1
  100. package/src/_internal/streams.d.cts +0 -21
  101. package/src/_internal/streams.d.ts +0 -21
  102. package/src/agent-pool.d.cts +0 -2
  103. package/src/agent-pool.d.ts +0 -2
  104. package/src/agent.d.cts +0 -3
  105. package/src/agent.d.ts +0 -3
  106. package/src/body.d.cts +0 -23
  107. package/src/body.d.ts +0 -23
  108. package/src/dialers/index.d.cts +0 -3
  109. package/src/dialers/index.d.ts +0 -3
  110. package/src/dialers/proxy.d.cts +0 -19
  111. package/src/dialers/proxy.d.ts +0 -19
  112. package/src/dialers/tcp.d.cts +0 -36
  113. package/src/dialers/tcp.d.ts +0 -36
  114. package/src/encoding.d.cts +0 -24
  115. package/src/encoding.d.ts +0 -24
  116. package/src/errors.d.cts +0 -110
  117. package/src/errors.d.ts +0 -110
  118. package/src/fetch.d.cts +0 -36
  119. package/src/fetch.d.ts +0 -36
  120. package/src/http-client.d.cts +0 -23
  121. package/src/http-client.d.ts +0 -23
  122. package/src/index.d.cts +0 -7
  123. package/src/index.d.ts +0 -7
  124. package/src/io/_utils.d.cts +0 -10
  125. package/src/io/_utils.d.ts +0 -10
  126. package/src/io/buf-writer.d.cts +0 -13
  127. package/src/io/buf-writer.d.ts +0 -13
  128. package/src/io/io.d.cts +0 -5
  129. package/src/io/io.d.ts +0 -5
  130. package/src/io/readers.d.cts +0 -199
  131. package/src/io/readers.d.ts +0 -199
  132. package/src/io/writers.d.cts +0 -22
  133. package/src/io/writers.d.ts +0 -22
  134. package/src/types/agent.d.cts +0 -128
  135. package/src/types/agent.d.ts +0 -128
  136. package/src/types/dialer.d.ts +0 -27
  137. package/src/types/index.d.cts +0 -2
  138. package/src/types/index.d.ts +0 -2
  139. package/tests/test-utils.d.cts +0 -8
  140. package/tests/test-utils.d.ts +0 -8
@@ -0,0 +1,348 @@
1
+ import { afterAll, describe, expect, test } from "bun:test";
2
+ import { createFetch } from "../src/fetch";
3
+ import { createTestServer } from "./test-utils";
4
+
5
+ interface MultipartEchoResponse {
6
+ method: string;
7
+ contentType: string;
8
+ contentLength: number | null;
9
+ fields: Record<
10
+ string,
11
+ | string
12
+ | string[]
13
+ | {
14
+ type: "file";
15
+ filename: string;
16
+ mimeType: string;
17
+ size: number;
18
+ content: string;
19
+ }
20
+ >;
21
+ }
22
+
23
+ describe("multipart/form-data — integration", () => {
24
+ const testServer = createTestServer();
25
+
26
+ afterAll(async () => {
27
+ await testServer.stop();
28
+ });
29
+
30
+ test("Content-Type is multipart/form-data and contains a boundary", async () => {
31
+ const fetchLike = createFetch();
32
+
33
+ try {
34
+ const form = new FormData();
35
+ form.append("x", "1");
36
+
37
+ const response = await fetchLike(
38
+ `${testServer.baseUrl}/multipart-echo`,
39
+ { method: "POST", body: form },
40
+ );
41
+
42
+ expect(response.status).toBe(200);
43
+
44
+ const echo: MultipartEchoResponse = await response.json();
45
+ expect(echo.contentType).toContain("multipart/form-data");
46
+ expect(echo.contentType).toMatch(/boundary=[^\s;]+/);
47
+ } finally {
48
+ await fetchLike.close();
49
+ }
50
+ });
51
+
52
+ test("Content-Length is sent and matches the body received by the server", async () => {
53
+ const fetchLike = createFetch();
54
+
55
+ try {
56
+ const form = new FormData();
57
+ form.append("field", "some value");
58
+
59
+ const response = await fetchLike(
60
+ `${testServer.baseUrl}/multipart-echo`,
61
+ { method: "POST", body: form },
62
+ );
63
+
64
+ expect(response.status).toBe(200);
65
+
66
+ const echo: MultipartEchoResponse = await response.json();
67
+
68
+ expect(echo.contentLength).not.toBeNull();
69
+ expect(echo.contentLength).toBeGreaterThan(0);
70
+ } finally {
71
+ await fetchLike.close();
72
+ }
73
+ });
74
+
75
+ test("single text field is received correctly", async () => {
76
+ const fetchLike = createFetch();
77
+
78
+ try {
79
+ const form = new FormData();
80
+ form.append("greeting", "hello world");
81
+
82
+ const response = await fetchLike(
83
+ `${testServer.baseUrl}/multipart-echo`,
84
+ { method: "POST", body: form },
85
+ );
86
+
87
+ const echo: MultipartEchoResponse = await response.json();
88
+ expect(echo.fields["greeting"]).toBe("hello world");
89
+ } finally {
90
+ await fetchLike.close();
91
+ }
92
+ });
93
+
94
+ test("multiple text fields are all received", async () => {
95
+ const fetchLike = createFetch();
96
+
97
+ try {
98
+ const form = new FormData();
99
+ form.append("first", "Alice");
100
+ form.append("last", "Smith");
101
+ form.append("age", "30");
102
+
103
+ const response = await fetchLike(
104
+ `${testServer.baseUrl}/multipart-echo`,
105
+ { method: "POST", body: form },
106
+ );
107
+
108
+ const echo: MultipartEchoResponse = await response.json();
109
+ expect(echo.fields["first"]).toBe("Alice");
110
+ expect(echo.fields["last"]).toBe("Smith");
111
+ expect(echo.fields["age"]).toBe("30");
112
+ } finally {
113
+ await fetchLike.close();
114
+ }
115
+ });
116
+
117
+ test("multiple values for the same field name are received as an array", async () => {
118
+ const fetchLike = createFetch();
119
+
120
+ try {
121
+ const form = new FormData();
122
+ form.append("tag", "typescript");
123
+ form.append("tag", "http");
124
+ form.append("tag", "fetch");
125
+
126
+ const response = await fetchLike(
127
+ `${testServer.baseUrl}/multipart-echo`,
128
+ { method: "POST", body: form },
129
+ );
130
+
131
+ const echo: MultipartEchoResponse = await response.json();
132
+ expect(echo.fields["tag"]).toEqual(["typescript", "http", "fetch"]);
133
+ } finally {
134
+ await fetchLike.close();
135
+ }
136
+ });
137
+
138
+ test("multibyte UTF-8 field values are received correctly", async () => {
139
+ const fetchLike = createFetch();
140
+
141
+ try {
142
+ const form = new FormData();
143
+ form.append("greeting", "こんにちは");
144
+ form.append("emoji", "🚀");
145
+
146
+ const response = await fetchLike(
147
+ `${testServer.baseUrl}/multipart-echo`,
148
+ { method: "POST", body: form },
149
+ );
150
+
151
+ const echo: MultipartEchoResponse = await response.json();
152
+ expect(echo.fields["greeting"]).toBe("こんにちは");
153
+ expect(echo.fields["emoji"]).toBe("🚀");
154
+ } finally {
155
+ await fetchLike.close();
156
+ }
157
+ });
158
+
159
+ test("Blob field content is received intact", async () => {
160
+ const fetchLike = createFetch();
161
+
162
+ try {
163
+ const content = "hello from blob";
164
+ const form = new FormData();
165
+ form.append(
166
+ "upload",
167
+ new Blob([content], { type: "text/plain" }),
168
+ "hello.txt",
169
+ );
170
+
171
+ const response = await fetchLike(
172
+ `${testServer.baseUrl}/multipart-echo`,
173
+ { method: "POST", body: form },
174
+ );
175
+
176
+ const echo: MultipartEchoResponse = await response.json();
177
+ const file = echo.fields["upload"] as {
178
+ type: string;
179
+ filename: string;
180
+ mimeType: string;
181
+ content: string;
182
+ };
183
+
184
+ expect(file.filename).toBe("hello.txt");
185
+ expect(file.mimeType).toContain("text/plain");
186
+ expect(file.content).toBe(content);
187
+ } finally {
188
+ await fetchLike.close();
189
+ }
190
+ });
191
+
192
+ test("binary Blob field is received with correct byte count", async () => {
193
+ const fetchLike = createFetch();
194
+
195
+ try {
196
+ const bytes = new Uint8Array(256).map((_, i) => i);
197
+ const form = new FormData();
198
+ form.append(
199
+ "bin",
200
+ new Blob([bytes], { type: "application/octet-stream" }),
201
+ "data.bin",
202
+ );
203
+
204
+ const response = await fetchLike(
205
+ `${testServer.baseUrl}/multipart-echo`,
206
+ { method: "POST", body: form },
207
+ );
208
+
209
+ const echo: MultipartEchoResponse = await response.json();
210
+ const file = echo.fields["bin"] as {
211
+ size: number;
212
+ mimeType: string;
213
+ };
214
+
215
+ expect(file.size).toBe(256);
216
+ expect(file.mimeType).toBe("application/octet-stream");
217
+ } finally {
218
+ await fetchLike.close();
219
+ }
220
+ });
221
+
222
+ test("Blob without explicit MIME type is received with correct content", async () => {
223
+ const fetchLike = createFetch();
224
+
225
+ try {
226
+ const form = new FormData();
227
+ form.append("f", new Blob(["data"]), "file.dat");
228
+
229
+ const response = await fetchLike(
230
+ `${testServer.baseUrl}/multipart-echo`,
231
+ { method: "POST", body: form },
232
+ );
233
+
234
+ const echo: MultipartEchoResponse = await response.json();
235
+ const file = echo.fields["f"] as { size: number; content: string };
236
+ expect(file.size).toBe(4);
237
+ expect(file.content).toBe("data");
238
+ } finally {
239
+ await fetchLike.close();
240
+ }
241
+ });
242
+
243
+ test("mixed text and Blob fields are all received correctly", async () => {
244
+ const fetchLike = createFetch();
245
+
246
+ try {
247
+ const form = new FormData();
248
+ form.append("title", "My Upload");
249
+ form.append(
250
+ "file",
251
+ new Blob(["<html><body>hi</body></html>"], {
252
+ type: "text/html",
253
+ }),
254
+ "index.html",
255
+ );
256
+ form.append("note", "optional note");
257
+
258
+ const response = await fetchLike(
259
+ `${testServer.baseUrl}/multipart-echo`,
260
+ { method: "POST", body: form },
261
+ );
262
+
263
+ const echo: MultipartEchoResponse = await response.json();
264
+
265
+ expect(echo.fields["title"]).toBe("My Upload");
266
+ expect(echo.fields["note"]).toBe("optional note");
267
+
268
+ const file = echo.fields["file"] as {
269
+ filename: string;
270
+ content: string;
271
+ };
272
+ expect(file.filename).toBe("index.html");
273
+ expect(file.content).toBe("<html><body>hi</body></html>");
274
+ } finally {
275
+ await fetchLike.close();
276
+ }
277
+ });
278
+
279
+ test("empty FormData sends a structurally valid body", async () => {
280
+ const fetchLike = createFetch();
281
+
282
+ try {
283
+ const response = await fetchLike(
284
+ `${testServer.baseUrl}/multipart-echo`,
285
+ { method: "POST", body: new FormData() },
286
+ );
287
+
288
+ expect(response.status).toBe(200);
289
+
290
+ const echo: MultipartEchoResponse = await response.json();
291
+ expect(echo.fields).toEqual({});
292
+ expect(echo.contentLength).toBeGreaterThan(0);
293
+ } finally {
294
+ await fetchLike.close();
295
+ }
296
+ });
297
+
298
+ test("Content-Length is accurate for a large Blob field", async () => {
299
+ const fetchLike = createFetch();
300
+
301
+ try {
302
+ const large = new Uint8Array(64 * 1024).fill(0x41);
303
+ const form = new FormData();
304
+ form.append(
305
+ "payload",
306
+ new Blob([large], { type: "application/octet-stream" }),
307
+ "large.bin",
308
+ );
309
+
310
+ const response = await fetchLike(
311
+ `${testServer.baseUrl}/multipart-echo`,
312
+ { method: "POST", body: form },
313
+ );
314
+
315
+ expect(response.status).toBe(200);
316
+
317
+ const echo: MultipartEchoResponse = await response.json();
318
+ const file = echo.fields["payload"] as { size: number };
319
+
320
+ expect(file.size).toBe(64 * 1024);
321
+ expect(echo.contentLength).toBeGreaterThan(64 * 1024);
322
+ } finally {
323
+ await fetchLike.close();
324
+ }
325
+ });
326
+
327
+ test("Request object with FormData body is forwarded correctly", async () => {
328
+ const fetchLike = createFetch();
329
+
330
+ try {
331
+ const form = new FormData();
332
+ form.append("from", "request-object");
333
+
334
+ const request = new Request(
335
+ `${testServer.baseUrl}/multipart-echo`,
336
+ { method: "POST", body: form },
337
+ );
338
+
339
+ const response = await fetchLike(request);
340
+ expect(response.status).toBe(200);
341
+
342
+ const echo: MultipartEchoResponse = await response.json();
343
+ expect(echo.fields["from"]).toBe("request-object");
344
+ } finally {
345
+ await fetchLike.close();
346
+ }
347
+ });
348
+ });
@@ -0,0 +1,335 @@
1
+ import { gzipSync } from "node:zlib";
2
+
3
+ export interface TestServer {
4
+ server: ReturnType<typeof Bun.serve>;
5
+ baseUrl: string;
6
+ stop(): Promise<void>;
7
+ }
8
+
9
+ const encoder = new TextEncoder();
10
+ const decoder = new TextDecoder();
11
+
12
+ export function sleep(ms: number): Promise<void> {
13
+ return new Promise((resolve) => setTimeout(resolve, ms));
14
+ }
15
+
16
+ export function headersToObject(headers: Headers): Record<string, string> {
17
+ const out: Record<string, string> = {};
18
+ for (const [key, value] of headers.entries()) {
19
+ out[key] = value;
20
+ }
21
+ return out;
22
+ }
23
+
24
+ function json(value: unknown, init?: ResponseInit): Response {
25
+ return Response.json(value, init);
26
+ }
27
+
28
+ function textResponse(
29
+ body: string,
30
+ init?: ResponseInit & { headers?: Record<string, string> },
31
+ ): Response {
32
+ return new Response(body, {
33
+ ...init,
34
+ headers: {
35
+ "content-type": "text/plain; charset=utf-8",
36
+ ...(init?.headers ?? {}),
37
+ },
38
+ });
39
+ }
40
+
41
+ function headAwareText(
42
+ request: Request,
43
+ body: string,
44
+ init?: ResponseInit & { headers?: Record<string, string> },
45
+ ): Response {
46
+ if (request.method === "HEAD") {
47
+ return new Response(null, {
48
+ ...init,
49
+ headers: {
50
+ "content-type": "text/plain; charset=utf-8",
51
+ ...(init?.headers ?? {}),
52
+ },
53
+ });
54
+ }
55
+
56
+ return textResponse(body, init);
57
+ }
58
+
59
+ function headAwareJson(
60
+ request: Request,
61
+ body: unknown,
62
+ init?: ResponseInit & { headers?: Record<string, string> },
63
+ ): Response {
64
+ if (request.method === "HEAD") {
65
+ return new Response(null, {
66
+ ...init,
67
+ headers: {
68
+ "content-type": "application/json",
69
+ ...(init?.headers ?? {}),
70
+ },
71
+ });
72
+ }
73
+
74
+ return json(body, {
75
+ ...init,
76
+ headers: {
77
+ "content-type": "application/json",
78
+ ...(init?.headers ?? {}),
79
+ },
80
+ });
81
+ }
82
+
83
+ export function createTestServer(): TestServer {
84
+ const server = Bun.serve({
85
+ hostname: "127.0.0.1",
86
+ port: 0,
87
+ async fetch(request: Request): Promise<Response> {
88
+ const url = new URL(request.url);
89
+ const headers = headersToObject(request.headers);
90
+
91
+ switch (url.pathname) {
92
+ case "/text": {
93
+ return headAwareText(request, "Hello, World!");
94
+ }
95
+
96
+ case "/json": {
97
+ return headAwareJson(request, { message: "Hello, JSON!" });
98
+ }
99
+
100
+ case "/echo": {
101
+ if (request.method === "HEAD") {
102
+ return new Response(null, {
103
+ status: 200,
104
+ headers: {
105
+ "content-type": "application/json",
106
+ },
107
+ });
108
+ }
109
+
110
+ const bodyBytes = new Uint8Array(
111
+ await request.arrayBuffer(),
112
+ );
113
+ const bodyText = decoder.decode(bodyBytes);
114
+
115
+ return json(
116
+ {
117
+ method: request.method,
118
+ url: `${url.pathname}${url.search}`,
119
+ headers,
120
+ bodyText,
121
+ bodyLength: bodyBytes.byteLength,
122
+ },
123
+ {
124
+ status: 200,
125
+ headers: {
126
+ "content-type": "application/json",
127
+ },
128
+ },
129
+ );
130
+ }
131
+
132
+ case "/slow": {
133
+ await sleep(200);
134
+ return textResponse("Finally!");
135
+ }
136
+
137
+ case "/slow-body": {
138
+ let sent = 0;
139
+
140
+ const stream = new ReadableStream<Uint8Array>({
141
+ async pull(controller) {
142
+ if (sent === 0) {
143
+ controller.enqueue(encoder.encode("part-1 "));
144
+ sent = 1;
145
+ return;
146
+ }
147
+
148
+ if (sent === 1) {
149
+ await sleep(200);
150
+ controller.enqueue(encoder.encode("part-2"));
151
+ controller.close();
152
+ sent = 2;
153
+ }
154
+ },
155
+ });
156
+
157
+ return new Response(stream, {
158
+ status: 200,
159
+ headers: {
160
+ "content-type": "text/plain; charset=utf-8",
161
+ },
162
+ });
163
+ }
164
+
165
+ case "/chunked": {
166
+ let index = 0;
167
+ const chunks = ["chunk1", "chunk2", "chunk3"];
168
+
169
+ const stream = new ReadableStream<Uint8Array>({
170
+ pull(controller) {
171
+ if (index >= chunks.length) {
172
+ controller.close();
173
+ return;
174
+ }
175
+
176
+ controller.enqueue(encoder.encode(chunks[index]));
177
+ index += 1;
178
+ },
179
+ });
180
+
181
+ return new Response(stream, {
182
+ status: 200,
183
+ headers: {
184
+ "content-type": "text/plain; charset=utf-8",
185
+ },
186
+ });
187
+ }
188
+
189
+ case "/gzip": {
190
+ const payload = gzipSync("This is compressed content!");
191
+
192
+ return new Response(payload, {
193
+ status: 200,
194
+ headers: {
195
+ "content-type": "text/plain; charset=utf-8",
196
+ "content-encoding": "gzip",
197
+ "content-length": String(payload.byteLength),
198
+ },
199
+ });
200
+ }
201
+
202
+ case "/bad-gzip": {
203
+ const payload = encoder.encode("not actually gzip");
204
+
205
+ return new Response(payload, {
206
+ status: 200,
207
+ headers: {
208
+ "content-type": "text/plain; charset=utf-8",
209
+ "content-encoding": "gzip",
210
+ "content-length": String(payload.byteLength),
211
+ },
212
+ });
213
+ }
214
+
215
+ case "/large": {
216
+ const body = "x".repeat(1024);
217
+
218
+ return textResponse(body, {
219
+ headers: {
220
+ "content-length": String(
221
+ encoder.encode(body).byteLength,
222
+ ),
223
+ },
224
+ });
225
+ }
226
+
227
+ case "/large-stream": {
228
+ let remaining = 32;
229
+
230
+ const stream = new ReadableStream<Uint8Array>({
231
+ pull(controller) {
232
+ if (remaining === 0) {
233
+ controller.close();
234
+ return;
235
+ }
236
+
237
+ controller.enqueue(encoder.encode("x".repeat(64)));
238
+ remaining -= 1;
239
+ },
240
+ });
241
+
242
+ return new Response(stream, {
243
+ status: 200,
244
+ headers: {
245
+ "content-type": "text/plain; charset=utf-8",
246
+ },
247
+ });
248
+ }
249
+
250
+ case "/huge-header": {
251
+ return new Response("ok", {
252
+ status: 200,
253
+ headers: {
254
+ "content-type": "text/plain; charset=utf-8",
255
+ "x-huge": "a".repeat(4096),
256
+ },
257
+ });
258
+ }
259
+
260
+ case "/redirect": {
261
+ return textResponse("Redirecting to /redirected-target", {
262
+ status: 302,
263
+ headers: {
264
+ location: "/redirected-target",
265
+ },
266
+ });
267
+ }
268
+
269
+ case "/redirected-target": {
270
+ return textResponse("You followed the redirect");
271
+ }
272
+
273
+ case "/multipart-echo": {
274
+ let fields: Record<string, unknown>;
275
+
276
+ try {
277
+ const formData = await request.formData();
278
+ fields = {};
279
+
280
+ for (const [key, value] of formData.entries()) {
281
+ const v = value as unknown;
282
+
283
+ if (v instanceof Blob) {
284
+ fields[key] = {
285
+ type: "file",
286
+ filename:
287
+ "name" in v
288
+ ? String((v as any).name)
289
+ : "blob",
290
+ mimeType: (v as Blob).type,
291
+ size: (v as Blob).size,
292
+ content: await (v as Blob).text(),
293
+ };
294
+ } else {
295
+ const existing = fields[key];
296
+ fields[key] =
297
+ existing !== undefined
298
+ ? [
299
+ ...(Array.isArray(existing)
300
+ ? existing
301
+ : [existing]),
302
+ value,
303
+ ]
304
+ : value;
305
+ }
306
+ }
307
+ } catch (err) {
308
+ return json({ error: String(err) }, { status: 400 });
309
+ }
310
+
311
+ return json({
312
+ method: request.method,
313
+ contentType: request.headers.get("content-type"),
314
+ contentLength: request.headers.get("content-length")
315
+ ? Number(request.headers.get("content-length"))
316
+ : null,
317
+ fields,
318
+ });
319
+ }
320
+
321
+ default: {
322
+ return textResponse("Not Found", { status: 404 });
323
+ }
324
+ }
325
+ },
326
+ });
327
+
328
+ return {
329
+ server,
330
+ baseUrl: `http://${server.hostname}:${server.port}`,
331
+ async stop() {
332
+ await server.stop(true);
333
+ },
334
+ };
335
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "isolatedDeclarations": false
5
+ },
6
+ "include": ["src/**/*.ts"],
7
+ "exclude": [
8
+ "tests",
9
+ "examples",
10
+ "benchmarks",
11
+ "**/*.test.ts",
12
+ "**/*.test-utils.ts",
13
+ "**/__fixtures__/**"
14
+ ]
15
+ }