From a816c4a3b437c80a03419d65bebee1be4b2d7b28 Mon Sep 17 00:00:00 2001 From: Pengyi Peng <74917296+pengpengyi92@users.noreply.github.com> Date: Tue, 11 Aug 2026 21:40:58 +0000 Subject: [PATCH] fix(errors): detect HTML after HTTP reason phrases Share one internal HTTP response-body parser across assistant error formatting and provider runtime failure classification so reason phrases do not hide complete HTML error pages. Fixes #122244. --- .../provider-runtime-failure.test.ts | 6 ++++++ .../provider-runtime-failure.ts | 3 ++- src/shared/assistant-error-format.html.test.ts | 10 ++++++++++ src/shared/assistant-error-format.test.ts | 12 ++++++++++++ src/shared/assistant-error-format.ts | 5 +++-- src/shared/http-error-response.ts | 17 +++++++++++++++++ src/tui/tui-formatters.test.ts | 14 ++++++++++++++ 7 files changed, 64 insertions(+), 3 deletions(-) create mode 100644 src/shared/http-error-response.ts diff --git a/src/agents/embedded-agent-helpers/provider-runtime-failure.test.ts b/src/agents/embedded-agent-helpers/provider-runtime-failure.test.ts index fbc519819d3a..ee29fc7b14d1 100644 --- a/src/agents/embedded-agent-helpers/provider-runtime-failure.test.ts +++ b/src/agents/embedded-agent-helpers/provider-runtime-failure.test.ts @@ -17,6 +17,12 @@ function expectNotFailoverSample(sample: string) { } describe("classifyProviderRuntimeFailureKind", () => { + it("classifies complete HTML after an HTTP reason phrase as upstream_html", () => { + const raw = "HTTP 502 Bad Gateway\n\n
down"; + + expect(classifyProviderRuntimeFailureKind(raw)).toBe("upstream_html"); + }); + it("classifies generic resource-exhausted codes as rate_limit", () => { expect( classifyProviderRuntimeFailureKind({ diff --git a/src/agents/embedded-agent-helpers/provider-runtime-failure.ts b/src/agents/embedded-agent-helpers/provider-runtime-failure.ts index 94327e996277..56b71d254299 100644 --- a/src/agents/embedded-agent-helpers/provider-runtime-failure.ts +++ b/src/agents/embedded-agent-helpers/provider-runtime-failure.ts @@ -1,5 +1,6 @@ import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; import { extractLeadingHttpStatus } from "../../shared/assistant-error-format.js"; +import { extractHttpResponseBody } from "../../shared/http-error-response.js"; import { classifyOAuthRefreshFailure } from "../auth-profiles/oauth-refresh-failure.js"; import { formatExecDeniedUserMessage } from "../exec-approval-result.js"; import { @@ -70,7 +71,7 @@ function isHtmlErrorResponse(raw: string, status?: number): boolean { if (typeof inferred !== "number" || inferred < 400) { return false; } - const rest = extractLeadingHttpStatus(candidate)?.rest ?? candidate; + const rest = extractHttpResponseBody(extractLeadingHttpStatus(candidate))?.body ?? candidate; return HTML_BODY_RE.test(rest) && HTML_CLOSE_RE.test(rest); } function isCloudflareChallengeResponse(message: string): boolean { diff --git a/src/shared/assistant-error-format.html.test.ts b/src/shared/assistant-error-format.html.test.ts index 55dd903abf01..729e0fa895ba 100644 --- a/src/shared/assistant-error-format.html.test.ts +++ b/src/shared/assistant-error-format.html.test.ts @@ -17,6 +17,16 @@ describe("isCloudflareOrHtmlErrorPage", () => { expect(isCloudflareOrHtmlErrorPage(htmlError)).toBe(true); }); + it("detects complete 5xx HTML pages after an HTTP reason phrase", () => { + const htmlError = "HTTP 502 Bad Gateway\n\ndown"; + expect(isCloudflareOrHtmlErrorPage(htmlError)).toBe(true); + }); + + it("does not flag partial HTML after an HTTP reason phrase", () => { + const partialHtml = "HTTP 502 Bad Gateway\n\ndown"; + expect(isCloudflareOrHtmlErrorPage(partialHtml)).toBe(false); + }); + it("detects standalone Cloudflare challenge HTML pages", () => { // HTML challenge pages are provider transport failures, not model text. const htmlError = ` diff --git a/src/shared/assistant-error-format.test.ts b/src/shared/assistant-error-format.test.ts index 6093a3664cad..c3ca52044e2b 100644 --- a/src/shared/assistant-error-format.test.ts +++ b/src/shared/assistant-error-format.test.ts @@ -81,6 +81,18 @@ describe("extractErrorHttpStatus", () => { }); describe("HTTP status consumers", () => { + it("does not return raw HTML after an HTTP reason phrase", () => { + const raw = [ + "HTTP 502 Bad Gateway", + "", + "