Skip to content

Commit c4e1ec8

Browse files
Copilotpelikhan
andauthored
fix: sanitize HTML error pages in getErrorMessage to avoid noisy logs
When GitHub returns a 504/5xx response with an HTML body (e.g. the "Unicorn" error page), getErrorMessage was returning the full raw HTML as the error message. This caused CI logs to be flooded with hundreds of lines of HTML markup. Add isHtmlContent() to detect DOCTYPE/html responses and modify getErrorMessage() to replace them with a concise human-readable message that includes the HTTP status code when available. Fixes: Review finalization failed: <!DOCTYPE html>... Now shows: Review finalization failed: GitHub returned an unexpected HTML response (HTTP 504) Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
1 parent 4d91040 commit c4e1ec8

2 files changed

Lines changed: 90 additions & 6 deletions

File tree

actions/setup/js/error_helpers.cjs

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,44 @@
11
// @ts-check
22

3+
/**
4+
* Detect whether a string looks like an HTML page rather than a plain error message.
5+
* Used to sanitize GitHub's "Unicorn" / gateway error HTML responses so they don't
6+
* pollute CI logs with hundreds of lines of markup.
7+
*
8+
* @param {string} str - The string to inspect
9+
* @returns {boolean} True when the string appears to be an HTML document
10+
*/
11+
function isHtmlContent(str) {
12+
return /^\s*<!DOCTYPE\s/i.test(str) || /^\s*<html[\s>]/i.test(str);
13+
}
14+
315
/**
416
* Safely extract an error message from an unknown error value.
517
* Handles Error instances, objects with message properties, and other values.
618
*
19+
* When the extracted message looks like an HTML page (e.g. GitHub's "Unicorn"
20+
* 504 error page), it is replaced with a concise human-readable description so
21+
* that CI logs stay readable. The HTTP status code is included when available.
22+
*
723
* @param {unknown} error - The error value to extract a message from
824
* @returns {string} The error message as a string
925
*/
1026
function getErrorMessage(error) {
27+
let message;
1128
if (error instanceof Error) {
12-
return error.message;
29+
message = error.message;
30+
} else if (error && typeof error === "object" && "message" in error && typeof error.message === "string") {
31+
message = error.message;
32+
} else {
33+
return String(error);
1334
}
14-
if (error && typeof error === "object" && "message" in error && typeof error.message === "string") {
15-
return error.message;
35+
36+
if (isHtmlContent(message)) {
37+
const status = error && typeof error === "object" && "status" in error && typeof (/** @type {any} */ error.status) === "number" ? /** @type {any} */ error.status : null;
38+
return status != null ? `GitHub returned an unexpected HTML response (HTTP ${status})` : "GitHub returned an unexpected HTML response";
1639
}
17-
return String(error);
40+
41+
return message;
1842
}
1943

2044
/**
@@ -54,4 +78,4 @@ function isRateLimitError(error) {
5478
return /\bapi rate limit\b|\brate limit exceeded\b/i.test(errorMessage);
5579
}
5680

57-
module.exports = { getErrorMessage, isLockedError, isRateLimitError };
81+
module.exports = { getErrorMessage, isHtmlContent, isLockedError, isRateLimitError };

actions/setup/js/error_helpers.test.cjs

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { describe, it, expect } from "vitest";
2-
import { getErrorMessage, isLockedError, isRateLimitError } from "./error_helpers.cjs";
2+
import { getErrorMessage, isHtmlContent, isLockedError, isRateLimitError } from "./error_helpers.cjs";
33

44
describe("error_helpers", () => {
55
describe("getErrorMessage", () => {
@@ -38,6 +38,66 @@ describe("error_helpers", () => {
3838
const error = { code: "ERROR_CODE", status: 500 };
3939
expect(getErrorMessage(error)).toBe("[object Object]");
4040
});
41+
42+
it("should sanitize HTML DOCTYPE error response with status", () => {
43+
const html = "<!DOCTYPE html>\n<html><head><title>Unicorn!</title></head><body>...</body></html>";
44+
const error = new Error(html);
45+
/** @type {any} */ error.status = 504;
46+
expect(getErrorMessage(error)).toBe("GitHub returned an unexpected HTML response (HTTP 504)");
47+
});
48+
49+
it("should sanitize HTML DOCTYPE error response without status", () => {
50+
const html = "<!DOCTYPE html>\n<html><head><title>Unicorn!</title></head><body>...</body></html>";
51+
const error = new Error(html);
52+
expect(getErrorMessage(error)).toBe("GitHub returned an unexpected HTML response");
53+
});
54+
55+
it("should sanitize bare <html> error response with status", () => {
56+
const html = "<html><head><title>Service Unavailable</title></head><body>...</body></html>";
57+
const error = { message: html, status: 503 };
58+
expect(getErrorMessage(error)).toBe("GitHub returned an unexpected HTML response (HTTP 503)");
59+
});
60+
61+
it("should sanitize html with leading whitespace", () => {
62+
const html = " \n<!DOCTYPE html><html>...</html>";
63+
const error = new Error(html);
64+
expect(getErrorMessage(error)).toBe("GitHub returned an unexpected HTML response");
65+
});
66+
67+
it("should not sanitize plain-text error messages that happen to mention html", () => {
68+
const error = new Error("Validation failed: invalid html content provided");
69+
expect(getErrorMessage(error)).toBe("Validation failed: invalid html content provided");
70+
});
71+
});
72+
73+
describe("isHtmlContent", () => {
74+
it("should return true for DOCTYPE HTML string", () => {
75+
expect(isHtmlContent("<!DOCTYPE html><html></html>")).toBe(true);
76+
});
77+
78+
it("should return true for bare html tag", () => {
79+
expect(isHtmlContent("<html><head></head><body></body></html>")).toBe(true);
80+
});
81+
82+
it("should return true with leading whitespace", () => {
83+
expect(isHtmlContent("\n <!DOCTYPE html>...")).toBe(true);
84+
});
85+
86+
it("should return true for case-insensitive DOCTYPE", () => {
87+
expect(isHtmlContent("<!doctype HTML><html></html>")).toBe(true);
88+
});
89+
90+
it("should return false for plain text", () => {
91+
expect(isHtmlContent("Resource not accessible by integration")).toBe(false);
92+
});
93+
94+
it("should return false for JSON-like content", () => {
95+
expect(isHtmlContent('{"message":"Not Found","documentation_url":"..."}'));
96+
});
97+
98+
it("should return false for empty string", () => {
99+
expect(isHtmlContent("")).toBe(false);
100+
});
41101
});
42102

43103
describe("isLockedError", () => {

0 commit comments

Comments
 (0)