Add a plugin API for reading HTTP response bodies

Plugins read bodies by response id through ctx.httpResponse.body()
instead of opening HttpResponse.bodyPath themselves. The accessors are
named after fetch's, minus the single-use semantics, since the bytes are
durable and re-reading should work.

Underneath is a chunked pull over the existing plugin protocol, so the
host can move bodies off the filesystem without plugins noticing.
text() now decodes with the response's charset rather than assuming
UTF-8, and the buffering accessors refuse past 32 MiB and point at
chunks().
This commit is contained in:
Gregory Schier
2026-08-16 09:20:29 -07:00
parent 6a02cbe525
commit 96c8a95094
18 changed files with 915 additions and 46 deletions
+20 -24
View File
@@ -1,4 +1,3 @@
import { readFileSync } from "node:fs";
import type {
CallTemplateFunctionArgs,
Context,
@@ -196,17 +195,8 @@ export const plugin: PluginDefinition = {
});
if (response == null) return null;
if (response.bodyPath == null) {
return null;
}
const BOM = "\ufeff";
let body: string;
try {
body = readFileSync(response.bodyPath, "utf-8").replace(BOM, "");
} catch {
return null;
}
const body = await readResponseBody(ctx, response);
if (body == null) return null;
try {
const result: JSONPathResult =
@@ -261,23 +251,29 @@ export const plugin: PluginDefinition = {
});
if (response == null) return null;
if (response.bodyPath == null) {
return null;
}
let body: string;
try {
body = readFileSync(response.bodyPath, "utf-8");
} catch {
return null;
}
return body;
return await readResponseBody(ctx, response);
},
},
],
};
/**
* The response's body as text, or null when there is nothing to read.
*
* The host is asked for it by response id, so this works wherever the bytes
* happen to live. A body over the runtime's size limit throws rather than
* coming back empty, since a template silently rendering to nothing is worse
* than one that says why.
*/
async function readResponseBody(ctx: Context, response: HttpResponse): Promise<string | null> {
if (!response.id) return null;
const body = await ctx.httpResponse.body({ responseId: response.id });
if (body.contentLength === 0) return null;
return await body.text();
}
async function getResponse(
ctx: Context,
{