@sproutboat/runtime 0.42.1 → 0.43.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 +12 -0
- package/package.json +1 -1
- package/src/native-fetch-prelude.js +186 -24
- package/src/transport-embedded.js +35 -6
- package/test/native-base64.ts +12 -26
- package/test/native-bodies.ts +19 -26
- package/test/native-byte-text.ts +3 -19
- package/test/native-compile.ts +163 -0
- package/test/native-cpu-requests.ts +3 -20
- package/test/native-cpu-stamp.ts +3 -20
- package/test/native-crypto.ts +3 -22
- package/test/native-domexception.ts +3 -21
- package/test/native-errors.ts +12 -24
- package/test/native-events.ts +21 -25
- package/test/native-fetch-request.ts +11 -20
- package/test/native-getrandomvalues.ts +3 -21
- package/test/native-headers.ts +3 -21
- package/test/native-http.ts +10 -20
- package/test/native-iterators.ts +24 -27
- package/test/native-json-strings.ts +3 -21
- package/test/native-json.ts +15 -25
- package/test/native-jwt.ts +3 -22
- package/test/native-model-resolver.ts +5 -24
- package/test/native-raw-body.ts +7 -53
- package/test/native-request-log.ts +197 -0
- package/test/native-request-streaming.ts +3 -19
- package/test/native-request.ts +24 -26
- package/test/native-response-defaults.ts +4 -22
- package/test/native-service-bindings.ts +53 -27
- package/test/native-sockets-broker.ts +65 -20
- package/test/native-sockets-starttls.ts +404 -0
- package/test/native-streaming.ts +4 -22
- package/test/native-streams.ts +22 -25
- package/test/native-subtle.ts +3 -21
- package/test/native-textdecoder-encodings.ts +3 -21
- package/test/native-timers.ts +4 -22
- package/test/native-unhandled-rejection.ts +3 -20
- package/test/native-url.ts +15 -25
- package/test/native-utf8.ts +15 -21
- package/test/native-waituntil.ts +7 -23
- package/test/native-web-apis.ts +3 -22
- package/test/native-websockets.ts +3 -19
package/test/native-http.ts
CHANGED
|
@@ -3,8 +3,8 @@ import { createHash } from "node:crypto";
|
|
|
3
3
|
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
4
4
|
import { connect } from "node:net";
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
|
-
import { join
|
|
7
|
-
import {
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { compileNative } from "./native-compile";
|
|
8
8
|
import { preludePath, TRANSPORT_MARKER, transportPath, wrapNativeFetchHandler } from "../src/wrap";
|
|
9
9
|
|
|
10
10
|
// The native HTTP server's wire behaviour with raw sockets: keep-alive, pipelining (responses in
|
|
@@ -188,6 +188,13 @@ async function suite(port: number, label: string) {
|
|
|
188
188
|
assert.equal(got[0].body.toString(), "n=close");
|
|
189
189
|
}
|
|
190
190
|
|
|
191
|
+
// #236: an HTTP/1.0 request line (nginx's default proxy_http_version) is answered, not refused with 505.
|
|
192
|
+
{
|
|
193
|
+
const got = await exchange(port, [Buffer.from("GET /n/old HTTP/1.0\r\nHost: t\r\n\r\n")], 1);
|
|
194
|
+
assert.equal(got[0].status, 200, `${label}: HTTP/1.0 request status`);
|
|
195
|
+
assert.equal(got[0].body.toString(), "n=old", `${label}: HTTP/1.0 request body`);
|
|
196
|
+
}
|
|
197
|
+
|
|
191
198
|
// 150 connections at once, 4 pipelined requests each, with garbage-heavy requests mixed in.
|
|
192
199
|
{
|
|
193
200
|
const results = await Promise.all(
|
|
@@ -210,9 +217,6 @@ async function suite(port: number, label: string) {
|
|
|
210
217
|
const workdir = await mkdtemp(join(tmpdir(), "sb-native-http-"));
|
|
211
218
|
const servers: Array<ReturnType<typeof Bun.spawn>> = [];
|
|
212
219
|
try {
|
|
213
|
-
const porffor = await ensurePorffor();
|
|
214
|
-
const node = Bun.which("node");
|
|
215
|
-
if (!node) throw new Error("native http test needs node on PATH");
|
|
216
220
|
const [core, broker] = await Promise.all([readFile(preludePath, "utf8"), readFile(transportPath("broker"), "utf8")]);
|
|
217
221
|
assert(core.includes(TRANSPORT_MARKER), "runtime prelude is missing its transport marker");
|
|
218
222
|
const generated = join(workdir, "http.generated.js");
|
|
@@ -228,21 +232,7 @@ try {
|
|
|
228
232
|
"2026-10-21",
|
|
229
233
|
),
|
|
230
234
|
);
|
|
231
|
-
|
|
232
|
-
[node, resolve(porffor, "runtime/index.js"), "native", generated, "-o", binary, "-s", "-O0"],
|
|
233
|
-
{
|
|
234
|
-
cwd: workdir,
|
|
235
|
-
env: process.env,
|
|
236
|
-
stdout: "pipe",
|
|
237
|
-
stderr: "pipe",
|
|
238
|
-
},
|
|
239
|
-
);
|
|
240
|
-
const [exitCode, stdout, stderr] = await Promise.all([
|
|
241
|
-
compile.exited,
|
|
242
|
-
new Response(compile.stdout).text(),
|
|
243
|
-
new Response(compile.stderr).text(),
|
|
244
|
-
]);
|
|
245
|
-
assert.equal(exitCode, 0, `Porffor native compile failed:\n${stderr}\n${stdout}`);
|
|
235
|
+
await compileNative({ generated, binary, dir: workdir });
|
|
246
236
|
|
|
247
237
|
for (const every of ["1", "8"]) {
|
|
248
238
|
const port = freePort();
|
package/test/native-iterators.ts
CHANGED
|
@@ -1,16 +1,31 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
|
-
import { join
|
|
5
|
-
import {
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { compileNative } from "./native-compile";
|
|
6
6
|
import { preludePath, TRANSPORT_MARKER, transportPath, wrapNativeFetchHandler } from "../src/wrap";
|
|
7
7
|
|
|
8
8
|
const expected = {
|
|
9
|
-
query: [
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
query: [
|
|
10
|
+
["a", "1"],
|
|
11
|
+
["a", "2"],
|
|
12
|
+
["b", "3"],
|
|
13
|
+
],
|
|
14
|
+
keys: ["a", "a", "b"],
|
|
15
|
+
values: ["1", "2", "3"],
|
|
16
|
+
live: ["x", "y", true],
|
|
17
|
+
form: [
|
|
18
|
+
["x", "1"],
|
|
19
|
+
["x", "2"],
|
|
20
|
+
],
|
|
21
|
+
formLive: ["x", "y", true],
|
|
22
|
+
self: true,
|
|
23
|
+
headers: [
|
|
24
|
+
["a", "1"],
|
|
25
|
+
["b", "2"],
|
|
26
|
+
],
|
|
27
|
+
headersMap: 2,
|
|
28
|
+
headersFromEntries: { a: "1", b: "2" },
|
|
14
29
|
};
|
|
15
30
|
const handler = `export default { fetch(request) {
|
|
16
31
|
if (new URL(request.url).pathname === "/null") return new Response(null);
|
|
@@ -47,14 +62,7 @@ function freePort(): number {
|
|
|
47
62
|
const workdir = await mkdtemp(join(tmpdir(), "sb-native-iterators-"));
|
|
48
63
|
let server: ReturnType<typeof Bun.spawn> | undefined;
|
|
49
64
|
try {
|
|
50
|
-
const
|
|
51
|
-
const node = Bun.which("node");
|
|
52
|
-
if (!node) throw new Error("native iterators test needs node on PATH");
|
|
53
|
-
|
|
54
|
-
const [core, broker] = await Promise.all([
|
|
55
|
-
readFile(preludePath, "utf8"),
|
|
56
|
-
readFile(transportPath("broker"), "utf8"),
|
|
57
|
-
]);
|
|
65
|
+
const [core, broker] = await Promise.all([readFile(preludePath, "utf8"), readFile(transportPath("broker"), "utf8")]);
|
|
58
66
|
assert(core.includes(TRANSPORT_MARKER), "runtime prelude is missing its transport marker");
|
|
59
67
|
const prelude = core.replace(TRANSPORT_MARKER, broker);
|
|
60
68
|
const generated = join(workdir, "iterators.generated.js");
|
|
@@ -62,18 +70,7 @@ try {
|
|
|
62
70
|
await writeFile(generated, wrapNativeFetchHandler(handler, prelude));
|
|
63
71
|
await rm(binary, { force: true });
|
|
64
72
|
|
|
65
|
-
|
|
66
|
-
cwd: workdir,
|
|
67
|
-
env: process.env,
|
|
68
|
-
stdout: "pipe",
|
|
69
|
-
stderr: "pipe",
|
|
70
|
-
});
|
|
71
|
-
const [exitCode, stdout, stderr] = await Promise.all([
|
|
72
|
-
compile.exited,
|
|
73
|
-
new Response(compile.stdout).text(),
|
|
74
|
-
new Response(compile.stderr).text(),
|
|
75
|
-
]);
|
|
76
|
-
assert.equal(exitCode, 0, `Porffor native compile failed:\n${stderr}\n${stdout}`);
|
|
73
|
+
await compileNative({ generated, binary, dir: workdir, cacheRoot: workdir });
|
|
77
74
|
|
|
78
75
|
const port = freePort();
|
|
79
76
|
server = Bun.spawn([binary], {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
|
-
import { join
|
|
5
|
-
import {
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { compileNative } from "./native-compile";
|
|
6
6
|
import { preludePath, TRANSPORT_MARKER, transportPath, wrapNativeFetchHandler } from "../src/wrap";
|
|
7
7
|
|
|
8
8
|
// JSON.stringify of strings as the spec defines it (Bun is the reference): quotes and backslash,
|
|
@@ -61,10 +61,6 @@ function freePort(): number {
|
|
|
61
61
|
const workdir = await mkdtemp(join(tmpdir(), "sb-native-json-strings-"));
|
|
62
62
|
let server: ReturnType<typeof Bun.spawn> | undefined;
|
|
63
63
|
try {
|
|
64
|
-
const porffor = await ensurePorffor();
|
|
65
|
-
const node = Bun.which("node");
|
|
66
|
-
if (!node) throw new Error("native streams test needs node on PATH");
|
|
67
|
-
|
|
68
64
|
const [core, broker] = await Promise.all([readFile(preludePath, "utf8"), readFile(transportPath("broker"), "utf8")]);
|
|
69
65
|
assert(core.includes(TRANSPORT_MARKER), "runtime prelude is missing its transport marker");
|
|
70
66
|
const prelude = core.replace(TRANSPORT_MARKER, broker);
|
|
@@ -73,21 +69,7 @@ try {
|
|
|
73
69
|
await writeFile(generated, wrapNativeFetchHandler(handler, prelude));
|
|
74
70
|
await rm(binary, { force: true });
|
|
75
71
|
|
|
76
|
-
|
|
77
|
-
[node, resolve(porffor, "runtime/index.js"), "native", generated, "-o", binary, "-s", "-O0"],
|
|
78
|
-
{
|
|
79
|
-
cwd: workdir,
|
|
80
|
-
env: process.env,
|
|
81
|
-
stdout: "pipe",
|
|
82
|
-
stderr: "pipe",
|
|
83
|
-
},
|
|
84
|
-
);
|
|
85
|
-
const [exitCode, stdout, stderr] = await Promise.all([
|
|
86
|
-
compile.exited,
|
|
87
|
-
new Response(compile.stdout).text(),
|
|
88
|
-
new Response(compile.stderr).text(),
|
|
89
|
-
]);
|
|
90
|
-
assert.equal(exitCode, 0, `Porffor native compile failed:\n${stderr}\n${stdout}`);
|
|
72
|
+
await compileNative({ generated, binary, dir: workdir });
|
|
91
73
|
|
|
92
74
|
const port = freePort();
|
|
93
75
|
server = Bun.spawn([binary], {
|
package/test/native-json.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
|
-
import { join
|
|
5
|
-
import {
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { compileNative } from "./native-compile";
|
|
6
6
|
import { preludePath, TRANSPORT_MARKER, transportPath, wrapNativeFetchHandler } from "../src/wrap";
|
|
7
7
|
|
|
8
8
|
// JSON.stringify as the spec defines it (Bun is the reference): toJSON on Date, URL and
|
|
@@ -46,14 +46,7 @@ function freePort(): number {
|
|
|
46
46
|
const workdir = await mkdtemp(join(tmpdir(), "sb-native-json-"));
|
|
47
47
|
let server: ReturnType<typeof Bun.spawn> | undefined;
|
|
48
48
|
try {
|
|
49
|
-
const
|
|
50
|
-
const node = Bun.which("node");
|
|
51
|
-
if (!node) throw new Error("native streams test needs node on PATH");
|
|
52
|
-
|
|
53
|
-
const [core, broker] = await Promise.all([
|
|
54
|
-
readFile(preludePath, "utf8"),
|
|
55
|
-
readFile(transportPath("broker"), "utf8"),
|
|
56
|
-
]);
|
|
49
|
+
const [core, broker] = await Promise.all([readFile(preludePath, "utf8"), readFile(transportPath("broker"), "utf8")]);
|
|
57
50
|
assert(core.includes(TRANSPORT_MARKER), "runtime prelude is missing its transport marker");
|
|
58
51
|
const prelude = core.replace(TRANSPORT_MARKER, broker);
|
|
59
52
|
const generated = join(workdir, "json.generated.js");
|
|
@@ -61,18 +54,7 @@ try {
|
|
|
61
54
|
await writeFile(generated, wrapNativeFetchHandler(handler, prelude));
|
|
62
55
|
await rm(binary, { force: true });
|
|
63
56
|
|
|
64
|
-
|
|
65
|
-
cwd: workdir,
|
|
66
|
-
env: process.env,
|
|
67
|
-
stdout: "pipe",
|
|
68
|
-
stderr: "pipe",
|
|
69
|
-
});
|
|
70
|
-
const [exitCode, stdout, stderr] = await Promise.all([
|
|
71
|
-
compile.exited,
|
|
72
|
-
new Response(compile.stdout).text(),
|
|
73
|
-
new Response(compile.stderr).text(),
|
|
74
|
-
]);
|
|
75
|
-
assert.equal(exitCode, 0, `Porffor native compile failed:\n${stderr}\n${stdout}`);
|
|
57
|
+
await compileNative({ generated, binary, dir: workdir });
|
|
76
58
|
|
|
77
59
|
const port = freePort();
|
|
78
60
|
server = Bun.spawn([binary], {
|
|
@@ -95,9 +77,17 @@ try {
|
|
|
95
77
|
// SAFETY: the handler above is the only producer of this body: a JSON array with one
|
|
96
78
|
// string per case; the comparison below reports any entry that differs.
|
|
97
79
|
const got = JSON.parse(await (await fetch(`http://127.0.0.1:${port}/`)).text()) as string[];
|
|
98
|
-
const diffs = CASES.flatMap((c, i) =>
|
|
99
|
-
|
|
100
|
-
|
|
80
|
+
const diffs = CASES.flatMap((c, i) =>
|
|
81
|
+
got[i] === want[i] ? [] : [`${c}\n want ${JSON.stringify(want[i])}\n got ${JSON.stringify(got[i])}`],
|
|
82
|
+
);
|
|
83
|
+
assert.deepEqual(
|
|
84
|
+
diffs,
|
|
85
|
+
[],
|
|
86
|
+
`JSON.stringify differs from the spec in ${diffs.length} of ${CASES.length} cases:\n${diffs.join("\n")}`,
|
|
87
|
+
);
|
|
88
|
+
console.log(
|
|
89
|
+
`Native JSON.stringify honours toJSON in all ${CASES.length} cases: Date, URL, user objects, nested, undefined`,
|
|
90
|
+
);
|
|
101
91
|
} finally {
|
|
102
92
|
if (server) {
|
|
103
93
|
server.kill(9);
|
package/test/native-jwt.ts
CHANGED
|
@@ -2,9 +2,8 @@ import assert from "node:assert/strict";
|
|
|
2
2
|
import { createHmac } from "node:crypto";
|
|
3
3
|
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
|
-
import { join
|
|
6
|
-
import {
|
|
7
|
-
import { bearsslEnv } from "./bearssl";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { compileNative } from "./native-compile";
|
|
8
7
|
import { preludePath, TRANSPORT_MARKER, transportPath, wrapNativeFetchHandler } from "../src/wrap";
|
|
9
8
|
|
|
10
9
|
// The pieces of a JWT check (base64url decode, TextEncoder, HMAC verify) against values that do
|
|
@@ -276,10 +275,6 @@ assert.equal(
|
|
|
276
275
|
const workdir = await mkdtemp(join(tmpdir(), "sb-native-jwt-"));
|
|
277
276
|
let server: ReturnType<typeof Bun.spawn> | undefined;
|
|
278
277
|
try {
|
|
279
|
-
const porffor = await ensurePorffor();
|
|
280
|
-
const node = Bun.which("node");
|
|
281
|
-
if (!node) throw new Error("native jwt test needs node on PATH");
|
|
282
|
-
|
|
283
278
|
const [core, broker] = await Promise.all([readFile(preludePath, "utf8"), readFile(transportPath("broker"), "utf8")]);
|
|
284
279
|
assert(core.includes(TRANSPORT_MARKER), "runtime prelude is missing its transport marker");
|
|
285
280
|
const prelude = core.replace(TRANSPORT_MARKER, broker);
|
|
@@ -287,21 +282,7 @@ try {
|
|
|
287
282
|
const binary = join(workdir, "jwt-native");
|
|
288
283
|
await writeFile(generated, wrapNativeFetchHandler(handler, prelude));
|
|
289
284
|
|
|
290
|
-
|
|
291
|
-
[node, resolve(porffor, "runtime/index.js"), "native", generated, "-o", binary, "-s", "-O0"],
|
|
292
|
-
{
|
|
293
|
-
cwd: workdir,
|
|
294
|
-
env: { ...process.env, ...(await bearsslEnv()) },
|
|
295
|
-
stdout: "pipe",
|
|
296
|
-
stderr: "pipe",
|
|
297
|
-
},
|
|
298
|
-
);
|
|
299
|
-
const [exitCode, stdout, stderr] = await Promise.all([
|
|
300
|
-
compile.exited,
|
|
301
|
-
new Response(compile.stdout).text(),
|
|
302
|
-
new Response(compile.stderr).text(),
|
|
303
|
-
]);
|
|
304
|
-
assert.equal(exitCode, 0, `Porffor native compile failed:\n${stderr}\n${stdout}`);
|
|
285
|
+
await compileNative({ generated, binary, dir: workdir, bearssl: true });
|
|
305
286
|
|
|
306
287
|
const port = freePort();
|
|
307
288
|
server = Bun.spawn([binary], {
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
|
-
import { join
|
|
5
|
-
import {
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { compileNative } from "./native-compile";
|
|
6
6
|
import { preludePath, TRANSPORT_MARKER, transportPath, wrapNativeFetchHandler } from "../src/wrap";
|
|
7
7
|
|
|
8
|
-
const expected = {a: "user", b: "account", again: "user", plural: "user", custom: "user"};
|
|
8
|
+
const expected = { a: "user", b: "account", again: "user", plural: "user", custom: "user" };
|
|
9
9
|
const handler = `
|
|
10
10
|
const makeResolver = ({usePlural, schema}) => {
|
|
11
11
|
return (model) => {
|
|
@@ -31,7 +31,6 @@ export default {fetch() {
|
|
|
31
31
|
custom:makeResolver({schema:{user:{modelName:"members"}},usePlural:false})("members")});
|
|
32
32
|
}};`;
|
|
33
33
|
|
|
34
|
-
|
|
35
34
|
function freePort(): number {
|
|
36
35
|
const listener = Bun.listen({ hostname: "127.0.0.1", port: 0, socket: { data() {} } });
|
|
37
36
|
const port = listener.port;
|
|
@@ -42,14 +41,7 @@ function freePort(): number {
|
|
|
42
41
|
const workdir = await mkdtemp(join(tmpdir(), "sb-native-model-resolver-"));
|
|
43
42
|
let server: ReturnType<typeof Bun.spawn> | undefined;
|
|
44
43
|
try {
|
|
45
|
-
const
|
|
46
|
-
const node = Bun.which("node");
|
|
47
|
-
if (!node) throw new Error("native streams test needs node on PATH");
|
|
48
|
-
|
|
49
|
-
const [core, broker] = await Promise.all([
|
|
50
|
-
readFile(preludePath, "utf8"),
|
|
51
|
-
readFile(transportPath("broker"), "utf8"),
|
|
52
|
-
]);
|
|
44
|
+
const [core, broker] = await Promise.all([readFile(preludePath, "utf8"), readFile(transportPath("broker"), "utf8")]);
|
|
53
45
|
assert(core.includes(TRANSPORT_MARKER), "runtime prelude is missing its transport marker");
|
|
54
46
|
const prelude = core.replace(TRANSPORT_MARKER, broker);
|
|
55
47
|
const generated = join(workdir, "model-resolver.generated.js");
|
|
@@ -57,18 +49,7 @@ try {
|
|
|
57
49
|
await writeFile(generated, wrapNativeFetchHandler(handler, prelude));
|
|
58
50
|
await rm(binary, { force: true });
|
|
59
51
|
|
|
60
|
-
|
|
61
|
-
cwd: workdir,
|
|
62
|
-
env: process.env,
|
|
63
|
-
stdout: "pipe",
|
|
64
|
-
stderr: "pipe",
|
|
65
|
-
});
|
|
66
|
-
const [exitCode, stdout, stderr] = await Promise.all([
|
|
67
|
-
compile.exited,
|
|
68
|
-
new Response(compile.stdout).text(),
|
|
69
|
-
new Response(compile.stderr).text(),
|
|
70
|
-
]);
|
|
71
|
-
assert.equal(exitCode, 0, `Porffor native compile failed:\n${stderr}\n${stdout}`);
|
|
52
|
+
await compileNative({ generated, binary, dir: workdir });
|
|
72
53
|
|
|
73
54
|
const port = freePort();
|
|
74
55
|
server = Bun.spawn([binary], {
|
package/test/native-raw-body.ts
CHANGED
|
@@ -1,14 +1,9 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
|
-
import { join
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
preludePath,
|
|
8
|
-
TRANSPORT_MARKER,
|
|
9
|
-
transportPath,
|
|
10
|
-
wrapNativeFetchHandler,
|
|
11
|
-
} from "../src/wrap";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { compileNative } from "./native-compile";
|
|
6
|
+
import { preludePath, TRANSPORT_MARKER, transportPath, wrapNativeFetchHandler } from "../src/wrap";
|
|
12
7
|
|
|
13
8
|
// fetch() hands a response's bytes back as a raw body (x-sb-raw-body, #176), and
|
|
14
9
|
// they can arrive as a UTF-16 string holding one byte per unit: JSON.parse of the
|
|
@@ -33,52 +28,15 @@ function freePort(): number {
|
|
|
33
28
|
const workdir = await mkdtemp(join(tmpdir(), "sb-native-raw-body-"));
|
|
34
29
|
let server: ReturnType<typeof Bun.spawn> | undefined;
|
|
35
30
|
try {
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
if (!node) throw new Error("native streams test needs node on PATH");
|
|
39
|
-
|
|
40
|
-
const [core, broker] = await Promise.all([
|
|
41
|
-
readFile(preludePath, "utf8"),
|
|
42
|
-
readFile(transportPath("broker"), "utf8"),
|
|
43
|
-
]);
|
|
44
|
-
assert(
|
|
45
|
-
core.includes(TRANSPORT_MARKER),
|
|
46
|
-
"runtime prelude is missing its transport marker",
|
|
47
|
-
);
|
|
31
|
+
const [core, broker] = await Promise.all([readFile(preludePath, "utf8"), readFile(transportPath("broker"), "utf8")]);
|
|
32
|
+
assert(core.includes(TRANSPORT_MARKER), "runtime prelude is missing its transport marker");
|
|
48
33
|
const prelude = core.replace(TRANSPORT_MARKER, broker);
|
|
49
34
|
const generated = join(workdir, "raw.generated.js");
|
|
50
35
|
const binary = join(workdir, "raw-native");
|
|
51
36
|
await writeFile(generated, wrapNativeFetchHandler(handler, prelude));
|
|
52
37
|
await rm(binary, { force: true });
|
|
53
38
|
|
|
54
|
-
|
|
55
|
-
[
|
|
56
|
-
node,
|
|
57
|
-
resolve(porffor, "runtime/index.js"),
|
|
58
|
-
"native",
|
|
59
|
-
generated,
|
|
60
|
-
"-o",
|
|
61
|
-
binary,
|
|
62
|
-
"-s",
|
|
63
|
-
"-O0",
|
|
64
|
-
],
|
|
65
|
-
{
|
|
66
|
-
cwd: workdir,
|
|
67
|
-
env: process.env,
|
|
68
|
-
stdout: "pipe",
|
|
69
|
-
stderr: "pipe",
|
|
70
|
-
},
|
|
71
|
-
);
|
|
72
|
-
const [exitCode, stdout, stderr] = await Promise.all([
|
|
73
|
-
compile.exited,
|
|
74
|
-
new Response(compile.stdout).text(),
|
|
75
|
-
new Response(compile.stderr).text(),
|
|
76
|
-
]);
|
|
77
|
-
assert.equal(
|
|
78
|
-
exitCode,
|
|
79
|
-
0,
|
|
80
|
-
`Porffor native compile failed:\n${stderr}\n${stdout}`,
|
|
81
|
-
);
|
|
39
|
+
await compileNative({ generated, binary, dir: workdir });
|
|
82
40
|
|
|
83
41
|
const port = freePort();
|
|
84
42
|
server = Bun.spawn([binary], {
|
|
@@ -103,11 +61,7 @@ try {
|
|
|
103
61
|
const response = await fetch(`http://127.0.0.1:${port}/`);
|
|
104
62
|
const got = [...new Uint8Array(await response.arrayBuffer())];
|
|
105
63
|
assert.deepEqual(got, want, "raw body bytes");
|
|
106
|
-
assert.equal(
|
|
107
|
-
response.headers.get("x-sb-raw-body"),
|
|
108
|
-
null,
|
|
109
|
-
"the marker never reaches the client",
|
|
110
|
-
);
|
|
64
|
+
assert.equal(response.headers.get("x-sb-raw-body"), null, "the marker never reaches the client");
|
|
111
65
|
console.log("A raw UTF-16 body is sent as its bytes");
|
|
112
66
|
} finally {
|
|
113
67
|
if (server) {
|
|
@@ -0,0 +1,197 @@
|
|
|
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 } from "node:path";
|
|
5
|
+
import { compileNative } from "./native-compile";
|
|
6
|
+
import { EMPTY_BINDINGS, preludePath, TRANSPORT_MARKER, transportPath, wrapNativeFetchHandler } from "../src/wrap";
|
|
7
|
+
import { isSafeInteger, isString, jsonObject, parseJsonValue, type JsonObject } from "../../wire/src/json";
|
|
8
|
+
|
|
9
|
+
// #268 — SB_LOG=json makes a standalone binary write one JSON line per request and one per uncaught
|
|
10
|
+
// error to stderr: correlated by id, bounded, with no bodies, headers, query strings or secret values.
|
|
11
|
+
// Without it the output is the plain text it always was.
|
|
12
|
+
const SECRET = "hunter2-secret-value";
|
|
13
|
+
const handler = `export default {
|
|
14
|
+
async fetch(request, env, ctx) {
|
|
15
|
+
const path = new URL(request.url).pathname;
|
|
16
|
+
if (path === "/boom") throw new Error("upstream rejected token ${SECRET}");
|
|
17
|
+
if (path === "/async-boom") {
|
|
18
|
+
await Promise.resolve();
|
|
19
|
+
throw new TypeError("async failure ${SECRET}");
|
|
20
|
+
}
|
|
21
|
+
if (path === "/unawaited") {
|
|
22
|
+
async function failNow() { throw new Error("background failure"); }
|
|
23
|
+
failNow();
|
|
24
|
+
return new Response("fired");
|
|
25
|
+
}
|
|
26
|
+
if (path === "/wait") {
|
|
27
|
+
ctx.waitUntil(Promise.reject(new Error("waitUntil failure")));
|
|
28
|
+
return new Response("queued");
|
|
29
|
+
}
|
|
30
|
+
if (request.method === "POST") return new Response("echo:" + (await request.text()));
|
|
31
|
+
return new Response("ok");
|
|
32
|
+
}
|
|
33
|
+
};`;
|
|
34
|
+
|
|
35
|
+
function freePort(): number {
|
|
36
|
+
const listener = Bun.listen({ hostname: "127.0.0.1", port: 0, socket: { data() {} } });
|
|
37
|
+
const port = listener.port;
|
|
38
|
+
listener.stop(true);
|
|
39
|
+
return port;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
type Run = { get: (path: string, init?: RequestInit) => Promise<Response>; stop: () => Promise<string> };
|
|
43
|
+
|
|
44
|
+
async function start(binary: string, workdir: string, extra: Record<string, string>): Promise<Run> {
|
|
45
|
+
const port = freePort();
|
|
46
|
+
const server = Bun.spawn([binary], {
|
|
47
|
+
cwd: workdir,
|
|
48
|
+
env: { ...process.env, PORT: String(port), SB_BROKER_TOKEN: "native-test-token", TOKEN: SECRET, ...extra },
|
|
49
|
+
stdout: "ignore",
|
|
50
|
+
stderr: "pipe",
|
|
51
|
+
});
|
|
52
|
+
const stderr = new Response(server.stderr).text();
|
|
53
|
+
let ready = false;
|
|
54
|
+
for (let attempt = 0; attempt < 50 && !ready; attempt++) {
|
|
55
|
+
try {
|
|
56
|
+
await fetch(`http://127.0.0.1:${port}/`, { signal: AbortSignal.timeout(500) });
|
|
57
|
+
ready = true;
|
|
58
|
+
} catch {
|
|
59
|
+
if (server.exitCode !== null) break;
|
|
60
|
+
await Bun.sleep(100);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
assert(ready, "compiled handler did not start");
|
|
64
|
+
return {
|
|
65
|
+
get: (path, init) => fetch(`http://127.0.0.1:${port}${path}`, init),
|
|
66
|
+
stop: async () => {
|
|
67
|
+
server.kill(9);
|
|
68
|
+
await server.exited;
|
|
69
|
+
return stderr;
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function records(log: string): JsonObject[] {
|
|
75
|
+
const found: JsonObject[] = [];
|
|
76
|
+
for (const line of log.split("\n")) {
|
|
77
|
+
// The binary also prints a startup banner of its own; every line that is JSON must be a record.
|
|
78
|
+
if (!line.startsWith("{")) continue;
|
|
79
|
+
const record = jsonObject(parseJsonValue(line));
|
|
80
|
+
assert(record, `a stderr line is not a JSON record: ${line}`);
|
|
81
|
+
found.push(record);
|
|
82
|
+
}
|
|
83
|
+
return found;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const workdir = await mkdtemp(join(tmpdir(), "sb-native-request-log-"));
|
|
87
|
+
try {
|
|
88
|
+
const [core, broker] = await Promise.all([readFile(preludePath, "utf8"), readFile(transportPath("broker"), "utf8")]);
|
|
89
|
+
assert(core.includes(TRANSPORT_MARKER), "runtime prelude is missing its transport marker");
|
|
90
|
+
const generated = join(workdir, "log.generated.js");
|
|
91
|
+
const binary = join(workdir, "log-native");
|
|
92
|
+
await writeFile(
|
|
93
|
+
generated,
|
|
94
|
+
wrapNativeFetchHandler(
|
|
95
|
+
handler,
|
|
96
|
+
core.replace(TRANSPORT_MARKER, broker),
|
|
97
|
+
undefined,
|
|
98
|
+
{ ...EMPTY_BINDINGS, secrets: ["TOKEN"] },
|
|
99
|
+
undefined,
|
|
100
|
+
"2026-11-07",
|
|
101
|
+
),
|
|
102
|
+
);
|
|
103
|
+
await compileNative({ generated, binary, dir: workdir });
|
|
104
|
+
|
|
105
|
+
// Off by default: the plain text line, no records.
|
|
106
|
+
const plain = await start(binary, workdir, {});
|
|
107
|
+
assert.equal(await (await plain.get("/ok")).text(), "ok");
|
|
108
|
+
assert.equal((await plain.get("/boom")).status, 500);
|
|
109
|
+
const plainLog = await plain.stop();
|
|
110
|
+
assert(plainLog.includes("Uncaught (in fetch)"), `the default error line changed: ${plainLog}`);
|
|
111
|
+
assert(!plainLog.includes('"type"'), `records were written without SB_LOG=json: ${plainLog}`);
|
|
112
|
+
|
|
113
|
+
// On: request and error records.
|
|
114
|
+
const json = await start(binary, workdir, { SB_LOG: "json" });
|
|
115
|
+
assert.equal(
|
|
116
|
+
await (await json.get("/ok?token=abc123&page=2", { headers: { authorization: "Bearer topsecret" } })).text(),
|
|
117
|
+
"ok",
|
|
118
|
+
);
|
|
119
|
+
assert.equal(
|
|
120
|
+
await (
|
|
121
|
+
await json.get("/echo", { method: "POST", headers: { cookie: "sid=cookiesecret" }, body: "body-payload-xyz" })
|
|
122
|
+
).text(),
|
|
123
|
+
"echo:body-payload-xyz",
|
|
124
|
+
);
|
|
125
|
+
assert.equal((await json.get("/boom")).status, 500);
|
|
126
|
+
assert.equal((await json.get("/async-boom")).status, 500);
|
|
127
|
+
assert.equal(await (await json.get("/unawaited")).text(), "fired");
|
|
128
|
+
assert.equal(await (await json.get("/wait")).text(), "queued");
|
|
129
|
+
await Bun.sleep(300);
|
|
130
|
+
const log = await json.stop();
|
|
131
|
+
for (const leaked of ["abc123", "topsecret", "cookiesecret", "body-payload-xyz", SECRET, "Uncaught"]) {
|
|
132
|
+
assert(!log.includes(leaked), `"${leaked}" reached the log: ${log}`);
|
|
133
|
+
}
|
|
134
|
+
const all = records(log);
|
|
135
|
+
const requests = all.filter((record) => record.type === "request");
|
|
136
|
+
const errors = all.filter((record) => record.type === "error");
|
|
137
|
+
const request = (path: string) => {
|
|
138
|
+
const found = requests.find((record) => record.path === path);
|
|
139
|
+
assert(found, `no request record for ${path}: ${log}`);
|
|
140
|
+
return found;
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const ok = request("/ok");
|
|
144
|
+
assert.equal(ok.v, 1);
|
|
145
|
+
assert.equal(ok.method, "GET");
|
|
146
|
+
assert.equal(ok.status, 200);
|
|
147
|
+
assert.equal(ok.outcome, "ok");
|
|
148
|
+
assert(isString(ok.id) && ok.id.length > 0);
|
|
149
|
+
assert(isSafeInteger(ok.durationMs) && ok.durationMs >= 0);
|
|
150
|
+
assert(isSafeInteger(ok.timestamp) && Math.abs(ok.timestamp - Date.now()) < 60_000);
|
|
151
|
+
assert.deepEqual(
|
|
152
|
+
Object.keys(ok).sort(),
|
|
153
|
+
["durationMs", "id", "method", "outcome", "path", "status", "timestamp", "type", "v"],
|
|
154
|
+
"a request record carries exactly these fields",
|
|
155
|
+
);
|
|
156
|
+
assert.equal(request("/echo").method, "POST");
|
|
157
|
+
|
|
158
|
+
for (const [path, name] of [
|
|
159
|
+
["/boom", "Error"],
|
|
160
|
+
["/async-boom", "TypeError"],
|
|
161
|
+
] as const) {
|
|
162
|
+
const failed = request(path);
|
|
163
|
+
assert.equal(failed.status, 500);
|
|
164
|
+
assert.equal(failed.outcome, "exception");
|
|
165
|
+
const error = errors.find((record) => record.id === failed.id);
|
|
166
|
+
assert(error, `no error record shares the id of ${path}: ${log}`);
|
|
167
|
+
assert.equal(error.source, "fetch");
|
|
168
|
+
assert.equal(error.name, name);
|
|
169
|
+
assert(String(error.message).includes("[redacted]"), `the secret was not redacted: ${error.message}`);
|
|
170
|
+
}
|
|
171
|
+
const background = errors.find((record) => record.source === "promise");
|
|
172
|
+
assert(background, `no record for the unawaited rejection: ${log}`);
|
|
173
|
+
assert.equal(background.id, null);
|
|
174
|
+
const waitUntil = errors.find((record) => record.source === "waitUntil");
|
|
175
|
+
assert(waitUntil, `no record for the waitUntil rejection: ${log}`);
|
|
176
|
+
assert.equal(waitUntil.id, request("/wait").id);
|
|
177
|
+
|
|
178
|
+
// Bounded: a burst past 200 lines a second is counted, not written, and the count is reported.
|
|
179
|
+
const flood = await start(binary, workdir, { SB_LOG: "json" });
|
|
180
|
+
const burst = 700;
|
|
181
|
+
for (let sent = 0; sent < burst; sent += 50) {
|
|
182
|
+
await Promise.all(Array.from({ length: 50 }, () => flood.get("/ok")));
|
|
183
|
+
}
|
|
184
|
+
await Bun.sleep(1100);
|
|
185
|
+
await flood.get("/ok");
|
|
186
|
+
await Bun.sleep(200);
|
|
187
|
+
const floodRecords = records(await flood.stop());
|
|
188
|
+
const written = floodRecords.filter((record) => record.type === "request").length;
|
|
189
|
+
const dropped = floodRecords
|
|
190
|
+
.filter((record) => record.type === "dropped")
|
|
191
|
+
.reduce((sum, record) => sum + Number(record.count), 0);
|
|
192
|
+
assert(dropped > 0, "a burst past the rate limit was not reported as dropped");
|
|
193
|
+
assert.equal(written + dropped, burst + 2, "every request is either written or counted as dropped");
|
|
194
|
+
console.log("Native request and error records are structured, correlated, redacted and bounded");
|
|
195
|
+
} finally {
|
|
196
|
+
await rm(workdir, { recursive: true, force: true });
|
|
197
|
+
}
|