diff --git a/actions/setup/js/error_helpers.cjs b/actions/setup/js/error_helpers.cjs index aaba9e84839..692bea862ef 100644 --- a/actions/setup/js/error_helpers.cjs +++ b/actions/setup/js/error_helpers.cjs @@ -1,20 +1,46 @@ // @ts-check +/** + * Detect whether a string looks like an HTML page rather than a plain error message. + * Used to sanitize GitHub's "Unicorn" / gateway error HTML responses so they don't + * pollute CI logs with hundreds of lines of markup. + * + * @param {string} str - The string to inspect + * @returns {boolean} True when the string appears to be an HTML document + */ +function isHtmlContent(str) { + return /^\s*]/i.test(str); +} + /** * Safely extract an error message from an unknown error value. * Handles Error instances, objects with message properties, and other values. * + * When the extracted message looks like an HTML page (e.g. GitHub's "Unicorn" + * 504 error page), it is replaced with a concise human-readable description so + * that CI logs stay readable. The HTTP status code is included when available. + * * @param {unknown} error - The error value to extract a message from * @returns {string} The error message as a string */ function getErrorMessage(error) { + // prettier-ignore + const errorAsAny = /** @type {any} */ (error); + let message; if (error instanceof Error) { - return error.message; + message = error.message; + } else if (error && typeof error === "object" && "message" in error && typeof error.message === "string") { + message = error.message; + } else { + message = String(error); } - if (error && typeof error === "object" && "message" in error && typeof error.message === "string") { - return error.message; + + if (isHtmlContent(message)) { + const status = errorAsAny != null && typeof errorAsAny.status === "number" ? errorAsAny.status : null; + return status != null ? `GitHub returned an unexpected HTML response (HTTP ${status})` : "GitHub returned an unexpected HTML response"; } - return String(error); + + return message; } /** @@ -54,4 +80,4 @@ function isRateLimitError(error) { return /\bapi rate limit\b|\brate limit exceeded\b/i.test(errorMessage); } -module.exports = { getErrorMessage, isLockedError, isRateLimitError }; +module.exports = { getErrorMessage, isHtmlContent, isLockedError, isRateLimitError }; diff --git a/actions/setup/js/error_helpers.test.cjs b/actions/setup/js/error_helpers.test.cjs index 7aa5357215c..4e31f7ff3a7 100644 --- a/actions/setup/js/error_helpers.test.cjs +++ b/actions/setup/js/error_helpers.test.cjs @@ -1,5 +1,5 @@ import { describe, it, expect } from "vitest"; -import { getErrorMessage, isLockedError, isRateLimitError } from "./error_helpers.cjs"; +import { getErrorMessage, isHtmlContent, isLockedError, isRateLimitError } from "./error_helpers.cjs"; describe("error_helpers", () => { describe("getErrorMessage", () => { @@ -38,6 +38,71 @@ describe("error_helpers", () => { const error = { code: "ERROR_CODE", status: 500 }; expect(getErrorMessage(error)).toBe("[object Object]"); }); + + it("should sanitize HTML DOCTYPE error response with status", () => { + const html = "\nUnicorn!..."; + const error = new Error(html); + /** @type {any} */ error.status = 504; + expect(getErrorMessage(error)).toBe("GitHub returned an unexpected HTML response (HTTP 504)"); + }); + + it("should sanitize HTML DOCTYPE error response without status", () => { + const html = "\nUnicorn!..."; + const error = new Error(html); + expect(getErrorMessage(error)).toBe("GitHub returned an unexpected HTML response"); + }); + + it("should sanitize bare error response with status", () => { + const html = "Service Unavailable..."; + const error = { message: html, status: 503 }; + expect(getErrorMessage(error)).toBe("GitHub returned an unexpected HTML response (HTTP 503)"); + }); + + it("should sanitize html with leading whitespace", () => { + const html = " \n..."; + const error = new Error(html); + expect(getErrorMessage(error)).toBe("GitHub returned an unexpected HTML response"); + }); + + it("should sanitize raw HTML string throw", () => { + const html = "Unicorn"; + expect(getErrorMessage(html)).toBe("GitHub returned an unexpected HTML response"); + }); + + it("should not sanitize plain-text error messages that happen to mention html", () => { + const error = new Error("Validation failed: invalid html content provided"); + expect(getErrorMessage(error)).toBe("Validation failed: invalid html content provided"); + }); + }); + + describe("isHtmlContent", () => { + it("should return true for DOCTYPE HTML string", () => { + expect(isHtmlContent("")).toBe(true); + }); + + it("should return true for bare html tag", () => { + expect(isHtmlContent("")).toBe(true); + }); + + it("should return true with leading whitespace", () => { + expect(isHtmlContent("\n ...")).toBe(true); + }); + + it("should return true for case-insensitive DOCTYPE", () => { + expect(isHtmlContent("")).toBe(true); + }); + + it("should return false for plain text", () => { + expect(isHtmlContent("Resource not accessible by integration")).toBe(false); + }); + + it("should return false for JSON-like content", () => { + expect(isHtmlContent('{"message":"Not Found","documentation_url":"..."}')).toBe(false); + }); + + it("should return false for empty string", () => { + expect(isHtmlContent("")).toBe(false); + }); }); describe("isLockedError", () => { diff --git a/actions/setup/js/error_recovery.cjs b/actions/setup/js/error_recovery.cjs index 96a5ab9edd2..fb32048be84 100644 --- a/actions/setup/js/error_recovery.cjs +++ b/actions/setup/js/error_recovery.cjs @@ -65,7 +65,7 @@ function isTransientError(error) { // GitHub REST APIs may crash and return an HTML error page (e.g. the "Unicorn!" // 500 page) instead of JSON. Detect this by checking for an HTML doctype at the // start of the error message and treat it as a transient server error. - if (errorMsgLower.startsWith("