This is an automated email from the ASF dual-hosted git repository.
dengliming pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/shenyu-dashboard.git
The following commit(s) were added to refs/heads/master by this push:
new e8988c65 fix(ApiDebug): render non-JSON response bodies as plain text
(#708)
e8988c65 is described below
commit e8988c65f46bcdb94b37ce5b24b66631418c36a6
Author: Bhaskar Gurram <[email protected]>
AuthorDate: Sat Oct 10 12:53:44 2026 -0400
fix(ApiDebug): render non-JSON response bodies as plain text (#708)
The API debug panel passed non-JSON upstream response bodies to
ReactHtmlParser, so markup returned by the gateway (styles, forms,
images, embedded content) became live elements inside the dashboard.
Render those bodies as text inside a <pre> with wrapped, preserved
whitespace, matching ImportResultModal. React escapes the text, so
markup is shown literally and no elements are created. JSON bodies are
still shown with ReactJson.
react-html-parser has no other users, so drop it from package.json and
package-lock.json, and remove its mock from ApiDebug.state.test.js. Add
a test that a markup body is rendered as literal text.
Fixes #614
---
package-lock.json | 107 +--------------------
package.json | 1 -
src/routes/Document/components/ApiDebug.js | 6 +-
.../Document/components/ApiDebug.state.test.js | 1 -
src/routes/Document/components/ApiDebug.test.js | 31 ++++++
5 files changed, 39 insertions(+), 107 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index daecd885..ba3a5647 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -31,7 +31,6 @@
"react-container-query": "^0.11.0",
"react-document-title": "^2.0.3",
"react-dom": "^16.13.1",
- "react-html-parser": "^2.0.2",
"react-intl-universal": "^2.4.2",
"react-json-view": "^1.21.3",
"react-resizable": "^1.11.0",
@@ -21661,6 +21660,7 @@
"version": "2.0.4",
"resolved":
"https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz",
"integrity":
"sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "dev": true,
"license": "ISC"
},
"node_modules/ini": {
@@ -36437,108 +36437,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/react-html-parser": {
- "version": "2.0.2",
- "resolved":
"https://registry.npmmirror.com/react-html-parser/-/react-html-parser-2.0.2.tgz",
- "integrity":
"sha512-XeerLwCVjTs3njZcgCOeDUqLgNIt/t+6Jgi5/qPsO/krUWl76kWKXMeVs2LhY2gwM6X378DkhLjur0zUQdpz0g==",
- "license": "MIT",
- "dependencies": {
- "htmlparser2": "^3.9.0"
- },
- "peerDependencies": {
- "react": "^0.14.0 || ^15.0.0 || ^16.0.0-0"
- }
- },
- "node_modules/react-html-parser/node_modules/dom-serializer": {
- "version": "0.2.2",
- "resolved":
"https://registry.npmmirror.com/dom-serializer/-/dom-serializer-0.2.2.tgz",
- "integrity":
"sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==",
- "license": "MIT",
- "dependencies": {
- "domelementtype": "^2.0.1",
- "entities": "^2.0.0"
- }
- },
-
"node_modules/react-html-parser/node_modules/dom-serializer/node_modules/domelementtype":
{
- "version": "2.3.0",
- "resolved":
"https://registry.npmmirror.com/domelementtype/-/domelementtype-2.3.0.tgz",
- "integrity":
"sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/fb55"
- }
- ],
- "license": "BSD-2-Clause"
- },
-
"node_modules/react-html-parser/node_modules/dom-serializer/node_modules/entities":
{
- "version": "2.2.0",
- "resolved":
"https://registry.npmmirror.com/entities/-/entities-2.2.0.tgz",
- "integrity":
"sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==",
- "license": "BSD-2-Clause",
- "funding": {
- "url": "https://github.com/fb55/entities?sponsor=1"
- }
- },
- "node_modules/react-html-parser/node_modules/domelementtype": {
- "version": "1.3.1",
- "resolved":
"https://registry.npmmirror.com/domelementtype/-/domelementtype-1.3.1.tgz",
- "integrity":
"sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==",
- "license": "BSD-2-Clause"
- },
- "node_modules/react-html-parser/node_modules/domhandler": {
- "version": "2.4.2",
- "resolved":
"https://registry.npmmirror.com/domhandler/-/domhandler-2.4.2.tgz",
- "integrity":
"sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==",
- "license": "BSD-2-Clause",
- "dependencies": {
- "domelementtype": "1"
- }
- },
- "node_modules/react-html-parser/node_modules/domutils": {
- "version": "1.7.0",
- "resolved":
"https://registry.npmmirror.com/domutils/-/domutils-1.7.0.tgz",
- "integrity":
"sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==",
- "license": "BSD-2-Clause",
- "dependencies": {
- "dom-serializer": "0",
- "domelementtype": "1"
- }
- },
- "node_modules/react-html-parser/node_modules/entities": {
- "version": "1.1.2",
- "resolved":
"https://registry.npmmirror.com/entities/-/entities-1.1.2.tgz",
- "integrity":
"sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==",
- "license": "BSD-2-Clause"
- },
- "node_modules/react-html-parser/node_modules/htmlparser2": {
- "version": "3.10.1",
- "resolved":
"https://registry.npmmirror.com/htmlparser2/-/htmlparser2-3.10.1.tgz",
- "integrity":
"sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==",
- "license": "MIT",
- "dependencies": {
- "domelementtype": "^1.3.1",
- "domhandler": "^2.3.0",
- "domutils": "^1.5.1",
- "entities": "^1.1.1",
- "inherits": "^2.0.1",
- "readable-stream": "^3.1.1"
- }
- },
- "node_modules/react-html-parser/node_modules/readable-stream": {
- "version": "3.6.2",
- "resolved":
"https://registry.npmmirror.com/readable-stream/-/readable-stream-3.6.2.tgz",
- "integrity":
"sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
- "license": "MIT",
- "dependencies": {
- "inherits": "^2.0.3",
- "string_decoder": "^1.1.1",
- "util-deprecate": "^1.0.1"
- },
- "engines": {
- "node": ">= 6"
- }
- },
"node_modules/react-intl-universal": {
"version": "2.13.4",
"resolved":
"https://registry.npmmirror.com/react-intl-universal/-/react-intl-universal-2.13.4.tgz",
@@ -38213,6 +38111,7 @@
"version": "5.1.2",
"resolved":
"https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity":
"sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+ "dev": true,
"license": "MIT"
},
"node_modules/safe-push-apply": {
@@ -39648,6 +39547,7 @@
"version": "1.1.1",
"resolved":
"https://registry.npmmirror.com/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity":
"sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.1.0"
@@ -44533,6 +44433,7 @@
"version": "1.0.2",
"resolved":
"https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity":
"sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+ "dev": true,
"license": "MIT"
},
"node_modules/util.promisify": {
diff --git a/package.json b/package.json
index 1b10fbd2..fb53077c 100644
--- a/package.json
+++ b/package.json
@@ -46,7 +46,6 @@
"react-container-query": "^0.11.0",
"react-document-title": "^2.0.3",
"react-dom": "^16.13.1",
- "react-html-parser": "^2.0.2",
"react-intl-universal": "^2.4.2",
"react-json-view": "^1.21.3",
"react-resizable": "^1.11.0",
diff --git a/src/routes/Document/components/ApiDebug.js
b/src/routes/Document/components/ApiDebug.js
index 3098c512..e33ea306 100644
--- a/src/routes/Document/components/ApiDebug.js
+++ b/src/routes/Document/components/ApiDebug.js
@@ -37,7 +37,6 @@ import React, {
useState,
} from "react";
import ReactJson from "react-json-view";
-import ReactHtmlParser from "react-html-parser";
import fetch from "dva/fetch";
import {
createOrUpdateMockRequest,
@@ -501,7 +500,10 @@ function ApiDebug() {
Object.keys(responseInfo).length ? (
<ReactJson src={responseInfo.body} name={false} />
) : responseInfo.body ? (
- ReactHtmlParser(responseInfo.body)
+ // Non-JSON upstream bodies are untrusted: show them as text only.
+ <pre style={{ whiteSpace: "pre-wrap", wordWrap: "break-word" }}>
+ {responseInfo.body}
+ </pre>
) : (
<Empty description={false} />
)}
diff --git a/src/routes/Document/components/ApiDebug.state.test.js
b/src/routes/Document/components/ApiDebug.state.test.js
index 31422397..cb89d475 100644
--- a/src/routes/Document/components/ApiDebug.state.test.js
+++ b/src/routes/Document/components/ApiDebug.state.test.js
@@ -41,7 +41,6 @@ jest.mock("antd", () => {
});
jest.mock("dva/fetch", () => jest.fn());
-jest.mock("react-html-parser", () => jest.fn(() => null));
jest.mock("react-json-view", () => () => null);
jest.mock("./HeadersEditor", () => {
const ReactModule = jest.requireActual("react");
diff --git a/src/routes/Document/components/ApiDebug.test.js
b/src/routes/Document/components/ApiDebug.test.js
index dec22674..f8809636 100644
--- a/src/routes/Document/components/ApiDebug.test.js
+++ b/src/routes/Document/components/ApiDebug.test.js
@@ -131,3 +131,34 @@ it("validates HTTP(S) URLs with or without an explicit
port", () => {
expect(isValidHttpUrl("http://api.example.test:8080/orders")).toBe(true);
expect(isValidHttpUrl("ftp://api.example.test/orders")).toBe(false);
});
+
+it("renders a non-JSON response body as literal text instead of HTML", async
() => {
+ const body =
+ '<img id="injected" src="x"><b>upstream</b>\n <form id="fake"></form>';
+ fetch.mockResolvedValue({
+ text: async () => body,
+ headers: { get: () => null },
+ });
+ jest.spyOn(console, "error").mockImplementation(() => {});
+
+ const requestUrl = "https://api.example.test/orders";
+ const { container } = render(
+ <ApiContext.Provider
+ value={{
+ ...contextValue,
+ apiDetail: { ...contextValue.apiDetail, apiPath: requestUrl },
+ }}
+ >
+ <ApiDebug />
+ </ApiContext.Provider>,
+ );
+
+ fireEvent.submit(container.querySelector("form"));
+
+ await waitFor(() => expect(container.querySelector("pre")).not.toBeNull());
+ const pre = container.querySelector("pre");
+ expect(pre.textContent).toBe(body);
+ expect(pre.children).toHaveLength(0);
+ expect(container.querySelector("#injected")).toBeNull();
+ expect(container.querySelector("#fake")).toBeNull();
+});