diff --git a/scripts/lib/orchestrator-gateway.mjs b/scripts/lib/orchestrator-gateway.mjs index e3650cae2..2204fef41 100644 --- a/scripts/lib/orchestrator-gateway.mjs +++ b/scripts/lib/orchestrator-gateway.mjs @@ -349,7 +349,14 @@ export async function verifyOrchestratorHealthz(healthzUrl, options = {}) { } let raw; - const reader = response.body?.getReader?.(); + let reader; + try { + reader = response.body?.getReader?.(); + } catch { + throw new Error( + "contextual-orchestrator health response body is not stream-readable", + ); + } if (reader) { const body = Buffer.allocUnsafe(HEALTH_BODY_LIMIT_BYTES); let totalBytes = 0; diff --git a/test/orchestrator-gateway-stream-bound.test.ts b/test/orchestrator-gateway-stream-bound.test.ts index cf6d3d1dd..ec968a7fb 100644 --- a/test/orchestrator-gateway-stream-bound.test.ts +++ b/test/orchestrator-gateway-stream-bound.test.ts @@ -131,6 +131,26 @@ describe("contextual-orchestrator streamed health response", () => { expect(released).toBe(true); }); + it("normalizes a locked health response body instead of surfacing stream implementation errors", async () => { + const response = new Response( + JSON.stringify({ status: "ok", service: "contextual-orchestrator" }), + { headers: { "content-type": "application/json" } }, + ); + const heldReader = response.body!.getReader(); + + try { + await expect( + verifyOrchestratorHealthz("https://orchestrator.example/healthz", { + fetchImpl: (async () => response) as typeof fetch, + }), + ).rejects.toThrow( + "contextual-orchestrator health response body is not stream-readable", + ); + } finally { + heldReader.releaseLock(); + } + }); + it("does not let stalled response-body cancellation delay content-length rejection", async () => { let cancellationStarted = false; const response = {