@sproutboat/runtime 0.13.1 → 0.13.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.
- package/CHANGELOG.md +12 -0
- package/package.json +1 -1
- package/src/native-fetch-prelude.js +35 -11
- package/src/r2-object.test.js +2 -1
- package/src/transport-broker.js +16 -0
- package/src/transport-embedded.js +5 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @sproutboat/runtime
|
|
2
2
|
|
|
3
|
+
## 0.13.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 733bd9e: Outbound `fetch()` and service-binding responses keep binary bodies intact (baronunread/sproutboat#232). `arrayBuffer()` and `bytes()` on a fetched response used to go through Porffor's `text()`, which the runtime overrides to decode UTF-8, so binary bodies collapsed; they now read the raw bytes. Through the broker, `fetch` and `service.fetch` sent over a v1 frame now get the upstream body as raw bytes in the frame's binary section instead of `TextDecoder` output. v0 frames, which already-deployed sprouts use, are unchanged, and a new sprout talking to an older broker falls back to the previous text behaviour.
|
|
8
|
+
|
|
9
|
+
## 0.13.2
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- dc6539c: Log uncaught handler exceptions to stderr before answering 500, as `Uncaught (in fetch) Error: ...` (and `scheduled` / `alarm` for those triggers). A thrown or rejected handler used to produce a bare 500 with nothing logged, which hid baronunread/sproutboat#168 entirely.
|
|
14
|
+
|
|
3
15
|
## 0.13.1
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -655,6 +655,12 @@ function __sbRawBodyResponse(body, init) {
|
|
|
655
655
|
return decoded;
|
|
656
656
|
};
|
|
657
657
|
resp.json = () => JSON.parse(resp.text());
|
|
658
|
+
// #232 — Porffor's arrayBuffer() goes through text(), which decodes UTF-8
|
|
659
|
+
// above and so collapsed a binary body's multi-byte runs. The body already
|
|
660
|
+
// is the bytes, one char per byte. blob() stays text-based: Porffor's Blob
|
|
661
|
+
// only holds text and has no arrayBuffer() to read bytes back out of.
|
|
662
|
+
resp.arrayBuffer = () => __sbBufFrom(body);
|
|
663
|
+
resp.bytes = () => new Uint8Array(__sbBufFrom(body));
|
|
658
664
|
}
|
|
659
665
|
return resp;
|
|
660
666
|
}
|
|
@@ -1584,7 +1590,7 @@ globalThis.__sbInstallBindings = function (target, bindings) {
|
|
|
1584
1590
|
if (__sbIsFn(opts.headers.forEach)) opts.headers.forEach((v, k) => headers.push([k, v]));
|
|
1585
1591
|
else for (const k in opts.headers) headers.push([k, opts.headers[k]]);
|
|
1586
1592
|
}
|
|
1587
|
-
const r =
|
|
1593
|
+
const r = globalThis.__sbFetchUpstream("service.fetch", {
|
|
1588
1594
|
binding,
|
|
1589
1595
|
url,
|
|
1590
1596
|
method: opts.method || "GET",
|
|
@@ -1611,7 +1617,7 @@ globalThis.__sbInstallBindings = function (target, bindings) {
|
|
|
1611
1617
|
if (__sbIsFn(opts.headers.forEach)) opts.headers.forEach((v, k) => headers.push([k, v]));
|
|
1612
1618
|
else for (const k in opts.headers) headers.push([k, opts.headers[k]]);
|
|
1613
1619
|
}
|
|
1614
|
-
const r =
|
|
1620
|
+
const r = globalThis.__sbFetchUpstream("fetch", {
|
|
1615
1621
|
url,
|
|
1616
1622
|
method: opts.method || "GET",
|
|
1617
1623
|
headers,
|
|
@@ -1993,27 +1999,43 @@ async function __sbFetchWithDrain(res, tasks) {
|
|
|
1993
1999
|
let r;
|
|
1994
2000
|
try {
|
|
1995
2001
|
r = res && __sbIsFn(res.then) ? await res : res;
|
|
1996
|
-
} catch {
|
|
2002
|
+
} catch (error) {
|
|
1997
2003
|
// #179 — a rejected handler promise must not take the process down with it.
|
|
1998
|
-
r = __sbErrorResponse();
|
|
2004
|
+
r = __sbErrorResponse(error);
|
|
1999
2005
|
}
|
|
2000
2006
|
await __sbDrainWaitUntil(tasks);
|
|
2001
2007
|
return r;
|
|
2002
2008
|
}
|
|
2003
2009
|
|
|
2004
2010
|
// #179 — turn an uncaught handler exception into a 500 instead of letting it
|
|
2005
|
-
// propagate past __sbEntry and crash the whole server.
|
|
2006
|
-
|
|
2011
|
+
// propagate past __sbEntry and crash the whole server. Logged first, the way a
|
|
2012
|
+
// Worker logs "Uncaught Error: ...": a swallowed exception left a 500 with
|
|
2013
|
+
// nothing on stderr, which hid #168 entirely.
|
|
2014
|
+
function __sbErrorResponse(error) {
|
|
2015
|
+
__sbLogUncaught("fetch", error);
|
|
2007
2016
|
return new Response("Internal Server Error", { status: 500 });
|
|
2008
2017
|
}
|
|
2009
2018
|
|
|
2019
|
+
function __sbLogUncaught(where, error) {
|
|
2020
|
+
try {
|
|
2021
|
+
let text = "";
|
|
2022
|
+
if (error && error.stack) text = String(error.stack);
|
|
2023
|
+
else if (error && error.message != null) text = (error.name || "Error") + ": " + error.message;
|
|
2024
|
+
else text = String(error);
|
|
2025
|
+
console.error("Uncaught (in " + where + ") " + text);
|
|
2026
|
+
} catch {
|
|
2027
|
+
/* logging must never turn a handled error into a crash */
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2010
2031
|
// Shared by alarm and scheduled: both reply with an empty 204 once their
|
|
2011
2032
|
// handler (and anything it queued via `waitUntil`) has settled.
|
|
2012
2033
|
async function __sb204WithDrain(res, tasks) {
|
|
2013
2034
|
try {
|
|
2014
2035
|
if (res && __sbIsFn(res.then)) await res;
|
|
2015
|
-
} catch {
|
|
2036
|
+
} catch (error) {
|
|
2016
2037
|
// #179 — same rule as fetch: a rejected scheduled/alarm promise must not crash the process.
|
|
2038
|
+
__sbLogUncaught("scheduled/alarm", error);
|
|
2017
2039
|
}
|
|
2018
2040
|
await __sbDrainWaitUntil(tasks);
|
|
2019
2041
|
return new Response("", { status: 204 });
|
|
@@ -2082,10 +2104,10 @@ globalThis.__sbEntry = function (handlers, request) {
|
|
|
2082
2104
|
__res = globalThis.__sbCompat >= "2026-09-28"
|
|
2083
2105
|
? handlers.fetch(request, globalThis.env, ctx)
|
|
2084
2106
|
: handlers.fetch(request, ctx);
|
|
2085
|
-
} catch {
|
|
2107
|
+
} catch (error) {
|
|
2086
2108
|
// #179 — a synchronous throw anywhere in the handler must not take the
|
|
2087
2109
|
// whole process (and every other in-flight request) down with it.
|
|
2088
|
-
return __sbErrorResponse();
|
|
2110
|
+
return __sbErrorResponse(error);
|
|
2089
2111
|
}
|
|
2090
2112
|
// Same promise-identity rule as above applies to `__sbFetchWithDrain`'s
|
|
2091
2113
|
// own return, which is why it's tail-called rather than chained on here.
|
|
@@ -2112,8 +2134,9 @@ globalThis.__sbEntry = function (handlers, request) {
|
|
|
2112
2134
|
__sres = globalThis.__sbCompat >= "2026-09-28"
|
|
2113
2135
|
? handlers.scheduled(event, globalThis.env, ctx)
|
|
2114
2136
|
: handlers.scheduled(event, ctx);
|
|
2115
|
-
} catch {
|
|
2137
|
+
} catch (error) {
|
|
2116
2138
|
// #179 — same rule as fetch: don't let a throw here crash the process.
|
|
2139
|
+
__sbLogUncaught("scheduled", error);
|
|
2117
2140
|
return new Response("", { status: 204 });
|
|
2118
2141
|
}
|
|
2119
2142
|
if (__sbTasks.length || (__sres && __sbIsFn(__sres.then))) return __sb204WithDrain(__sres, __sbTasks);
|
|
@@ -2146,8 +2169,9 @@ globalThis.__sbEntry = function (handlers, request) {
|
|
|
2146
2169
|
let __ares;
|
|
2147
2170
|
try {
|
|
2148
2171
|
__ares = inst.alarm();
|
|
2149
|
-
} catch {
|
|
2172
|
+
} catch (error) {
|
|
2150
2173
|
// #179 — same rule as fetch: don't let a throw here crash the process.
|
|
2174
|
+
__sbLogUncaught("alarm", error);
|
|
2151
2175
|
return new Response("", { status: 204 });
|
|
2152
2176
|
}
|
|
2153
2177
|
// Was fire-and-forget before: an async `alarm()` and any `state.waitUntil()`
|
package/src/r2-object.test.js
CHANGED
|
@@ -12,7 +12,8 @@ import { readFileSync } from "node:fs";
|
|
|
12
12
|
import { fileURLToPath } from "node:url";
|
|
13
13
|
|
|
14
14
|
const src = readFileSync(fileURLToPath(new URL("./native-fetch-prelude.js", import.meta.url)), "utf8");
|
|
15
|
-
|
|
15
|
+
// From __sbBufFrom (raw-body arrayBuffer, #232) through __sbFromUtf8.
|
|
16
|
+
const utf8Start = src.indexOf("function __sbBufFrom(");
|
|
16
17
|
const rawStart = src.indexOf("function __sbRawBodyResponse(");
|
|
17
18
|
const utf8End = rawStart;
|
|
18
19
|
const rawEnd = src.indexOf("function __sbEqCt(");
|
package/src/transport-broker.js
CHANGED
|
@@ -391,6 +391,22 @@ globalThis.__sbR2MultipartPut = function (bucket, key, uploadId, partNumber, bod
|
|
|
391
391
|
return reply;
|
|
392
392
|
};
|
|
393
393
|
|
|
394
|
+
/**
|
|
395
|
+
* #232 — outbound / service fetch over a v1 frame, so the body arrives as the
|
|
396
|
+
* upstream's raw bytes (one char per byte) instead of UTF-8-decoded text. A
|
|
397
|
+
* broker that predates this answers v1 fetch with the body still in its JSON:
|
|
398
|
+
* re-encode that to bytes so it matches today's behaviour rather than breaking.
|
|
399
|
+
*/
|
|
400
|
+
globalThis.__sbFetchUpstream = function (op, msg) {
|
|
401
|
+
const req = { v: 1, id: ++__sbReqId, token: __sbEnv("SB_BROKER_TOKEN"), op };
|
|
402
|
+
for (const k in msg) req[k] = msg[k];
|
|
403
|
+
const reply = JSON.parse(__sbCallBin(JSON.stringify(req), ""));
|
|
404
|
+
if (reply.ok === false) throw new Error("sproutboat " + op + ": " + (reply.error || "failed"));
|
|
405
|
+
const bytes = __sbTakeBin();
|
|
406
|
+
reply.body = reply.body != null ? __sbToBytes(reply.body) : bytes;
|
|
407
|
+
return reply;
|
|
408
|
+
};
|
|
409
|
+
|
|
394
410
|
/** Static asset metadata stays JSON; the response body uses the v1 byte tail. */
|
|
395
411
|
globalThis.__sbAssetsGet = function (path) {
|
|
396
412
|
const token = __sbEnv("SB_BROKER_TOKEN");
|
|
@@ -2603,6 +2603,11 @@ globalThis.__sbR2MultipartPut = function (bucket, key, uploadId, partNumber, bod
|
|
|
2603
2603
|
return { ok: true, part: { partNumber, etag: reply.etag } };
|
|
2604
2604
|
};
|
|
2605
2605
|
|
|
2606
|
+
// #232 — the C HTTP client already hands back raw bytes, one char per byte.
|
|
2607
|
+
globalThis.__sbFetchUpstream = function (op, msg) {
|
|
2608
|
+
return __sbRpc(op, msg);
|
|
2609
|
+
};
|
|
2610
|
+
|
|
2606
2611
|
globalThis.__sbAssetsGet = function (path) {
|
|
2607
2612
|
return __sbEmbeddedDispatch({ op: "assets.get", path });
|
|
2608
2613
|
};
|