fix(signal): cancel status-only response bodies

This commit is contained in:
Vincent Koc
2026-06-19 09:26:03 +02:00
parent e67f8ba459
commit fcec95ffd7
2 changed files with 39 additions and 6 deletions

View File

@@ -130,6 +130,22 @@ describe("containerCheck", () => {
expectFirstFetchCall("http://localhost:8080/v1/about", "GET");
});
it("cancels /v1/about response bodies after simple health checks", async () => {
const cancel = vi.fn(async () => undefined);
mockFetch.mockResolvedValue({
ok: true,
status: 200,
body: { cancel },
});
await expect(containerCheck("http://localhost:8080")).resolves.toEqual({
ok: true,
status: 200,
error: null,
});
expect(cancel).toHaveBeenCalledTimes(1);
});
it("returns ok:false when /v1/about returns 404", async () => {
mockFetch.mockResolvedValue({
ok: false,
@@ -635,9 +651,11 @@ describe("containerFetchAttachment", () => {
});
it("returns null on non-ok response", async () => {
const cancel = vi.fn(async () => undefined);
mockFetch.mockResolvedValue({
ok: false,
status: 404,
body: { cancel },
});
const result = await containerFetchAttachment("attachment-123", {
@@ -645,6 +663,7 @@ describe("containerFetchAttachment", () => {
});
expect(result).toBeNull();
expect(cancel).toHaveBeenCalledTimes(1);
});
it("encodes attachment ID in URL", async () => {

View File

@@ -111,6 +111,12 @@ async function readCappedResponseBuffer(res: Response, maxResponseBytes: number)
});
}
async function releaseUnreadResponseBody(res: Response | undefined): Promise<void> {
if (res?.bodyUsed !== true) {
await res?.body?.cancel().catch(() => undefined);
}
}
/**
* Check if bbernhard container REST API is available.
*/
@@ -120,8 +126,9 @@ export async function containerCheck(
account?: string,
): Promise<{ ok: boolean; status?: number | null; error?: string | null }> {
const normalized = normalizeBaseUrl(baseUrl);
let res: Response | undefined;
try {
const res = await fetchWithTimeout(`${normalized}/v1/about`, { method: "GET" }, timeoutMs);
res = await fetchWithTimeout(`${normalized}/v1/about`, { method: "GET" }, timeoutMs);
if (!res.ok) {
return { ok: false, status: res.status, error: `HTTP ${res.status}` };
}
@@ -136,6 +143,8 @@ export async function containerCheck(
status: null,
error: err instanceof Error ? err.message : String(err),
};
} finally {
await releaseUnreadResponseBody(res);
}
}
@@ -253,14 +262,19 @@ export async function containerFetchAttachment(
): Promise<Buffer | null> {
const baseUrl = normalizeBaseUrl(opts.baseUrl);
const url = `${baseUrl}/v1/attachments/${encodeURIComponent(attachmentId)}`;
let res: Response | undefined;
const res = await fetchWithTimeout(url, { method: "GET" }, opts.timeoutMs ?? DEFAULT_TIMEOUT_MS);
try {
res = await fetchWithTimeout(url, { method: "GET" }, opts.timeoutMs ?? DEFAULT_TIMEOUT_MS);
if (!res.ok) {
return null;
if (!res.ok) {
return null;
}
return await readCappedResponseBuffer(res, normalizeMaxResponseBytes(opts.maxResponseBytes));
} finally {
await releaseUnreadResponseBody(res);
}
return readCappedResponseBuffer(res, normalizeMaxResponseBytes(opts.maxResponseBytes));
}
/**