mirror of https://github.com/halo-dev/halo
feat: support display HTML format in API error responses (#7127)
#### What type of PR is this? /kind feature /area ui /milestone 2.20.x #### What this PR does / why we need it: Add supports for display HTML format in API error responses See #7115 Examples: <img width="917" alt="image" src="https://github.com/user-attachments/assets/1ab4531c-3238-4e7d-ba24-d2425184a757"> <img width="942" alt="image" src="https://github.com/user-attachments/assets/54621b31-0629-4772-95fd-8587a7704ca3"> #### Which issue(s) this PR fixes: Fixes #7115 #### Special notes for your reviewer: Nginx mock example: ```nginx server { listen 80; server_name localhost; error_page 500 502 503 504 /50x.html; location = /50x.html { root /usr/share/nginx/html; } location / { proxy_pass http://localhost:8090; proxy_set_header HOST $host; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } location ^~ /apis/content.halo.run/v1alpha1/posts/ { return 403; } } ``` #### Does this PR introduce a user-facing change? ```release-note 支持显示来自反向代理或者 WAF 的请求错误信息 ```pull/7123/head^2
parent
0e9466d29c
commit
41ea81cddd
|
@ -86,6 +86,7 @@
|
|||
"fuse.js": "^6.6.2",
|
||||
"jsencrypt": "^3.3.2",
|
||||
"lodash-es": "^4.17.21",
|
||||
"object-hash": "^3.0.0",
|
||||
"overlayscrollbars": "^2.5.0",
|
||||
"overlayscrollbars-vue": "^0.5.7",
|
||||
"path-browserify": "^1.0.1",
|
||||
|
@ -114,6 +115,7 @@
|
|||
"@types/jsdom": "^20.0.1",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/node": "^18.11.19",
|
||||
"@types/object-hash": "^3.0.6",
|
||||
"@types/qs": "^6.9.7",
|
||||
"@types/randomstring": "^1.1.8",
|
||||
"@types/ua-parser-js": "^0.7.39",
|
||||
|
|
|
@ -158,6 +158,9 @@ importers:
|
|||
lodash-es:
|
||||
specifier: ^4.17.21
|
||||
version: 4.17.21
|
||||
object-hash:
|
||||
specifier: ^3.0.0
|
||||
version: 3.0.0
|
||||
overlayscrollbars:
|
||||
specifier: ^2.5.0
|
||||
version: 2.5.0
|
||||
|
@ -237,6 +240,9 @@ importers:
|
|||
'@types/node':
|
||||
specifier: ^18.11.19
|
||||
version: 18.13.0
|
||||
'@types/object-hash':
|
||||
specifier: ^3.0.6
|
||||
version: 3.0.6
|
||||
'@types/qs':
|
||||
specifier: ^6.9.7
|
||||
version: 6.9.7
|
||||
|
@ -4315,6 +4321,9 @@ packages:
|
|||
'@types/normalize-package-data@2.4.1':
|
||||
resolution: {integrity: sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==}
|
||||
|
||||
'@types/object-hash@3.0.6':
|
||||
resolution: {integrity: sha512-fOBV8C1FIu2ELinoILQ+ApxcUKz4ngq+IWUYrxSGjXzzjUALijilampwkMgEtJ+h2njAW3pi853QpzNVCHB73w==}
|
||||
|
||||
'@types/pretty-hrtime@1.0.3':
|
||||
resolution: {integrity: sha512-nj39q0wAIdhwn7DGUyT9irmsKK1tV0bd5WFEhgpqNTMFZ8cE+jieuTphCW0tfdm47S2zVT5mr09B28b1chmQMA==}
|
||||
|
||||
|
@ -6280,6 +6289,7 @@ packages:
|
|||
eslint@8.43.0:
|
||||
resolution: {integrity: sha512-aaCpf2JqqKesMFGgmRPessmVKjcGXqdlAYLLC3THM8t5nBRZRQ+st5WM/hoJXkdioEXLLbXgclUpM0TXo5HX5Q==}
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options.
|
||||
hasBin: true
|
||||
|
||||
espree@9.5.2:
|
||||
|
@ -15309,6 +15319,8 @@ snapshots:
|
|||
|
||||
'@types/normalize-package-data@2.4.1': {}
|
||||
|
||||
'@types/object-hash@3.0.6': {}
|
||||
|
||||
'@types/pretty-hrtime@1.0.3': {}
|
||||
|
||||
'@types/prop-types@15.7.11': {}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<script lang="ts" setup>
|
||||
import { i18n } from "@/locales";
|
||||
import type { ProblemDetail } from "@/setup/setupApiClient";
|
||||
import { createHTMLContentModal } from "@/utils/modal";
|
||||
import { Toast } from "@halo-dev/components";
|
||||
import type { Restrictions } from "@uppy/core";
|
||||
import Uppy, { type SuccessResponse } from "@uppy/core";
|
||||
|
@ -13,7 +14,8 @@ import zh_CN from "@uppy/locales/lib/zh_CN";
|
|||
import zh_TW from "@uppy/locales/lib/zh_TW";
|
||||
import { Dashboard } from "@uppy/vue";
|
||||
import XHRUpload from "@uppy/xhr-upload";
|
||||
import { computed, onUnmounted } from "vue";
|
||||
import objectHash from "object-hash";
|
||||
import { computed, h, onUnmounted } from "vue";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
|
@ -91,6 +93,40 @@ const uppy = computed(() => {
|
|||
const responseBody = response as XMLHttpRequest;
|
||||
const { status, statusText } = responseBody;
|
||||
const defaultMessage = [status, statusText].join(": ");
|
||||
|
||||
// Catch error requests where the response is text/html,
|
||||
// which usually comes from a reverse proxy or WAF
|
||||
// fixme: Because there is no responseType in the response, we can only judge it in this way for now.
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(
|
||||
responseBody.response,
|
||||
"text/html"
|
||||
);
|
||||
|
||||
if (
|
||||
Array.from(doc.body.childNodes).some((node) => node.nodeType === 1)
|
||||
) {
|
||||
createHTMLContentModal({
|
||||
uniqueId: objectHash(responseBody.response || ""),
|
||||
title: responseBody.status.toString(),
|
||||
width: 700,
|
||||
height: "calc(100vh - 20px)",
|
||||
centered: true,
|
||||
content: h("iframe", {
|
||||
srcdoc: responseBody.response,
|
||||
sandbox: "",
|
||||
referrerpolicy: "no-referrer",
|
||||
loading: "lazy",
|
||||
style: {
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
return new Error(defaultMessage);
|
||||
}
|
||||
|
||||
Toast.error(defaultMessage, { duration: 5000 });
|
||||
return new Error(defaultMessage);
|
||||
}
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
import { i18n } from "@/locales";
|
||||
import { createHTMLContentModal } from "@/utils/modal";
|
||||
import { axiosInstance } from "@halo-dev/api-client";
|
||||
import { Dialog, Toast } from "@halo-dev/components";
|
||||
import type { AxiosError } from "axios";
|
||||
import objectHash from "object-hash";
|
||||
import { h } from "vue";
|
||||
|
||||
export interface ProblemDetail {
|
||||
detail: string;
|
||||
|
@ -16,7 +19,7 @@ export function setupApiClient() {
|
|||
(response) => {
|
||||
return response;
|
||||
},
|
||||
async (error: AxiosError<ProblemDetail>) => {
|
||||
async (error: AxiosError) => {
|
||||
if (error.code === "ERR_CANCELED") {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
@ -41,7 +44,7 @@ export function setupApiClient() {
|
|||
}
|
||||
|
||||
const { status } = errorResponse;
|
||||
const { title, detail } = errorResponse.data;
|
||||
const { title, detail } = errorResponse.data as ProblemDetail;
|
||||
|
||||
if (status === 401) {
|
||||
Dialog.warning({
|
||||
|
@ -64,6 +67,33 @@ export function setupApiClient() {
|
|||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
// Catch error requests where the response is text/html,
|
||||
// which usually comes from a reverse proxy or WAF
|
||||
|
||||
const contentType = error.response?.headers["content-type"];
|
||||
|
||||
if (contentType === "text/html") {
|
||||
createHTMLContentModal({
|
||||
uniqueId: objectHash(error.response?.data || ""),
|
||||
title: error.response?.status.toString(),
|
||||
width: 700,
|
||||
height: "calc(100vh - 20px)",
|
||||
centered: true,
|
||||
content: h("iframe", {
|
||||
srcdoc: error.response?.data?.toString(),
|
||||
sandbox: "",
|
||||
referrerpolicy: "no-referrer",
|
||||
loading: "lazy",
|
||||
style: {
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
if (title || detail) {
|
||||
Toast.error(detail || title);
|
||||
return Promise.reject(error);
|
||||
|
|
|
@ -0,0 +1,75 @@
|
|||
import { i18n } from "@/locales";
|
||||
import { VButton, VModal } from "@halo-dev/components";
|
||||
import { type Component, createApp, h } from "vue";
|
||||
|
||||
interface ModalOptions {
|
||||
uniqueId?: string;
|
||||
title?: string;
|
||||
width?: number;
|
||||
height?: string;
|
||||
centered?: boolean;
|
||||
content: Component;
|
||||
}
|
||||
|
||||
export function createHTMLContentModal(options: ModalOptions) {
|
||||
if (options.uniqueId) {
|
||||
const existingModal = document.getElementById(`modal-${options.uniqueId}`);
|
||||
if (existingModal) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const container = document.createElement("div");
|
||||
if (options.uniqueId) {
|
||||
container.id = `modal-${options.uniqueId}`;
|
||||
}
|
||||
|
||||
document.body.appendChild(container);
|
||||
|
||||
const app = createApp({
|
||||
setup() {
|
||||
const handleClose = () => {
|
||||
app.unmount();
|
||||
container.remove();
|
||||
};
|
||||
|
||||
return () =>
|
||||
h(
|
||||
VModal,
|
||||
{
|
||||
title: options.title,
|
||||
width: options.width || 500,
|
||||
height: options.height,
|
||||
centered: options.centered ?? true,
|
||||
onClose: handleClose,
|
||||
"onUpdate:visible": (value: boolean) => {
|
||||
if (!value) handleClose();
|
||||
},
|
||||
},
|
||||
{
|
||||
default: () => options.content,
|
||||
footer: () =>
|
||||
h(
|
||||
VButton,
|
||||
{
|
||||
onClick: handleClose,
|
||||
},
|
||||
{
|
||||
default: () =>
|
||||
h("div", i18n.global.t("core.common.buttons.close")),
|
||||
}
|
||||
),
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
app.mount(container);
|
||||
|
||||
return {
|
||||
close: () => {
|
||||
app.unmount();
|
||||
container.remove();
|
||||
},
|
||||
};
|
||||
}
|
Loading…
Reference in New Issue