Skip to content

Commit

Permalink
feat: support display HTML format in API error responses (#7127)
Browse files Browse the repository at this point in the history
#### 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 的请求错误信息
```
  • Loading branch information
ruibaby authored Dec 16, 2024
1 parent 0e9466d commit 41ea81c
Show file tree
Hide file tree
Showing 5 changed files with 158 additions and 3 deletions.
2 changes: 2 additions & 0 deletions ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
12 changes: 12 additions & 0 deletions ui/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

38 changes: 37 additions & 1 deletion ui/src/components/upload/UppyUpload.vue
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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<{
Expand Down Expand Up @@ -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);
}
Expand Down
34 changes: 32 additions & 2 deletions ui/src/setup/setupApiClient.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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);
}
Expand All @@ -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({
Expand All @@ -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);
Expand Down
75 changes: 75 additions & 0 deletions ui/src/utils/modal.ts
Original file line number Diff line number Diff line change
@@ -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();
},
};
}

0 comments on commit 41ea81c

Please sign in to comment.