From 2dbbef46bb428d9ea5ea40527f75398c0aecf795 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Fri, 19 Jun 2026 07:37:28 +0200 Subject: [PATCH] fix(e2e): cancel Open WebUI probe body reads --- scripts/e2e/openwebui-probe.mjs | 123 +++++++++++++++++---------- test/scripts/openwebui-probe.test.ts | 11 +++ 2 files changed, 87 insertions(+), 47 deletions(-) diff --git a/scripts/e2e/openwebui-probe.mjs b/scripts/e2e/openwebui-probe.mjs index 14a946340d8..2b9f406e3b1 100644 --- a/scripts/e2e/openwebui-probe.mjs +++ b/scripts/e2e/openwebui-probe.mjs @@ -74,12 +74,16 @@ function createTimeoutError(label, timeoutMs) { async function withRequestTimeout(label, timeoutMs, run) { const controller = new AbortController(); const timeoutError = createTimeoutError(label, timeoutMs); - const timer = setTimeout(() => { - controller.abort(timeoutError); - }, timeoutMs); - timer.unref?.(); + let timer; + const timeoutPromise = new Promise((_, reject) => { + timer = setTimeout(() => { + controller.abort(timeoutError); + reject(timeoutError); + }, timeoutMs); + timer.unref?.(); + }); try { - return await run(controller.signal); + return await Promise.race([run(controller.signal, timeoutPromise), timeoutPromise]); } catch (error) { if (controller.signal.aborted) { throw timeoutError; @@ -90,12 +94,17 @@ async function withRequestTimeout(label, timeoutMs, run) { } } -async function readBoundedResponseText(response, label, byteLimit = responseBodyMaxBytes) { - return await readBoundedResponseTextWithLimit(response, label, byteLimit); +async function readBoundedResponseText(response, label, timeoutPromise) { + return await readBoundedResponseTextWithLimit( + response, + label, + responseBodyMaxBytes, + timeoutPromise, + ); } -async function readBoundedResponseJson(response, label) { - const body = await readBoundedResponseText(response, label); +async function readBoundedResponseJson(response, label, timeoutPromise) { + const body = await readBoundedResponseText(response, label, timeoutPromise); try { return JSON.parse(body); } catch (error) { @@ -134,39 +143,51 @@ function sleep(ms) { } async function fetchSignin() { - return await withRequestTimeout("Open WebUI signin", controlTimeoutMs, async (signal) => { - const response = await fetch(`${baseUrl}/api/v1/auths/signin`, { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ email, password }), - signal, - }); - if (!response.ok) { - const body = await readBoundedResponseText(response, "Open WebUI signin"); - throw new Error(`signin failed: HTTP ${response.status} ${body}`); - } - return { - cookie: getCookieHeader(response), - json: await readBoundedResponseJson(response, "Open WebUI signin"), - }; - }); + return await withRequestTimeout( + "Open WebUI signin", + controlTimeoutMs, + async (signal, timeoutPromise) => { + const response = await fetch(`${baseUrl}/api/v1/auths/signin`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ email, password }), + signal, + }); + if (!response.ok) { + const body = await readBoundedResponseText(response, "Open WebUI signin", timeoutPromise); + throw new Error(`signin failed: HTTP ${response.status} ${body}`); + } + return { + cookie: getCookieHeader(response), + json: await readBoundedResponseJson(response, "Open WebUI signin", timeoutPromise), + }; + }, + ); } async function fetchModels(authHeaders, attempt) { return await withRequestTimeout( `Open WebUI models attempt ${attempt}`, controlTimeoutMs, - async (signal) => { + async (signal, timeoutPromise) => { const response = await fetch(`${baseUrl}/api/models`, { headers: authHeaders, signal }); if (!response.ok) { return { ok: false, status: response.status, - text: await readBoundedResponseText(response, `Open WebUI models attempt ${attempt}`), + text: await readBoundedResponseText( + response, + `Open WebUI models attempt ${attempt}`, + timeoutPromise, + ), }; } return { - json: await readBoundedResponseJson(response, `Open WebUI models attempt ${attempt}`), + json: await readBoundedResponseJson( + response, + `Open WebUI models attempt ${attempt}`, + timeoutPromise, + ), ok: true, }; }, @@ -174,25 +195,33 @@ async function fetchModels(authHeaders, attempt) { } async function fetchChatCompletion(authHeaders, targetModel) { - return await withRequestTimeout("Open WebUI chat completion", chatTimeoutMs, async (signal) => { - const response = await fetch(`${baseUrl}/api/chat/completions`, { - method: "POST", - headers: { - ...authHeaders, - "content-type": "application/json", - }, - body: JSON.stringify({ - model: targetModel, - messages: [{ role: "user", content: prompt }], - }), - signal, - }); - if (!response.ok) { - const body = await readBoundedResponseText(response, "Open WebUI chat completion"); - throw new Error(`/api/chat/completions failed: HTTP ${response.status} ${body}`); - } - return await readBoundedResponseJson(response, "Open WebUI chat completion"); - }); + return await withRequestTimeout( + "Open WebUI chat completion", + chatTimeoutMs, + async (signal, timeoutPromise) => { + const response = await fetch(`${baseUrl}/api/chat/completions`, { + method: "POST", + headers: { + ...authHeaders, + "content-type": "application/json", + }, + body: JSON.stringify({ + model: targetModel, + messages: [{ role: "user", content: prompt }], + }), + signal, + }); + if (!response.ok) { + const body = await readBoundedResponseText( + response, + "Open WebUI chat completion", + timeoutPromise, + ); + throw new Error(`/api/chat/completions failed: HTTP ${response.status} ${body}`); + } + return await readBoundedResponseJson(response, "Open WebUI chat completion", timeoutPromise); + }, + ); } function extractModelIds(modelsJson) { diff --git a/test/scripts/openwebui-probe.test.ts b/test/scripts/openwebui-probe.test.ts index 4b1169e9d3a..6ec9d6cabe0 100644 --- a/test/scripts/openwebui-probe.test.ts +++ b/test/scripts/openwebui-probe.test.ts @@ -1,5 +1,6 @@ // Openwebui Probe tests cover openwebui probe script behavior. import { spawn } from "node:child_process"; +import { readFileSync } from "node:fs"; import { createServer, type IncomingMessage, type Server as HttpServer } from "node:http"; import { createServer as createTcpServer, type Server as TcpServer, type Socket } from "node:net"; import path from "node:path"; @@ -186,6 +187,16 @@ describe("scripts/e2e/openwebui-probe.mjs", () => { } }); + it("passes Open WebUI request timeouts into bounded body reads", () => { + const script = readFileSync(probePath, "utf8"); + + expect(script).toContain("run(controller.signal, timeoutPromise)"); + expect(script).toMatch( + /readBoundedResponseTextWithLimit\(\s*response,\s*label,\s*responseBodyMaxBytes,\s*timeoutPromise,/u, + ); + expect(script.match(/async \(signal, timeoutPromise\)/gu)).toHaveLength(3); + }); + it("bounds sign-in error response bodies", async () => { const server = createServer((request, response) => { if (request.url === "/api/v1/auths/signin") {