Return the body of a send that saved nothing

Replaces the response-directory fallback with what the frontend already
does for ephemeral sends: the engine hands the body back, because it is
the only copy. Guessing at a file named for an id was the store reaching
around its own abstraction, and it is exactly what must not survive the
move to blob storage.

The send reply carries the bytes when the engine returned them, and the
runtime holds them for the rest of the call that sent them, so
ctx.httpResponse.body() answers for a saved and an unsaved response the
same way. Unsaved ones now report a real contentType too, taken from the
response the send already handed back.
This commit is contained in:
Gregory Schier
2026-08-16 09:54:56 -07:00
parent 8b031db685
commit ffa6a610b8
10 changed files with 128 additions and 80 deletions
+34 -1
View File
@@ -609,6 +609,14 @@ export class PluginInstance {
}
#newCtx(context: PluginContext): Context {
// Bodies of sends that saved nothing, keyed by the response id they were
// handed back with.
//
// A ctx is built per incoming call, so these last exactly as long as the
// plugin invocation that produced them — which is the whole life of an
// unsaved response. Nothing else can reach one: the host has no row for it.
const unsavedBodies = new Map<string, { bytes: Uint8Array; contentType: string | null }>();
const _windowInfo = async () => {
if (context.label == null) {
throw new Error("Can't get window context without an active window");
@@ -763,6 +771,18 @@ export class PluginInstance {
return httpResponses;
},
body: async ({ responseId }) => {
const unsaved = unsavedBodies.get(responseId);
if (unsaved != null) {
return createResponseBody(
{
responseId,
contentLength: unsaved.bytes.byteLength,
contentType: unsaved.contentType,
},
async (offset, length) => unsaved.bytes.slice(offset, offset + length),
);
}
const info = await this.#sendForReply<GetHttpResponseBodyInfoResponse>(
context,
{ type: "get_http_response_body_info_request", responseId },
@@ -816,10 +836,23 @@ export class PluginInstance {
type: "send_http_request_request",
...args,
} as const;
const { httpResponse } = await this.#sendForReply<SendHttpRequestResponse>(
const { httpResponse, body } = await this.#sendForReply<SendHttpRequestResponse>(
context,
payload,
);
// A send with no request behind it saves nothing, so this reply is
// the only copy of its body. Hold it so ctx.httpResponse.body() can
// answer for it the same way it answers for a saved response.
if (body != null) {
unsavedBodies.set(httpResponse.id, {
bytes: decodeBase64Chunk(body),
contentType:
httpResponse.headers.find((h) => h.name.toLowerCase() === "content-type")?.value ??
null,
});
}
return httpResponse;
},
render: async (args) => {