refactor: OpenAPI URL 抓取改为前端执行 + 服务端 CORS 代理
- 前端直接 fetch URL 支持 localhost/内网地址 - CORS 失败自动回退到服务端代理 /api/fetch-spec - 添加 js-yaml 支持 YAML 格式解析 - 服务端移除 specUrl 参数,只接收已解析的 spec 对象 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/vite": "^4.2.2",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^6.0.1",
|
||||
@@ -19,6 +20,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@tanstack/react-query": "^5.96.1",
|
||||
"js-yaml": "^4.1.1",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"react-router-dom": "^7.13.2"
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useState, useRef, useCallback } from 'react';
|
||||
import { useState, useRef, useCallback, type ReactNode } from 'react';
|
||||
import { useTheme } from '../lib/theme';
|
||||
import { useI18n, type TranslationKey } from '../lib/i18n';
|
||||
import { useClickOutside } from '../hooks/useClickOutside';
|
||||
|
||||
const themes: Array<{ key: 'light' | 'dark' | 'system'; icon: JSX.Element }> = [
|
||||
const themes: Array<{ key: 'light' | 'dark' | 'system'; icon: ReactNode }> = [
|
||||
{
|
||||
key: 'light',
|
||||
icon: (
|
||||
|
||||
32
packages/web/src/lib/fetch-spec.ts
Normal file
32
packages/web/src/lib/fetch-spec.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import yaml from 'js-yaml';
|
||||
import { apiFetch } from './api';
|
||||
|
||||
function parseSpecText(text: string): object {
|
||||
try {
|
||||
return JSON.parse(text);
|
||||
} catch {
|
||||
return yaml.load(text) as object;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch an OpenAPI spec from a URL and parse it.
|
||||
* 1. Try direct fetch from browser (works for localhost/intranet)
|
||||
* 2. If CORS blocks it, fall back to server-side proxy
|
||||
* Returns a parsed spec object (JSON or YAML).
|
||||
*/
|
||||
export async function fetchSpecFromUrl(url: string): Promise<object> {
|
||||
// Try direct fetch first (handles localhost, intranet, CORS-friendly APIs)
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
headers: { Accept: 'application/json, application/yaml, text/yaml, */*' },
|
||||
});
|
||||
if (res.ok) return parseSpecText(await res.text());
|
||||
} catch {
|
||||
// CORS or network error — fall through to server proxy
|
||||
}
|
||||
|
||||
// Fall back to server-side proxy for CORS-restricted URLs
|
||||
const data = await apiFetch<{ content: string }>(`/fetch-spec?url=${encodeURIComponent(url)}`);
|
||||
return parseSpecText(data.content);
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { useState, useRef } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { apiFetch } from '../lib/api';
|
||||
import { fetchSpecFromUrl } from '../lib/fetch-spec';
|
||||
import { useI18n } from '../lib/i18n';
|
||||
import Modal from '../components/Modal';
|
||||
|
||||
@@ -44,7 +45,8 @@ export default function ImportDialog({ onClose }: { onClose: () => void }) {
|
||||
try {
|
||||
let body: Record<string, unknown>;
|
||||
if (mode === 'url') {
|
||||
body = { specUrl: url };
|
||||
const spec = await fetchSpecFromUrl(url);
|
||||
body = { spec };
|
||||
} else {
|
||||
try { body = { spec: JSON.parse(fileContent) }; } catch { body = { spec: fileContent }; }
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState, useRef } from 'react';
|
||||
import { apiFetch } from '../lib/api';
|
||||
import { fetchSpecFromUrl } from '../lib/fetch-spec';
|
||||
import { useI18n } from '../lib/i18n';
|
||||
import Modal from '../components/Modal';
|
||||
|
||||
@@ -49,7 +50,8 @@ export default function ReimportDialog({ projectId, currentStats, onClose, onSuc
|
||||
try {
|
||||
let body: Record<string, unknown>;
|
||||
if (mode === 'url') {
|
||||
body = { specUrl: url };
|
||||
const spec = await fetchSpecFromUrl(url);
|
||||
body = { spec };
|
||||
} else {
|
||||
try { body = { spec: JSON.parse(fileContent) }; } catch { body = { spec: fileContent }; }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user