@ricsam/isolate-fetch 0.0.1 → 0.1.1
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 +9 -0
- package/package.json +27 -7
- package/src/debug-delayed.test.ts +89 -0
- package/src/debug-streaming.test.ts +81 -0
- package/src/download-streaming-simple.test.ts +167 -0
- package/src/download-streaming.test.ts +286 -0
- package/src/form-data.test.ts +824 -0
- package/src/formdata.test.ts +212 -0
- package/src/headers.test.ts +582 -0
- package/src/host-backed-stream.test.ts +363 -0
- package/src/index.test.ts +274 -0
- package/src/index.ts +2325 -0
- package/src/integration.test.ts +665 -0
- package/src/request.test.ts +482 -0
- package/src/response.test.ts +520 -0
- package/src/serve.test.ts +425 -0
- package/src/stream-state.test.ts +338 -0
- package/src/stream-state.ts +337 -0
- package/src/upload-streaming.test.ts +373 -0
- package/src/websocket.test.ts +627 -0
- package/tsconfig.json +8 -0
- package/README.md +0 -45
|
@@ -0,0 +1,665 @@
|
|
|
1
|
+
import { test, describe, beforeEach, afterEach } from "node:test";
|
|
2
|
+
import assert from "node:assert";
|
|
3
|
+
import ivm from "isolated-vm";
|
|
4
|
+
import { setupFetch, clearAllInstanceState, type FetchHandle } from "./index.ts";
|
|
5
|
+
|
|
6
|
+
describe("Request Body Consumption", () => {
|
|
7
|
+
let isolate: ivm.Isolate;
|
|
8
|
+
let context: ivm.Context;
|
|
9
|
+
let fetchHandle: FetchHandle;
|
|
10
|
+
|
|
11
|
+
beforeEach(async () => {
|
|
12
|
+
isolate = new ivm.Isolate();
|
|
13
|
+
context = await isolate.createContext();
|
|
14
|
+
clearAllInstanceState();
|
|
15
|
+
fetchHandle = await setupFetch(context);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
fetchHandle.dispose();
|
|
20
|
+
context.release();
|
|
21
|
+
isolate.dispose();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("accessing request.body before request.json() should not lose body data", async () => {
|
|
25
|
+
context.evalSync(`
|
|
26
|
+
serve({
|
|
27
|
+
async fetch(request) {
|
|
28
|
+
// This pattern is used by frameworks like Better Auth:
|
|
29
|
+
// First access request.body (e.g., to check if body exists)
|
|
30
|
+
const bodyStream = request.body; // Getter is called
|
|
31
|
+
|
|
32
|
+
// Then try to parse the JSON body
|
|
33
|
+
try {
|
|
34
|
+
const data = await request.json();
|
|
35
|
+
return Response.json({ success: true, received: data });
|
|
36
|
+
} catch (error) {
|
|
37
|
+
return Response.json({
|
|
38
|
+
success: false,
|
|
39
|
+
error: error.message,
|
|
40
|
+
bodyWasNull: bodyStream === null
|
|
41
|
+
}, { status: 500 });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
`);
|
|
46
|
+
|
|
47
|
+
const response = await fetchHandle.dispatchRequest(
|
|
48
|
+
new Request("http://localhost/api/test", {
|
|
49
|
+
method: "POST",
|
|
50
|
+
headers: { "Content-Type": "application/json" },
|
|
51
|
+
body: JSON.stringify({ email: "test@example.com" }),
|
|
52
|
+
})
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const data = await response.json();
|
|
56
|
+
assert.strictEqual(data.success, true);
|
|
57
|
+
assert.deepStrictEqual(data.received, { email: "test@example.com" });
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("accessing request.body getter multiple times should return consistent stream", async () => {
|
|
61
|
+
context.evalSync(`
|
|
62
|
+
serve({
|
|
63
|
+
async fetch(request) {
|
|
64
|
+
// Access body getter multiple times
|
|
65
|
+
const body1 = request.body;
|
|
66
|
+
const body2 = request.body;
|
|
67
|
+
|
|
68
|
+
// Both should reference the same stream
|
|
69
|
+
const areSame = body1 === body2;
|
|
70
|
+
|
|
71
|
+
// Should still be able to read the body
|
|
72
|
+
try {
|
|
73
|
+
const text = await request.text();
|
|
74
|
+
return Response.json({
|
|
75
|
+
success: true,
|
|
76
|
+
bodiesAreSame: areSame,
|
|
77
|
+
bodyText: text
|
|
78
|
+
});
|
|
79
|
+
} catch (error) {
|
|
80
|
+
return Response.json({
|
|
81
|
+
success: false,
|
|
82
|
+
error: error.message,
|
|
83
|
+
bodiesAreSame: areSame
|
|
84
|
+
}, { status: 500 });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
`);
|
|
89
|
+
|
|
90
|
+
const response = await fetchHandle.dispatchRequest(
|
|
91
|
+
new Request("http://localhost/api/test", {
|
|
92
|
+
method: "POST",
|
|
93
|
+
headers: { "Content-Type": "text/plain" },
|
|
94
|
+
body: "Hello World",
|
|
95
|
+
})
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
const data = await response.json();
|
|
99
|
+
assert.strictEqual(data.success, true);
|
|
100
|
+
assert.strictEqual(data.bodiesAreSame, true);
|
|
101
|
+
assert.strictEqual(data.bodyText, "Hello World");
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe("HTTP Roundtrip", () => {
|
|
106
|
+
let isolate: ivm.Isolate;
|
|
107
|
+
let context: ivm.Context;
|
|
108
|
+
let fetchHandle: FetchHandle;
|
|
109
|
+
|
|
110
|
+
beforeEach(async () => {
|
|
111
|
+
isolate = new ivm.Isolate();
|
|
112
|
+
context = await isolate.createContext();
|
|
113
|
+
clearAllInstanceState();
|
|
114
|
+
fetchHandle = await setupFetch(context);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
afterEach(() => {
|
|
118
|
+
fetchHandle.dispose();
|
|
119
|
+
context.release();
|
|
120
|
+
isolate.dispose();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("GET request returns correct response body", async () => {
|
|
124
|
+
context.evalSync(`
|
|
125
|
+
serve({
|
|
126
|
+
fetch(request) {
|
|
127
|
+
return new Response("Hello from isolate!");
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
`);
|
|
131
|
+
|
|
132
|
+
const response = await fetchHandle.dispatchRequest(
|
|
133
|
+
new Request("http://localhost/test")
|
|
134
|
+
);
|
|
135
|
+
assert.strictEqual(response.status, 200);
|
|
136
|
+
assert.strictEqual(await response.text(), "Hello from isolate!");
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("POST request with JSON body is received correctly", async () => {
|
|
140
|
+
context.evalSync(`
|
|
141
|
+
serve({
|
|
142
|
+
async fetch(request) {
|
|
143
|
+
const body = await request.json();
|
|
144
|
+
return Response.json({ received: body });
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
`);
|
|
148
|
+
|
|
149
|
+
const response = await fetchHandle.dispatchRequest(
|
|
150
|
+
new Request("http://localhost/api/data", {
|
|
151
|
+
method: "POST",
|
|
152
|
+
headers: { "Content-Type": "application/json" },
|
|
153
|
+
body: JSON.stringify({ name: "test", value: 42 }),
|
|
154
|
+
})
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
assert.strictEqual(response.status, 200);
|
|
158
|
+
const data = await response.json();
|
|
159
|
+
assert.deepStrictEqual(data.received, { name: "test", value: 42 });
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("Response headers are preserved", async () => {
|
|
163
|
+
context.evalSync(`
|
|
164
|
+
serve({
|
|
165
|
+
fetch(request) {
|
|
166
|
+
return new Response("OK", {
|
|
167
|
+
headers: {
|
|
168
|
+
"X-Custom-Header": "custom-value",
|
|
169
|
+
"X-Another-Header": "another-value"
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
`);
|
|
175
|
+
|
|
176
|
+
const response = await fetchHandle.dispatchRequest(
|
|
177
|
+
new Request("http://localhost/")
|
|
178
|
+
);
|
|
179
|
+
assert.strictEqual(response.headers.get("X-Custom-Header"), "custom-value");
|
|
180
|
+
assert.strictEqual(response.headers.get("X-Another-Header"), "another-value");
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test("Response status codes work correctly", async () => {
|
|
184
|
+
context.evalSync(`
|
|
185
|
+
serve({
|
|
186
|
+
fetch(request) {
|
|
187
|
+
const url = new URL(request.url);
|
|
188
|
+
const status = parseInt(url.searchParams.get("status") || "200", 10);
|
|
189
|
+
return new Response("Status test", { status });
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
`);
|
|
193
|
+
|
|
194
|
+
const ok = await fetchHandle.dispatchRequest(
|
|
195
|
+
new Request("http://localhost/?status=200")
|
|
196
|
+
);
|
|
197
|
+
assert.strictEqual(ok.status, 200);
|
|
198
|
+
|
|
199
|
+
const notFound = await fetchHandle.dispatchRequest(
|
|
200
|
+
new Request("http://localhost/?status=404")
|
|
201
|
+
);
|
|
202
|
+
assert.strictEqual(notFound.status, 404);
|
|
203
|
+
|
|
204
|
+
const serverError = await fetchHandle.dispatchRequest(
|
|
205
|
+
new Request("http://localhost/?status=500")
|
|
206
|
+
);
|
|
207
|
+
assert.strictEqual(serverError.status, 500);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test("JSON response via Response.json()", async () => {
|
|
211
|
+
context.evalSync(`
|
|
212
|
+
serve({
|
|
213
|
+
fetch(request) {
|
|
214
|
+
return Response.json({
|
|
215
|
+
message: "Hello",
|
|
216
|
+
items: [1, 2, 3],
|
|
217
|
+
nested: { foo: "bar" }
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
`);
|
|
222
|
+
|
|
223
|
+
const response = await fetchHandle.dispatchRequest(
|
|
224
|
+
new Request("http://localhost/")
|
|
225
|
+
);
|
|
226
|
+
assert.ok(response.headers.get("Content-Type")?.includes("application/json"));
|
|
227
|
+
const data = await response.json();
|
|
228
|
+
assert.deepStrictEqual(data, {
|
|
229
|
+
message: "Hello",
|
|
230
|
+
items: [1, 2, 3],
|
|
231
|
+
nested: { foo: "bar" },
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
test("Request URL and method are accessible", async () => {
|
|
236
|
+
context.evalSync(`
|
|
237
|
+
serve({
|
|
238
|
+
fetch(request) {
|
|
239
|
+
return Response.json({
|
|
240
|
+
method: request.method,
|
|
241
|
+
url: request.url
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
`);
|
|
246
|
+
|
|
247
|
+
const response = await fetchHandle.dispatchRequest(
|
|
248
|
+
new Request("http://localhost/api/test?foo=bar", { method: "PUT" })
|
|
249
|
+
);
|
|
250
|
+
const data = await response.json();
|
|
251
|
+
assert.strictEqual(data.method, "PUT");
|
|
252
|
+
assert.ok(data.url.includes("/api/test?foo=bar"));
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
test("Request headers are accessible", async () => {
|
|
256
|
+
context.evalSync(`
|
|
257
|
+
serve({
|
|
258
|
+
fetch(request) {
|
|
259
|
+
return Response.json({
|
|
260
|
+
auth: request.headers.get("Authorization"),
|
|
261
|
+
custom: request.headers.get("X-Custom")
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
`);
|
|
266
|
+
|
|
267
|
+
const response = await fetchHandle.dispatchRequest(
|
|
268
|
+
new Request("http://localhost/", {
|
|
269
|
+
headers: {
|
|
270
|
+
Authorization: "Bearer token123",
|
|
271
|
+
"X-Custom": "custom-value",
|
|
272
|
+
},
|
|
273
|
+
})
|
|
274
|
+
);
|
|
275
|
+
const data = await response.json();
|
|
276
|
+
assert.strictEqual(data.auth, "Bearer token123");
|
|
277
|
+
assert.strictEqual(data.custom, "custom-value");
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
test("Large response body can be read", async () => {
|
|
281
|
+
context.evalSync(`
|
|
282
|
+
serve({
|
|
283
|
+
fetch(request) {
|
|
284
|
+
// Generate a ~10KB response
|
|
285
|
+
const chunk = "0123456789".repeat(100);
|
|
286
|
+
const body = chunk.repeat(10);
|
|
287
|
+
return new Response(body);
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
`);
|
|
291
|
+
|
|
292
|
+
const response = await fetchHandle.dispatchRequest(
|
|
293
|
+
new Request("http://localhost/")
|
|
294
|
+
);
|
|
295
|
+
const text = await response.text();
|
|
296
|
+
assert.strictEqual(text.length, 10000);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
test("Request with text body is forwarded to handler", async () => {
|
|
300
|
+
context.evalSync(`
|
|
301
|
+
serve({
|
|
302
|
+
async fetch(request) {
|
|
303
|
+
const body = await request.text();
|
|
304
|
+
return new Response("Received: " + body.length + " chars");
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
`);
|
|
308
|
+
|
|
309
|
+
const largeBody = "x".repeat(5000);
|
|
310
|
+
const response = await fetchHandle.dispatchRequest(
|
|
311
|
+
new Request("http://localhost/", {
|
|
312
|
+
method: "POST",
|
|
313
|
+
body: largeBody,
|
|
314
|
+
})
|
|
315
|
+
);
|
|
316
|
+
const text = await response.text();
|
|
317
|
+
assert.strictEqual(text, "Received: 5000 chars");
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
describe("Response Clone", () => {
|
|
322
|
+
let isolate: ivm.Isolate;
|
|
323
|
+
let context: ivm.Context;
|
|
324
|
+
let fetchHandle: FetchHandle;
|
|
325
|
+
|
|
326
|
+
beforeEach(async () => {
|
|
327
|
+
isolate = new ivm.Isolate();
|
|
328
|
+
context = await isolate.createContext();
|
|
329
|
+
clearAllInstanceState();
|
|
330
|
+
fetchHandle = await setupFetch(context);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
afterEach(() => {
|
|
334
|
+
fetchHandle.dispose();
|
|
335
|
+
context.release();
|
|
336
|
+
isolate.dispose();
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
test("response.clone() preserves headers including cookies", async () => {
|
|
340
|
+
context.evalSync(`
|
|
341
|
+
serve({
|
|
342
|
+
async fetch(request) {
|
|
343
|
+
// Simulate auth handler that adds a cookie
|
|
344
|
+
function authHandler(req) {
|
|
345
|
+
const response = new Response(JSON.stringify({ authenticated: true }), {
|
|
346
|
+
status: 200,
|
|
347
|
+
headers: {
|
|
348
|
+
"Content-Type": "application/json",
|
|
349
|
+
"Set-Cookie": "session=abc123; HttpOnly; Secure"
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
return response;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const response = await authHandler(request);
|
|
356
|
+
const clone = response.clone();
|
|
357
|
+
|
|
358
|
+
// Read the clone body
|
|
359
|
+
const cloneBody = await clone.text();
|
|
360
|
+
|
|
361
|
+
// Return original response
|
|
362
|
+
return response;
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
`);
|
|
366
|
+
|
|
367
|
+
const response = await fetchHandle.dispatchRequest(
|
|
368
|
+
new Request("http://localhost/test")
|
|
369
|
+
);
|
|
370
|
+
assert.strictEqual(response.status, 200);
|
|
371
|
+
assert.strictEqual(response.headers.get("Set-Cookie"), "session=abc123; HttpOnly; Secure");
|
|
372
|
+
});
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
describe("Headers instanceof and constructor behavior (better-auth/better-call compatibility)", () => {
|
|
376
|
+
let isolate: ivm.Isolate;
|
|
377
|
+
let context: ivm.Context;
|
|
378
|
+
let fetchHandle: FetchHandle;
|
|
379
|
+
|
|
380
|
+
beforeEach(async () => {
|
|
381
|
+
isolate = new ivm.Isolate();
|
|
382
|
+
context = await isolate.createContext();
|
|
383
|
+
clearAllInstanceState();
|
|
384
|
+
fetchHandle = await setupFetch(context);
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
afterEach(() => {
|
|
388
|
+
fetchHandle.dispose();
|
|
389
|
+
context.release();
|
|
390
|
+
isolate.dispose();
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
test("request.headers should work with instanceof check", async () => {
|
|
394
|
+
context.evalSync(`
|
|
395
|
+
serve({
|
|
396
|
+
async fetch(request) {
|
|
397
|
+
const cookie = request.headers.get("cookie");
|
|
398
|
+
const instanceofHeaders = request.headers instanceof Headers;
|
|
399
|
+
const constructorName = request.headers.constructor.name;
|
|
400
|
+
|
|
401
|
+
return Response.json({
|
|
402
|
+
cookie,
|
|
403
|
+
instanceofHeaders,
|
|
404
|
+
constructorName
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
});
|
|
408
|
+
`);
|
|
409
|
+
|
|
410
|
+
const response = await fetchHandle.dispatchRequest(
|
|
411
|
+
new Request("http://localhost/test", {
|
|
412
|
+
headers: {
|
|
413
|
+
cookie: "session=abc123; other=value",
|
|
414
|
+
},
|
|
415
|
+
})
|
|
416
|
+
);
|
|
417
|
+
|
|
418
|
+
const data = await response.json();
|
|
419
|
+
assert.strictEqual(data.cookie, "session=abc123; other=value");
|
|
420
|
+
assert.strictEqual(data.instanceofHeaders, true);
|
|
421
|
+
assert.strictEqual(data.constructorName, "Headers");
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
test("new Headers(request.headers) should preserve cookies", async () => {
|
|
425
|
+
context.evalSync(`
|
|
426
|
+
serve({
|
|
427
|
+
async fetch(request) {
|
|
428
|
+
const originalCookie = request.headers.get("cookie");
|
|
429
|
+
|
|
430
|
+
// This is what better-call does internally
|
|
431
|
+
const copiedHeaders = new Headers(request.headers);
|
|
432
|
+
const copiedCookie = copiedHeaders.get("cookie");
|
|
433
|
+
|
|
434
|
+
return Response.json({
|
|
435
|
+
originalCookie,
|
|
436
|
+
copiedCookie,
|
|
437
|
+
cookiesMatch: originalCookie === copiedCookie
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
`);
|
|
442
|
+
|
|
443
|
+
const response = await fetchHandle.dispatchRequest(
|
|
444
|
+
new Request("http://localhost/test", {
|
|
445
|
+
headers: {
|
|
446
|
+
cookie: "session=abc123; token=xyz",
|
|
447
|
+
},
|
|
448
|
+
})
|
|
449
|
+
);
|
|
450
|
+
|
|
451
|
+
const data = await response.json();
|
|
452
|
+
assert.strictEqual(data.originalCookie, "session=abc123; token=xyz");
|
|
453
|
+
assert.strictEqual(data.copiedCookie, "session=abc123; token=xyz");
|
|
454
|
+
assert.strictEqual(data.cookiesMatch, true);
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
test("headers passed to nested function should preserve instanceof behavior", async () => {
|
|
458
|
+
context.evalSync(`
|
|
459
|
+
serve({
|
|
460
|
+
async fetch(request) {
|
|
461
|
+
// Simulate what better-auth does: pass headers to a nested function
|
|
462
|
+
function processContext(context) {
|
|
463
|
+
const headers = context.headers;
|
|
464
|
+
return {
|
|
465
|
+
hasCookie: headers.has("cookie"),
|
|
466
|
+
getCookie: headers.get("cookie"),
|
|
467
|
+
instanceofHeaders: headers instanceof Headers,
|
|
468
|
+
constructorName: headers.constructor.name,
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const result = processContext({ headers: request.headers });
|
|
473
|
+
|
|
474
|
+
return Response.json(result);
|
|
475
|
+
}
|
|
476
|
+
});
|
|
477
|
+
`);
|
|
478
|
+
|
|
479
|
+
const response = await fetchHandle.dispatchRequest(
|
|
480
|
+
new Request("http://localhost/test", {
|
|
481
|
+
headers: {
|
|
482
|
+
cookie: "better-auth.session_token=abc123",
|
|
483
|
+
},
|
|
484
|
+
})
|
|
485
|
+
);
|
|
486
|
+
|
|
487
|
+
const data = await response.json();
|
|
488
|
+
assert.strictEqual(data.hasCookie, true);
|
|
489
|
+
assert.strictEqual(data.getCookie, "better-auth.session_token=abc123");
|
|
490
|
+
assert.strictEqual(data.instanceofHeaders, true);
|
|
491
|
+
assert.strictEqual(data.constructorName, "Headers");
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
test("better-call createInternalContext pattern should preserve cookies", async () => {
|
|
495
|
+
context.evalSync(`
|
|
496
|
+
serve({
|
|
497
|
+
async fetch(request) {
|
|
498
|
+
// Simulate the createInternalContext pattern from better-call
|
|
499
|
+
function createInternalContext(context) {
|
|
500
|
+
const isHeadersLike = (obj) => obj && typeof obj.get === "function" && typeof obj.has === "function";
|
|
501
|
+
|
|
502
|
+
let requestHeaders = null;
|
|
503
|
+
|
|
504
|
+
if ("headers" in context && context.headers) {
|
|
505
|
+
if (isHeadersLike(context.headers)) {
|
|
506
|
+
requestHeaders = context.headers;
|
|
507
|
+
} else if (context.headers instanceof Headers) {
|
|
508
|
+
requestHeaders = context.headers;
|
|
509
|
+
} else {
|
|
510
|
+
try {
|
|
511
|
+
requestHeaders = new Headers(context.headers);
|
|
512
|
+
} catch (e) {
|
|
513
|
+
// Ignore errors
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
return {
|
|
519
|
+
requestHeadersType: requestHeaders?.constructor?.name,
|
|
520
|
+
hasCookie: requestHeaders?.has?.("cookie"),
|
|
521
|
+
getCookie: requestHeaders?.get?.("cookie"),
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// Call like better-auth does: auth.api.getSession({ headers: request.headers })
|
|
526
|
+
const result = createInternalContext({ headers: request.headers });
|
|
527
|
+
|
|
528
|
+
return Response.json(result);
|
|
529
|
+
}
|
|
530
|
+
});
|
|
531
|
+
`);
|
|
532
|
+
|
|
533
|
+
const response = await fetchHandle.dispatchRequest(
|
|
534
|
+
new Request("http://localhost/test", {
|
|
535
|
+
headers: {
|
|
536
|
+
cookie: "better-auth.session_token=abc123.signature",
|
|
537
|
+
},
|
|
538
|
+
})
|
|
539
|
+
);
|
|
540
|
+
|
|
541
|
+
const data = await response.json();
|
|
542
|
+
assert.strictEqual(data.requestHeadersType, "Headers");
|
|
543
|
+
assert.strictEqual(data.hasCookie, true);
|
|
544
|
+
assert.strictEqual(data.getCookie, "better-auth.session_token=abc123.signature");
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
test("headers.entries() should iterate all headers including cookies", async () => {
|
|
548
|
+
context.evalSync(`
|
|
549
|
+
serve({
|
|
550
|
+
async fetch(request) {
|
|
551
|
+
const entries = [];
|
|
552
|
+
for (const [key, value] of request.headers.entries()) {
|
|
553
|
+
entries.push({ key, value });
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
const cookieEntry = entries.find(e => e.key === "cookie");
|
|
557
|
+
|
|
558
|
+
return Response.json({
|
|
559
|
+
entriesCount: entries.length,
|
|
560
|
+
hasCookieEntry: !!cookieEntry,
|
|
561
|
+
cookieValue: cookieEntry?.value,
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
});
|
|
565
|
+
`);
|
|
566
|
+
|
|
567
|
+
const response = await fetchHandle.dispatchRequest(
|
|
568
|
+
new Request("http://localhost/test", {
|
|
569
|
+
headers: {
|
|
570
|
+
cookie: "session=test123",
|
|
571
|
+
"content-type": "application/json",
|
|
572
|
+
},
|
|
573
|
+
})
|
|
574
|
+
);
|
|
575
|
+
|
|
576
|
+
const data = await response.json();
|
|
577
|
+
assert.strictEqual(data.hasCookieEntry, true);
|
|
578
|
+
assert.strictEqual(data.cookieValue, "session=test123");
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
test("async function receiving headers should preserve cookie access", async () => {
|
|
582
|
+
context.evalSync(`
|
|
583
|
+
serve({
|
|
584
|
+
async fetch(request) {
|
|
585
|
+
async function getSession(options) {
|
|
586
|
+
// Simulate async processing like better-auth does
|
|
587
|
+
await Promise.resolve();
|
|
588
|
+
|
|
589
|
+
const headers = options.headers;
|
|
590
|
+
return {
|
|
591
|
+
hasCookie: headers.has("cookie"),
|
|
592
|
+
cookieValue: headers.get("cookie"),
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
const session = await getSession({ headers: request.headers });
|
|
597
|
+
|
|
598
|
+
return Response.json(session);
|
|
599
|
+
}
|
|
600
|
+
});
|
|
601
|
+
`);
|
|
602
|
+
|
|
603
|
+
const response = await fetchHandle.dispatchRequest(
|
|
604
|
+
new Request("http://localhost/test", {
|
|
605
|
+
headers: {
|
|
606
|
+
cookie: "auth_token=secret123",
|
|
607
|
+
},
|
|
608
|
+
})
|
|
609
|
+
);
|
|
610
|
+
|
|
611
|
+
const data = await response.json();
|
|
612
|
+
assert.strictEqual(data.hasCookie, true);
|
|
613
|
+
assert.strictEqual(data.cookieValue, "auth_token=secret123");
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
test("multiple async hops should preserve headers", async () => {
|
|
617
|
+
context.evalSync(`
|
|
618
|
+
serve({
|
|
619
|
+
async fetch(request) {
|
|
620
|
+
// Level 1: Router context
|
|
621
|
+
async function routerContext(req) {
|
|
622
|
+
return await authApi({ headers: req.headers });
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// Level 2: Auth API
|
|
626
|
+
async function authApi(options) {
|
|
627
|
+
await Promise.resolve();
|
|
628
|
+
return await createInternalContext(options);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// Level 3: Internal context (like better-call)
|
|
632
|
+
async function createInternalContext(context) {
|
|
633
|
+
await Promise.resolve();
|
|
634
|
+
const headers = context.headers;
|
|
635
|
+
|
|
636
|
+
return {
|
|
637
|
+
level: "createInternalContext",
|
|
638
|
+
hasCookie: headers?.has?.("cookie") ?? false,
|
|
639
|
+
getCookie: headers?.get?.("cookie") ?? null,
|
|
640
|
+
constructorName: headers?.constructor?.name,
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
const result = await routerContext(request);
|
|
645
|
+
|
|
646
|
+
return Response.json(result);
|
|
647
|
+
}
|
|
648
|
+
});
|
|
649
|
+
`);
|
|
650
|
+
|
|
651
|
+
const response = await fetchHandle.dispatchRequest(
|
|
652
|
+
new Request("http://localhost/test", {
|
|
653
|
+
headers: {
|
|
654
|
+
cookie: "session_token=deep_test_123",
|
|
655
|
+
},
|
|
656
|
+
})
|
|
657
|
+
);
|
|
658
|
+
|
|
659
|
+
const data = await response.json();
|
|
660
|
+
assert.strictEqual(data.level, "createInternalContext");
|
|
661
|
+
assert.strictEqual(data.hasCookie, true);
|
|
662
|
+
assert.strictEqual(data.getCookie, "session_token=deep_test_123");
|
|
663
|
+
assert.strictEqual(data.constructorName, "Headers");
|
|
664
|
+
});
|
|
665
|
+
});
|