@sproutboat/wire 0.9.0 → 0.9.2
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 +14 -1
- package/package.json +1 -1
- package/src/broker.test.ts +17 -0
- package/src/broker.ts +22 -9
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# @sproutboat/wire
|
|
2
2
|
|
|
3
|
+
## 0.9.2
|
|
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.9.1
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- ed9640a: Point Porffor patch diagnostics, runtime comments, and historical notes to the
|
|
14
|
+
shared upstream notes in the Sproutboat platform repository.
|
|
15
|
+
|
|
3
16
|
## 0.9.0
|
|
4
17
|
|
|
5
18
|
### Minor Changes
|
|
@@ -62,7 +75,7 @@
|
|
|
62
75
|
|
|
63
76
|
Verified end-to-end against both transports (kitchen-sink's full conformance suite, 24/24 standalone + 27/27 broker) and with new unit tests for file-backed persistence, the `:memory:` fallback, and a real binary round-trip over the v1 wire protocol.
|
|
64
77
|
|
|
65
|
-
**Known limitation found while verifying this, not fixed here:** a binary `put()` body (bytes ≥ 0x80) is corrupted on the way *in*, before it ever reaches storage
|
|
78
|
+
**Known limitation found while verifying this, not fixed here:** a binary `put()` body (bytes ≥ 0x80) is corrupted on the way *in*, before it ever reaches storage: `porf_native_fetch_read_value` (the function every inline-C string read in the runtime goes through) always UTF-8-encodes a Latin-1-range string on read, which is correct for genuine text but wrong for opaque bytes coming off a raw HTTP body. This predates this change (the old SQLite-blob path had the identical corruption) and is not something the storage backend can fix. It needs an input-side counterpart to `#176`'s `x-sb-raw-body` marker, or real `ArrayBuffer`/`Uint8Array` body support (the upstream ask was filed as Porffor #386 and later retracted; see `https://github.com/baronunread/sproutboat/blob/main/patches/upstream/request-body-report-386.md`).
|
|
66
79
|
|
|
67
80
|
## 0.5.0
|
|
68
81
|
|
package/package.json
CHANGED
package/src/broker.test.ts
CHANGED
|
@@ -154,6 +154,23 @@ test("fetch is gated by the exact-host allowlist", async () => {
|
|
|
154
154
|
expect(calls).toEqual(["https://api.example.com/x"]);
|
|
155
155
|
});
|
|
156
156
|
|
|
157
|
+
test("#232: a v1 fetch returns the upstream body as raw bytes; v0 keeps decoded text", async () => {
|
|
158
|
+
const binary = Uint8Array.from({ length: 256 }, (_, i) => i);
|
|
159
|
+
const fetchImpl: FetchLike = async (url) =>
|
|
160
|
+
new Response(String(url).endsWith("/bin") ? binary : "café", { status: 200 });
|
|
161
|
+
const b = make({ bindings: { outbound: ["api.example.com"] }, fetchImpl });
|
|
162
|
+
|
|
163
|
+
const raw = decodeV1Frame(
|
|
164
|
+
await b.handleFrame(encodeV1({ v: 1, token: "tok", op: "fetch", url: "https://api.example.com/bin" })),
|
|
165
|
+
);
|
|
166
|
+
expect(raw.json).toMatchObject({ ok: true, status: 200 });
|
|
167
|
+
expect(raw.json.body).toBeUndefined();
|
|
168
|
+
expect(Buffer.from(raw.bytes).equals(Buffer.from(binary))).toBe(true);
|
|
169
|
+
|
|
170
|
+
// Already-deployed sprouts speak v0 and expect the body as text in the JSON.
|
|
171
|
+
expect(await b.dispatch({ op: "fetch", url: "https://api.example.com/t" })).toMatchObject({ body: "café" });
|
|
172
|
+
});
|
|
173
|
+
|
|
157
174
|
test("D1: query / run / batch on a bound database, isolated per name", async () => {
|
|
158
175
|
const b = make({ bindings: { d1: ["DB", "OTHER"] } });
|
|
159
176
|
await b.dispatch({ op: "d1.exec", db: "DB", sql: "CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT)" });
|
package/src/broker.ts
CHANGED
|
@@ -1119,12 +1119,12 @@ export function createBroker(opts: BrokerOptions = {}): Broker {
|
|
|
1119
1119
|
* end the process. 32 MiB by default, and the same variable the embedded
|
|
1120
1120
|
* transport reads so both backends agree.
|
|
1121
1121
|
*/
|
|
1122
|
-
async function readCapped(response: Response, host: string): Promise<
|
|
1122
|
+
async function readCapped(response: Response, host: string): Promise<Buffer> {
|
|
1123
1123
|
const cap = Number(process.env.SB_FETCH_MAX_BYTES) || 32 * 1024 * 1024;
|
|
1124
1124
|
const declared = Number(response.headers.get("content-length") || 0);
|
|
1125
1125
|
if (declared > cap) throw new Error(`response exceeds SB_FETCH_MAX_BYTES from ${host}`);
|
|
1126
1126
|
const reader = response.body?.getReader();
|
|
1127
|
-
if (!reader) return
|
|
1127
|
+
if (!reader) return Buffer.alloc(0);
|
|
1128
1128
|
const parts: Uint8Array[] = [];
|
|
1129
1129
|
let total = 0;
|
|
1130
1130
|
for (;;) {
|
|
@@ -1137,10 +1137,19 @@ export function createBroker(opts: BrokerOptions = {}): Broker {
|
|
|
1137
1137
|
}
|
|
1138
1138
|
parts.push(value);
|
|
1139
1139
|
}
|
|
1140
|
-
return
|
|
1140
|
+
return Buffer.concat(parts);
|
|
1141
1141
|
}
|
|
1142
1142
|
|
|
1143
|
-
|
|
1143
|
+
/**
|
|
1144
|
+
* #232 — an upstream response as metadata plus its raw bytes. The v0 frame
|
|
1145
|
+
* carries the body as UTF-8 text inside the JSON (what already-deployed
|
|
1146
|
+
* sprouts expect), which turns a binary body into U+FFFD soup; the v1 frame
|
|
1147
|
+
* sends the bytes untouched in its binary section instead.
|
|
1148
|
+
*/
|
|
1149
|
+
type Upstream = { reply: Frame; bytes: Buffer };
|
|
1150
|
+
const asTextFrame = (u: Upstream) => ({ ...u.reply, body: new TextDecoder().decode(u.bytes) });
|
|
1151
|
+
|
|
1152
|
+
async function proxyFetch(msg: Frame): Promise<Upstream> {
|
|
1144
1153
|
let url: URL;
|
|
1145
1154
|
try {
|
|
1146
1155
|
url = new URL(str(msg.url));
|
|
@@ -1165,7 +1174,7 @@ export function createBroker(opts: BrokerOptions = {}): Broker {
|
|
|
1165
1174
|
});
|
|
1166
1175
|
const outHeaders: Array<[string, string]> = [];
|
|
1167
1176
|
res.headers.forEach((v, k) => outHeaders.push([k, v]));
|
|
1168
|
-
return { ok: true, status: res.status, headers: outHeaders,
|
|
1177
|
+
return { reply: { ok: true, status: res.status, headers: outHeaders }, bytes: await readCapped(res, url.host) };
|
|
1169
1178
|
}
|
|
1170
1179
|
|
|
1171
1180
|
/**
|
|
@@ -1176,7 +1185,7 @@ export function createBroker(opts: BrokerOptions = {}): Broker {
|
|
|
1176
1185
|
* minus the trip through the network (and therefore minus TLS and the
|
|
1177
1186
|
* default-deny egress rules, which is why this is not `fetch`).
|
|
1178
1187
|
*/
|
|
1179
|
-
async function serviceFetch(msg: Frame): Promise<
|
|
1188
|
+
async function serviceFetch(msg: Frame): Promise<Upstream> {
|
|
1180
1189
|
const binding = str(msg.binding);
|
|
1181
1190
|
const declared = bindings.services.find((entry) => entry.binding === binding);
|
|
1182
1191
|
if (!declared) throw new Error(`service not bound: ${binding}`);
|
|
@@ -1211,7 +1220,7 @@ export function createBroker(opts: BrokerOptions = {}): Broker {
|
|
|
1211
1220
|
});
|
|
1212
1221
|
const outHeaders: Array<[string, string]> = [];
|
|
1213
1222
|
res.headers.forEach((v, k) => outHeaders.push([k, v]));
|
|
1214
|
-
return { ok: true, status: res.status, headers: outHeaders,
|
|
1223
|
+
return { reply: { ok: true, status: res.status, headers: outHeaders }, bytes: await readCapped(res, host) };
|
|
1215
1224
|
}
|
|
1216
1225
|
|
|
1217
1226
|
async function dispatch(msg: Frame): Promise<Frame> {
|
|
@@ -1299,10 +1308,10 @@ export function createBroker(opts: BrokerOptions = {}): Broker {
|
|
|
1299
1308
|
return { ok: true, value: secrets[name] };
|
|
1300
1309
|
}
|
|
1301
1310
|
case "fetch":
|
|
1302
|
-
return proxyFetch(msg);
|
|
1311
|
+
return asTextFrame(await proxyFetch(msg));
|
|
1303
1312
|
|
|
1304
1313
|
case "service.fetch":
|
|
1305
|
-
return serviceFetch(msg);
|
|
1314
|
+
return asTextFrame(await serviceFetch(msg));
|
|
1306
1315
|
|
|
1307
1316
|
case "d1.query": {
|
|
1308
1317
|
const conn = d1(requireD1(msg.db));
|
|
@@ -1695,6 +1704,10 @@ export function createBroker(opts: BrokerOptions = {}): Broker {
|
|
|
1695
1704
|
};
|
|
1696
1705
|
}
|
|
1697
1706
|
|
|
1707
|
+
// #232 — a v1 fetch gets the upstream body as raw bytes, not decoded text.
|
|
1708
|
+
if (msg.op === "fetch") return proxyFetch(msg);
|
|
1709
|
+
if (msg.op === "service.fetch") return serviceFetch(msg);
|
|
1710
|
+
|
|
1698
1711
|
if (msg.op === "assets.get") {
|
|
1699
1712
|
if (!bindings.assets) throw new Error("assets not bound");
|
|
1700
1713
|
const reqPath = str(msg.path) || "/";
|