Add a plugin API for reading HTTP response bodies (#560)

This commit is contained in:
Gregory Schier
2026-08-16 11:10:14 -07:00
committed by GitHub
parent 78954e10c8
commit 10e962a0e6
29 changed files with 1274 additions and 70 deletions
+1 -1
View File
@@ -67,7 +67,7 @@ export const plugin: PluginDefinition = {
const type1 = ntlm.createType1Message(options);
const negotiateResponse = await ctx.httpRequest.send({
const { httpResponse: negotiateResponse } = await ctx.httpRequest.send({
httpRequest: {
method,
url,
+5 -3
View File
@@ -1,4 +1,3 @@
import { readFileSync } from "node:fs";
import type { Context, HttpRequest, HttpUrlParameter } from "@yaakapp/api";
import type { AccessTokenRawResponse } from "./store";
@@ -57,7 +56,7 @@ export async function fetchAccessToken(
}
httpRequest.authenticationType = "none"; // Don't inherit workspace auth
const resp = await ctx.httpRequest.send({ httpRequest });
const { httpResponse: resp, body: responseBody } = await ctx.httpRequest.send({ httpRequest });
console.log("[oauth2] Got access token response", resp.status);
@@ -65,7 +64,10 @@ export async function fetchAccessToken(
throw new Error(`Failed to fetch access token: ${resp.error}`);
}
const body = resp.bodyPath ? readFileSync(resp.bodyPath, "utf8") : "";
// A token request is sent ad-hoc, with no id, so nothing saves the response
// and this body is the only copy of it. An empty one parses to {} below,
// which is what reading a missing file used to give.
const body = await responseBody.text();
if (resp.status < 200 || resp.status >= 300) {
throw new Error(`Failed to fetch access token with status=${resp.status} and body=${body}`);
@@ -1,4 +1,3 @@
import { readFileSync } from "node:fs";
import type { Context, HttpRequest } from "@yaakapp/api";
import type { AccessToken, AccessTokenRawResponse, TokenStoreArgs } from "./store";
import { deleteToken, getToken, storeToken } from "./store";
@@ -71,7 +70,7 @@ export async function getOrRefreshAccessToken(
}
httpRequest.authenticationType = "none"; // Don't inherit workspace auth
const resp = await ctx.httpRequest.send({ httpRequest });
const { httpResponse: resp, body: responseBody } = await ctx.httpRequest.send({ httpRequest });
if (resp.error) {
throw new Error(`Failed to refresh access token: ${resp.error}`);
@@ -85,7 +84,9 @@ export async function getOrRefreshAccessToken(
return null;
}
const body = resp.bodyPath ? readFileSync(resp.bodyPath, "utf8") : "";
// Sent ad-hoc, so this body came back with the response rather than being
// saved anywhere to read later.
const body = await responseBody.text();
console.log("[oauth2] Got refresh token response", resp.status);
+24 -25
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,32 @@ 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 — including responses it never recorded, which still get an
* id. 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> {
// Belt and braces: everything reaching here came from find() or send() and so
// has an id. An empty one would just be an unreadable id.
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,
{
@@ -320,7 +319,7 @@ async function getResponse(
// Explicitly render the request before send (instead of relying on send() to render) so that we can
// preserve the render purpose.
const renderedHttpRequest = await ctx.httpRequest.render({ httpRequest, purpose });
response = await ctx.httpRequest.send({ httpRequest: renderedHttpRequest });
response = (await ctx.httpRequest.send({ httpRequest: renderedHttpRequest })).httpResponse;
}
return response;