@sproutboat/runtime 0.19.1 → 0.20.0
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 +70 -0
- package/WEB_STREAMS.md +4 -2
- package/package.json +1 -1
- package/src/native-fetch-prelude.js +273 -69
- package/src/transport-broker.js +566 -56
- package/src/transport-embedded.js +4 -0
- package/test/native-bodies.ts +1 -1
- package/test/native-request.ts +5 -4
- package/test/native-service-bindings.ts +346 -0
- package/test/native-utf8.ts +3 -3
|
@@ -2082,6 +2082,10 @@ function __sbHttpAsyncResult(asyncId) {
|
|
|
2082
2082
|
return res;
|
|
2083
2083
|
}
|
|
2084
2084
|
|
|
2085
|
+
// An async request's caller gave up. The host's request runs to the end; with its
|
|
2086
|
+
// placeholder timer cleared, the reply is dropped when it arrives.
|
|
2087
|
+
function __sbCancelAsync() {}
|
|
2088
|
+
|
|
2085
2089
|
// Whether the host runs async requests (sb_async_enable was called).
|
|
2086
2090
|
function __sbHttpAsyncEnabled() {
|
|
2087
2091
|
let res = false;
|
package/test/native-bodies.ts
CHANGED
|
@@ -92,7 +92,7 @@ try {
|
|
|
92
92
|
const prelude = core.replace(TRANSPORT_MARKER, broker);
|
|
93
93
|
const generated = join(workdir, "bodies.generated.js");
|
|
94
94
|
const binary = join(workdir, "bodies-native");
|
|
95
|
-
await writeFile(generated, wrapNativeFetchHandler(handler, prelude, undefined, undefined, undefined, "2026-10-
|
|
95
|
+
await writeFile(generated, wrapNativeFetchHandler(handler, prelude, undefined, undefined, undefined, "2026-10-21"));
|
|
96
96
|
await rm(binary, { force: true });
|
|
97
97
|
|
|
98
98
|
const compile = Bun.spawn([node, resolve(porffor, "runtime/index.js"), "native", generated, "-o", binary, "-s", "-O0"], {
|
package/test/native-request.ts
CHANGED
|
@@ -19,8 +19,9 @@ const CASES = [
|
|
|
19
19
|
`new Request("http://a.test/x", { method: "post" }).method`,
|
|
20
20
|
];
|
|
21
21
|
const handler = `export default {async fetch(request) {
|
|
22
|
-
//
|
|
23
|
-
|
|
22
|
+
// Before compatibility date 2026-10-21 (this build has the baseline date) the incoming body is
|
|
23
|
+
// the raw value, as apps written against earlier releases read it; text() decodes it.
|
|
24
|
+
if (request.method === "POST") return new Response((request.body instanceof ReadableStream) + ":" + (typeof request.body) + ":" + (await request.text()));
|
|
24
25
|
const cases = [
|
|
25
26
|
${CASES.map((c) => ` () => (${c}),`).join("\n")}
|
|
26
27
|
];
|
|
@@ -94,8 +95,8 @@ try {
|
|
|
94
95
|
const diffs = CASES.flatMap((c, i) => (got[i] === want[i] ? [] : [`${c}\n want ${JSON.stringify(want[i])}\n got ${JSON.stringify(got[i])}`]));
|
|
95
96
|
assert.deepEqual(diffs, [], `Request copies differ from Bun in ${diffs.length} of ${CASES.length} cases:\n${diffs.join("\n")}`);
|
|
96
97
|
const posted = await (await fetch(`http://127.0.0.1:${port}/`, { method: "POST", body: "posted é" })).text();
|
|
97
|
-
assert.equal(posted, "
|
|
98
|
-
console.log(`Native Request copies match Bun in all ${CASES.length} cases;
|
|
98
|
+
assert.equal(posted, "false:string:posted é", "before 2026-10-21 the incoming body stays the raw value");
|
|
99
|
+
console.log(`Native Request copies match Bun in all ${CASES.length} cases; an app before 2026-10-21 keeps its raw body`);
|
|
99
100
|
} finally {
|
|
100
101
|
if (server) {
|
|
101
102
|
server.kill(9);
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join, resolve } from "node:path";
|
|
5
|
+
import { ensurePorffor } from "../../toolchain/src/acquire";
|
|
6
|
+
import { createBroker, listen } from "../../wire/src/broker";
|
|
7
|
+
import { EMPTY_BINDINGS, preludePath, TRANSPORT_MARKER, transportPath, wrapNativeFetchHandler } from "../src/wrap";
|
|
8
|
+
|
|
9
|
+
// Service bindings on a standalone sprout. env.PEER.fetch() returns a promise and runs
|
|
10
|
+
// off the event loop (the broker transport's async threads), so the sprout keeps
|
|
11
|
+
// serving while a call is in flight: other requests answer at once, concurrent calls
|
|
12
|
+
// overlap, and a deployment that calls itself through a binding does not deadlock.
|
|
13
|
+
// Requests read like Fetch's: a URL object, a Request plus init, a stream body; every
|
|
14
|
+
// Set-Cookie of the reply survives, and a reply without a content type keeps none.
|
|
15
|
+
// From compatibility date 2026-10-22, redirects are followed as Fetch does. A stand-in
|
|
16
|
+
// edge plays the platform's.
|
|
17
|
+
const handler = `
|
|
18
|
+
export default {
|
|
19
|
+
async fetch(request, env) {
|
|
20
|
+
const url = new URL(request.url);
|
|
21
|
+
if (url.pathname === "/ping") return new Response("pong");
|
|
22
|
+
if (url.pathname === "/requests") {
|
|
23
|
+
const pending = env.PEER.fetch(new URL("https://peer/echo?u=1"));
|
|
24
|
+
const isPromise = pending instanceof Promise;
|
|
25
|
+
const viaUrl = await (await pending).json();
|
|
26
|
+
const viaRequest = await (await env.PEER.fetch(new Request("https://peer/echo", { method: "PUT", headers: { "x-from": "request" }, body: "req body" }), { method: "PATCH" })).json();
|
|
27
|
+
const stream = new ReadableStream({ start(c) { c.enqueue(new TextEncoder().encode("streamed ")); c.enqueue(new TextEncoder().encode("body")); c.close(); } });
|
|
28
|
+
const viaStream = await (await env.PEER.fetch("https://peer/echo", { method: "POST", body: stream, duplex: "half" })).json();
|
|
29
|
+
const cookies = (await env.PEER.fetch("https://peer/cookies")).headers.getSetCookie();
|
|
30
|
+
const bare = (await env.PEER.fetch("https://peer/bare")).headers.get("content-type");
|
|
31
|
+
return Response.json({ isPromise, viaUrl, viaRequest, viaStream, cookies, bare });
|
|
32
|
+
}
|
|
33
|
+
if (url.pathname === "/redirects") {
|
|
34
|
+
const followed = await (await env.PEER.fetch("https://peer/redirect?status=302&to=/echo?hop=1")).json();
|
|
35
|
+
const manual = await env.PEER.fetch("https://peer/redirect?status=302&to=/echo", { redirect: "manual" });
|
|
36
|
+
const post = (status, body) => env.PEER.fetch("https://peer/redirect?status=" + status + "&to=/typed", { method: "POST", body, headers: { "content-type": "text/plain" } });
|
|
37
|
+
const seeOther = await (await post(303, "dropped")).json();
|
|
38
|
+
const temporary = await (await post(307, "kept")).json();
|
|
39
|
+
let refused = "";
|
|
40
|
+
try {
|
|
41
|
+
await env.PEER.fetch("https://peer/redirect?status=302&to=/echo", { redirect: "error" });
|
|
42
|
+
} catch (e) {
|
|
43
|
+
refused = e.name;
|
|
44
|
+
}
|
|
45
|
+
return Response.json({ followed, manual: [manual.status, manual.headers.get("location")], seeOther, temporary, refused });
|
|
46
|
+
}
|
|
47
|
+
if (url.pathname === "/slow") return new Response(await (await env.PEER.fetch("https://peer/slow" + url.search)).text());
|
|
48
|
+
if (url.pathname === "/abort") {
|
|
49
|
+
const outcome = async (signal) => {
|
|
50
|
+
const t0 = Date.now();
|
|
51
|
+
try {
|
|
52
|
+
await env.PEER.fetch("https://peer/slow?ms=3000", { signal });
|
|
53
|
+
return "answered";
|
|
54
|
+
} catch (e) {
|
|
55
|
+
return e.name + " " + (Date.now() - t0 < 1000 ? "fast" : "slow");
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
const controller = new AbortController();
|
|
59
|
+
setTimeout(() => controller.abort(), 100);
|
|
60
|
+
const aborted = await outcome(controller.signal);
|
|
61
|
+
const timedOut = await outcome(AbortSignal.timeout(100));
|
|
62
|
+
const already = await outcome(AbortSignal.abort());
|
|
63
|
+
return Response.json({ aborted, timedOut, already });
|
|
64
|
+
}
|
|
65
|
+
if (url.pathname === "/self") {
|
|
66
|
+
const depth = Number(url.searchParams.get("depth") || 0);
|
|
67
|
+
if (depth >= 30) return new Response("bottom " + depth);
|
|
68
|
+
return new Response(await (await env.SELF.fetch("https://self/self?depth=" + (depth + 1))).text());
|
|
69
|
+
}
|
|
70
|
+
if (url.pathname === "/streamed") {
|
|
71
|
+
// Four chunks 300 ms apart upstream: a streamed body gets the first one early.
|
|
72
|
+
const t0 = Date.now();
|
|
73
|
+
const res = await env.PEER.fetch("https://peer/drip");
|
|
74
|
+
const reader = res.body.getReader();
|
|
75
|
+
let firstMs = -1;
|
|
76
|
+
let text = "";
|
|
77
|
+
for (;;) {
|
|
78
|
+
const { value, done } = await reader.read();
|
|
79
|
+
if (done) break;
|
|
80
|
+
if (firstMs < 0) firstMs = Date.now() - t0;
|
|
81
|
+
text += new TextDecoder().decode(value);
|
|
82
|
+
}
|
|
83
|
+
const whole = await (await env.PEER.fetch("https://peer/drip")).text();
|
|
84
|
+
const big = new Uint8Array(await (await env.PEER.fetch("https://peer/big")).arrayBuffer());
|
|
85
|
+
let bigOk = big.length === 12 * 1048576;
|
|
86
|
+
for (let i = 0; bigOk && i < big.length; i += 4093) bigOk = big[i] === (i & 255);
|
|
87
|
+
return Response.json({ firstEarly: firstMs >= 0 && firstMs < 250, text, whole, bigOk });
|
|
88
|
+
}
|
|
89
|
+
if (url.pathname === "/whoami") {
|
|
90
|
+
return Response.json({ url: request.url, internal: [...request.headers.keys()].filter((k) => k.startsWith("x-sb-")) });
|
|
91
|
+
}
|
|
92
|
+
if (url.pathname === "/boom") throw new RangeError("kaboom");
|
|
93
|
+
if (url.pathname === "/callself") {
|
|
94
|
+
const who = await (await env.SELF.fetch("https://self.example/whoami?q=1")).json();
|
|
95
|
+
let thrown = null;
|
|
96
|
+
try {
|
|
97
|
+
await env.SELF.fetch("https://self.example/boom");
|
|
98
|
+
} catch (e) {
|
|
99
|
+
thrown = [e.name, e.message, e instanceof RangeError];
|
|
100
|
+
}
|
|
101
|
+
return Response.json({ who, thrown });
|
|
102
|
+
}
|
|
103
|
+
return new Response("no route", { status: 404 });
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
`;
|
|
107
|
+
|
|
108
|
+
function freePort(): number {
|
|
109
|
+
const listener = Bun.listen({ hostname: "127.0.0.1", port: 0, socket: { data() {} } });
|
|
110
|
+
const port = listener.port;
|
|
111
|
+
listener.stop(true);
|
|
112
|
+
return port;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const port = freePort();
|
|
116
|
+
const EDGE_SECRET = "native-services-edge-secret";
|
|
117
|
+
const EDGE_TOKEN = "native-services-edge-token";
|
|
118
|
+
// Upstream requests whose client went away before the answer: a cancelled call must
|
|
119
|
+
// stop here, not only be ignored by the caller.
|
|
120
|
+
let slowAbandoned = 0;
|
|
121
|
+
// The platform's edge, reduced to what the broker forwards to it: routing by Host.
|
|
122
|
+
const edge = Bun.serve({
|
|
123
|
+
hostname: "127.0.0.1",
|
|
124
|
+
port: 0,
|
|
125
|
+
async fetch(request) {
|
|
126
|
+
const url = new URL(request.url);
|
|
127
|
+
const host = request.headers.get("host");
|
|
128
|
+
if (host === "self.test") {
|
|
129
|
+
// Vouch for the call as the platform's edge does: the broker's secret in, the
|
|
130
|
+
// sprout's own token out, and the caller's URL passed along.
|
|
131
|
+
const headers = new Headers(request.headers);
|
|
132
|
+
for (const name of ["x-sb-edge-secret", "x-sb-edge-token", "x-sb-binding-call", "x-sb-url"]) headers.delete(name);
|
|
133
|
+
if (request.headers.get("x-sb-edge-secret") === EDGE_SECRET) {
|
|
134
|
+
headers.set("x-sb-binding-call", "1");
|
|
135
|
+
headers.set("x-sb-edge-token", EDGE_TOKEN);
|
|
136
|
+
const callerUrl = request.headers.get("x-sb-url");
|
|
137
|
+
if (callerUrl) headers.set("x-sb-url", callerUrl);
|
|
138
|
+
}
|
|
139
|
+
return fetch(`http://127.0.0.1:${port}${url.pathname}${url.search}`, { method: request.method, headers });
|
|
140
|
+
}
|
|
141
|
+
if (url.pathname === "/echo") {
|
|
142
|
+
return Response.json({
|
|
143
|
+
method: request.method,
|
|
144
|
+
path: url.pathname + url.search,
|
|
145
|
+
from: request.headers.get("x-from"),
|
|
146
|
+
body: await request.text(),
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
if (url.pathname === "/typed") {
|
|
150
|
+
return Response.json({ method: request.method, type: request.headers.get("content-type"), body: await request.text() });
|
|
151
|
+
}
|
|
152
|
+
if (url.pathname === "/redirect") {
|
|
153
|
+
const status = Number(url.searchParams.get("status"));
|
|
154
|
+
return new Response(null, { status, headers: { location: url.searchParams.get("to") ?? "/" } });
|
|
155
|
+
}
|
|
156
|
+
if (url.pathname === "/drip") {
|
|
157
|
+
const encoder = new TextEncoder();
|
|
158
|
+
return new Response(
|
|
159
|
+
new ReadableStream({
|
|
160
|
+
async start(controller) {
|
|
161
|
+
for (let i = 0; i < 4; i++) {
|
|
162
|
+
if (i) await Bun.sleep(300);
|
|
163
|
+
controller.enqueue(encoder.encode(i + " "));
|
|
164
|
+
}
|
|
165
|
+
controller.close();
|
|
166
|
+
},
|
|
167
|
+
}),
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
if (url.pathname === "/big") {
|
|
171
|
+
const bytes = new Uint8Array(12 * 1048576);
|
|
172
|
+
for (let i = 0; i < bytes.length; i++) bytes[i] = i & 255;
|
|
173
|
+
return new Response(bytes);
|
|
174
|
+
}
|
|
175
|
+
if (url.pathname === "/bare") return new Response(null, { headers: { "x-bare": "1" } });
|
|
176
|
+
if (url.pathname === "/slow") {
|
|
177
|
+
request.signal.addEventListener("abort", () => slowAbandoned++);
|
|
178
|
+
await Bun.sleep(Number(url.searchParams.get("ms") || 500));
|
|
179
|
+
return new Response("slow done");
|
|
180
|
+
}
|
|
181
|
+
if (url.pathname === "/cookies") {
|
|
182
|
+
const headers = new Headers();
|
|
183
|
+
headers.append("set-cookie", "a=1; Path=/");
|
|
184
|
+
headers.append("set-cookie", "b=2; Path=/");
|
|
185
|
+
return new Response("cookies", { headers });
|
|
186
|
+
}
|
|
187
|
+
return new Response("edge: no route", { status: 404 });
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
const workdir = await mkdtemp(join(tmpdir(), "sb-native-services-"));
|
|
192
|
+
const services = [
|
|
193
|
+
{ binding: "PEER", service: "peer" },
|
|
194
|
+
{ binding: "SELF", service: "self" },
|
|
195
|
+
];
|
|
196
|
+
const bindings = { ...EMPTY_BINDINGS, services };
|
|
197
|
+
const broker = createBroker({
|
|
198
|
+
db: ":memory:",
|
|
199
|
+
token: "native-services",
|
|
200
|
+
bindings: { ...bindings, resources: {} },
|
|
201
|
+
edgeUrl: `http://127.0.0.1:${edge.port}/`,
|
|
202
|
+
edgeSecret: EDGE_SECRET,
|
|
203
|
+
services: { PEER: "peer.test", SELF: "self.test" },
|
|
204
|
+
});
|
|
205
|
+
const brokerServer = listen(broker, "127.0.0.1", 0);
|
|
206
|
+
let server: ReturnType<typeof Bun.spawn> | undefined;
|
|
207
|
+
const get = async (path: string) => {
|
|
208
|
+
const t0 = performance.now();
|
|
209
|
+
const res = await fetch(`http://127.0.0.1:${port}${path}`, { signal: AbortSignal.timeout(20_000) });
|
|
210
|
+
return { status: res.status, text: await res.text(), ms: performance.now() - t0 };
|
|
211
|
+
};
|
|
212
|
+
try {
|
|
213
|
+
const porffor = await ensurePorffor({ cacheRoot: workdir });
|
|
214
|
+
const node = Bun.which("node");
|
|
215
|
+
if (!node) throw new Error("native service-binding test needs node on PATH");
|
|
216
|
+
const [core, transport] = await Promise.all([
|
|
217
|
+
readFile(preludePath, "utf8"),
|
|
218
|
+
readFile(transportPath("broker"), "utf8"),
|
|
219
|
+
]);
|
|
220
|
+
assert(core.includes(TRANSPORT_MARKER), "runtime prelude is missing its transport marker");
|
|
221
|
+
const generated = join(workdir, "services.generated.js");
|
|
222
|
+
const binary = join(workdir, "services-native");
|
|
223
|
+
await writeFile(generated, wrapNativeFetchHandler(handler, core.replace(TRANSPORT_MARKER, transport), {}, bindings, undefined, "2026-10-22"));
|
|
224
|
+
|
|
225
|
+
const compile = Bun.spawn([node, resolve(porffor, "runtime/index.js"), "native", generated, "-o", binary, "-s", "-O0"], {
|
|
226
|
+
cwd: workdir,
|
|
227
|
+
env: process.env,
|
|
228
|
+
stdout: "pipe",
|
|
229
|
+
stderr: "pipe",
|
|
230
|
+
});
|
|
231
|
+
const [exitCode, stdout, stderr] = await Promise.all([
|
|
232
|
+
compile.exited,
|
|
233
|
+
new Response(compile.stdout).text(),
|
|
234
|
+
new Response(compile.stderr).text(),
|
|
235
|
+
]);
|
|
236
|
+
assert.equal(exitCode, 0, `Porffor native compile failed:\n${stderr}\n${stdout}`);
|
|
237
|
+
|
|
238
|
+
server = Bun.spawn([binary], {
|
|
239
|
+
cwd: workdir,
|
|
240
|
+
env: {
|
|
241
|
+
...process.env,
|
|
242
|
+
PORT: String(port),
|
|
243
|
+
SB_BROKER_PORT: String(brokerServer.port),
|
|
244
|
+
SB_BROKER_TOKEN: "native-services",
|
|
245
|
+
SB_EDGE_TOKEN: EDGE_TOKEN,
|
|
246
|
+
},
|
|
247
|
+
stdout: "ignore",
|
|
248
|
+
stderr: "inherit",
|
|
249
|
+
});
|
|
250
|
+
let started = false;
|
|
251
|
+
for (let attempt = 0; attempt < 50 && !started; attempt++) {
|
|
252
|
+
try {
|
|
253
|
+
started = (await get("/ping")).text === "pong";
|
|
254
|
+
} catch {
|
|
255
|
+
if (server.exitCode !== null) break;
|
|
256
|
+
await Bun.sleep(100);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
assert(started, "compiled service-binding handler did not start");
|
|
260
|
+
|
|
261
|
+
const requests = await get("/requests");
|
|
262
|
+
assert.equal(requests.status, 200, requests.text);
|
|
263
|
+
assert.deepEqual(JSON.parse(requests.text), {
|
|
264
|
+
isPromise: true,
|
|
265
|
+
viaUrl: { method: "GET", path: "/echo?u=1", from: null, body: "" },
|
|
266
|
+
viaRequest: { method: "PATCH", path: "/echo", from: "request", body: "req body" },
|
|
267
|
+
viaStream: { method: "POST", path: "/echo", from: null, body: "streamed body" },
|
|
268
|
+
cookies: ["a=1; Path=/", "b=2; Path=/"],
|
|
269
|
+
bare: null,
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
// Redirects (compatibility date 2026-10-22): followed through the binding by default,
|
|
273
|
+
// returned under "manual", refused under "error"; a 303 turns a POST into a bodiless GET.
|
|
274
|
+
const redirects = await get("/redirects");
|
|
275
|
+
assert.equal(redirects.status, 200, redirects.text);
|
|
276
|
+
assert.deepEqual(JSON.parse(redirects.text), {
|
|
277
|
+
followed: { method: "GET", path: "/echo?hop=1", from: null, body: "" },
|
|
278
|
+
manual: [302, "/echo"],
|
|
279
|
+
seeOther: { method: "GET", type: null, body: "" },
|
|
280
|
+
temporary: { method: "POST", type: "text/plain", body: "kept" },
|
|
281
|
+
refused: "TypeError",
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
// A call in flight does not hold up other requests.
|
|
285
|
+
const slow = get("/slow?ms=1500");
|
|
286
|
+
await Bun.sleep(200);
|
|
287
|
+
const ping = await get("/ping");
|
|
288
|
+
assert.equal(ping.text, "pong");
|
|
289
|
+
assert(ping.ms < 500, `/ping took ${Math.round(ping.ms)} ms behind a 1.5 s call`);
|
|
290
|
+
assert.equal((await slow).text, "slow done");
|
|
291
|
+
|
|
292
|
+
// Calls overlap.
|
|
293
|
+
const t0 = performance.now();
|
|
294
|
+
const all = await Promise.all(Array.from({ length: 8 }, () => get("/slow?ms=500")));
|
|
295
|
+
const total = performance.now() - t0;
|
|
296
|
+
assert(all.every((r) => r.text === "slow done"), JSON.stringify(all));
|
|
297
|
+
assert(total < 1500, `8 concurrent 500 ms calls took ${Math.round(total)} ms`);
|
|
298
|
+
|
|
299
|
+
// Cancellation: an aborted signal rejects at once, and the abort reaches the upstream
|
|
300
|
+
// request (the broker drops it when the sprout closes the exchange's connection).
|
|
301
|
+
const abandonedBefore = slowAbandoned;
|
|
302
|
+
const aborts = await get("/abort");
|
|
303
|
+
assert.equal(aborts.status, 200, aborts.text);
|
|
304
|
+
assert.deepEqual(JSON.parse(aborts.text), { aborted: "AbortError fast", timedOut: "TimeoutError fast", already: "AbortError fast" });
|
|
305
|
+
await Bun.sleep(300);
|
|
306
|
+
assert.equal(slowAbandoned - abandonedBefore, 2, "both in-flight calls were stopped upstream");
|
|
307
|
+
|
|
308
|
+
// A call the edge vouched for sees the caller's URL and none of the edge's headers,
|
|
309
|
+
// and the callee's error is thrown in the caller, class and message intact.
|
|
310
|
+
const callself = await get("/callself");
|
|
311
|
+
assert.equal(callself.status, 200, callself.text);
|
|
312
|
+
assert.deepEqual(JSON.parse(callself.text), {
|
|
313
|
+
who: { url: "https://self.example/whoami?q=1", internal: [] },
|
|
314
|
+
thrown: ["RangeError", "kaboom", true],
|
|
315
|
+
});
|
|
316
|
+
// A client cannot claim to be a binding call: no caller URL, no error marker.
|
|
317
|
+
const forged = { "x-sb-binding-call": "1", "x-sb-edge-token": "guess", "x-sb-url": "https://forged.example/" };
|
|
318
|
+
const spoofed = await fetch(`http://127.0.0.1:${port}/whoami`, { headers: forged });
|
|
319
|
+
assert.equal((await spoofed.json()).url, `http://127.0.0.1:${port}/whoami`);
|
|
320
|
+
const publicBoom = await fetch(`http://127.0.0.1:${port}/boom`, { headers: forged });
|
|
321
|
+
assert.equal(publicBoom.status, 500);
|
|
322
|
+
assert.equal(publicBoom.headers.get("x-sb-uncaught"), null);
|
|
323
|
+
|
|
324
|
+
// Streamed responses (compatibility date 2026-10-21 on): the call resolves on the
|
|
325
|
+
// head, the first chunk arrives before the rest, and a 12 MiB body arrives intact.
|
|
326
|
+
const streamed = await get("/streamed");
|
|
327
|
+
assert.equal(streamed.status, 200, streamed.text);
|
|
328
|
+
assert.deepEqual(JSON.parse(streamed.text), { firstEarly: true, text: "0 1 2 3 ", whole: "0 1 2 3 ", bigOk: true });
|
|
329
|
+
|
|
330
|
+
// A deployment calling itself, 30 levels deep: every level waits on the next at once.
|
|
331
|
+
const self = await get("/self");
|
|
332
|
+
assert.equal(self.text, "bottom 30");
|
|
333
|
+
console.log(
|
|
334
|
+
`Service bindings return promises and run off the loop: /ping ${Math.round(ping.ms)} ms during a call, ` +
|
|
335
|
+
`8x500 ms in ${Math.round(total)} ms, self-recursion ok; URL, Request+init, stream bodies, Set-Cookie and missing content types preserved; redirects follow Fetch; aborted calls stop upstream; vouched calls see the caller's URL and rethrow the callee's errors; responses stream`,
|
|
336
|
+
);
|
|
337
|
+
} finally {
|
|
338
|
+
if (server) {
|
|
339
|
+
server.kill(9);
|
|
340
|
+
await server.exited;
|
|
341
|
+
}
|
|
342
|
+
brokerServer.stop();
|
|
343
|
+
broker.close();
|
|
344
|
+
edge.stop(true);
|
|
345
|
+
await rm(workdir, { recursive: true, force: true });
|
|
346
|
+
}
|
package/test/native-utf8.ts
CHANGED
|
@@ -23,7 +23,7 @@ export default {
|
|
|
23
23
|
if (request.method === "POST") {
|
|
24
24
|
env.UPLOADS.put("upload", request.body);
|
|
25
25
|
await env.KV.put("upload", request.body);
|
|
26
|
-
return Response.json({ r2: env.UPLOADS.get("upload").text(), kv: await env.KV.get("upload") });
|
|
26
|
+
return Response.json({ r2: env.UPLOADS.get("upload").text(), kv: await env.KV.get("upload"), stream: await new Response(request.body).text() });
|
|
27
27
|
}
|
|
28
28
|
await env.KV.put("k", ${JSON.stringify(text)});
|
|
29
29
|
const kv = await env.KV.get("k");
|
|
@@ -65,7 +65,7 @@ try {
|
|
|
65
65
|
assert(core.includes(TRANSPORT_MARKER), "runtime prelude is missing its transport marker");
|
|
66
66
|
const generated = join(workdir, "utf8.generated.js");
|
|
67
67
|
const binary = join(workdir, "utf8-native");
|
|
68
|
-
await writeFile(generated, wrapNativeFetchHandler(handler, core.replace(TRANSPORT_MARKER, transport), {}, bindings));
|
|
68
|
+
await writeFile(generated, wrapNativeFetchHandler(handler, core.replace(TRANSPORT_MARKER, transport), {}, bindings, undefined, "2026-10-21"));
|
|
69
69
|
|
|
70
70
|
const compile = Bun.spawn([node, resolve(porffor, "runtime/index.js"), "native", generated, "-o", binary, "-s", "-O0"], {
|
|
71
71
|
cwd: workdir,
|
|
@@ -121,7 +121,7 @@ try {
|
|
|
121
121
|
assert.match(body.error, /tabellà/);
|
|
122
122
|
assert.equal(body.url, "shadowed");
|
|
123
123
|
const stored = await (await fetch(`http://127.0.0.1:${port}/`, { method: "POST", body: "uploaded é" })).json();
|
|
124
|
-
assert.deepEqual(stored, { r2: "uploaded é", kv: "uploaded é" }, "put(key, request.body) stores the request body");
|
|
124
|
+
assert.deepEqual(stored, { r2: "uploaded é", kv: "uploaded é", stream: "uploaded é" }, "put(key, request.body) stores the request body, and body reads as UTF-8");
|
|
125
125
|
console.log("Non-ASCII KV, D1 and error text round-trip through the broker; a handler-level URL stays scoped; put(key, request.body) stores the body");
|
|
126
126
|
} finally {
|
|
127
127
|
if (server) {
|