mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 04:47:03 -06:00
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.
This commit is contained in:
@@ -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<!doctype html><html><body>down</body></html>";
|
||||
|
||||
expect(classifyProviderRuntimeFailureKind(raw)).toBe("upstream_html");
|
||||
});
|
||||
|
||||
it("classifies generic resource-exhausted codes as rate_limit", () => {
|
||||
expect(
|
||||
classifyProviderRuntimeFailureKind({
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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\n<!doctype html><html><body>down</body></html>";
|
||||
expect(isCloudflareOrHtmlErrorPage(htmlError)).toBe(true);
|
||||
});
|
||||
|
||||
it("does not flag partial HTML after an HTTP reason phrase", () => {
|
||||
const partialHtml = "HTTP 502 Bad Gateway\n\n<!doctype html><html><body>down";
|
||||
expect(isCloudflareOrHtmlErrorPage(partialHtml)).toBe(false);
|
||||
});
|
||||
|
||||
it("detects standalone Cloudflare challenge HTML pages", () => {
|
||||
// HTML challenge pages are provider transport failures, not model text.
|
||||
const htmlError = `<!DOCTYPE html>
|
||||
|
||||
@@ -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",
|
||||
"",
|
||||
"<!doctype html><html><body><h1>502</h1></body></html>",
|
||||
].join("\n");
|
||||
|
||||
expect(formatRawAssistantErrorForUi(raw)).toBe(
|
||||
"The AI service is temporarily unavailable (HTTP 502). Please try again in a moment.",
|
||||
);
|
||||
});
|
||||
|
||||
it("formats only status lines inside the HTTP range", () => {
|
||||
expect(formatRawAssistantErrorForUi("100 Continue")).toBe("HTTP 100: Continue");
|
||||
expect(formatRawAssistantErrorForUi("599 Provider Error")).toBe("HTTP 599: Provider Error");
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Assistant error formatting helpers normalize assistant-visible error payloads.
|
||||
import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
|
||||
import { extractHttpResponseBody } from "./http-error-response.js";
|
||||
const ERROR_PAYLOAD_PREFIX_RE =
|
||||
/^(?:error|(?:[a-z][\w-]*\s+)?api\s*error|apierror|openai\s*error|anthropic\s*error|gateway\s*error|codex\s*error)(?:\s+\d{3})?[:\s-]+/i;
|
||||
const HTTP_STATUS_DELIMITER_RE = /(?:\s*:\s*|\s+)/;
|
||||
@@ -160,7 +161,7 @@ export function isCloudflareOrHtmlErrorPage(raw: string): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
const status = extractLeadingHttpStatus(trimmed);
|
||||
const status = extractHttpResponseBody(extractLeadingHttpStatus(trimmed));
|
||||
if (!status || status.code < 500) {
|
||||
return false;
|
||||
}
|
||||
@@ -170,7 +171,7 @@ export function isCloudflareOrHtmlErrorPage(raw: string): boolean {
|
||||
}
|
||||
|
||||
return (
|
||||
status.code < 600 && HTML_ERROR_PREFIX_RE.test(status.rest) && HTML_CLOSE_RE.test(status.rest)
|
||||
status.code < 600 && HTML_ERROR_PREFIX_RE.test(status.body) && HTML_CLOSE_RE.test(status.body)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
const HTML_ERROR_PREFIX_RE = /^\s*(?:<!doctype\s+html\b|<html\b)/i;
|
||||
|
||||
export function extractHttpResponseBody(
|
||||
status: { code: number; rest: string } | null,
|
||||
): { code: number; body: string } | null {
|
||||
if (!status) {
|
||||
return null;
|
||||
}
|
||||
if (HTML_ERROR_PREFIX_RE.test(status.rest)) {
|
||||
return { code: status.code, body: status.rest };
|
||||
}
|
||||
const lineBreak = status.rest.indexOf("\n");
|
||||
return {
|
||||
code: status.code,
|
||||
body: lineBreak === -1 ? status.rest : status.rest.slice(lineBreak + 1).trimStart(),
|
||||
};
|
||||
}
|
||||
@@ -12,11 +12,25 @@ import {
|
||||
isolateRtlRenderedLine,
|
||||
isTerminalSafeAutocompleteValue,
|
||||
isCommandMarkedMessage,
|
||||
resolveFinalAssistantText,
|
||||
sanitizeMarkdownSource,
|
||||
sanitizeRenderableLine,
|
||||
sanitizeRenderableText,
|
||||
} from "./tui-formatters.js";
|
||||
|
||||
describe("resolveFinalAssistantText", () => {
|
||||
it("hides complete HTML error pages after an HTTP reason phrase", () => {
|
||||
const raw = "HTTP 502 Bad Gateway\n\n<!doctype html><html><body>down</body></html>";
|
||||
|
||||
const rendered = resolveFinalAssistantText({ errorMessage: raw });
|
||||
|
||||
expect(rendered).toBe(
|
||||
"The AI service is temporarily unavailable (HTTP 502). Please try again in a moment.",
|
||||
);
|
||||
expect(rendered).not.toContain("<html>");
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatTuiFooter", () => {
|
||||
it("shows session modes and the process delivery mode in one compact summary", () => {
|
||||
expect(
|
||||
|
||||
Reference in New Issue
Block a user