fix: 修复重新导入的模块数缺失、名称被覆盖与 URL 抓取报错
- 项目详情接口 _count 漏 select modules,重新导入弹框因此显示「undefined 个模块」 - 重新导入不再用文档中的 name/description 覆盖用户自行设置的项目名称与描述 - URL 抓取失败原因此前被完全吞掉,只剩无信息量的 502:前端改为预判混合内容、 跳过必然超时的内网服务端代理、直连加超时,并抛出带错误码的 SpecFetchError - 服务端代理加 15s 超时并始终返回 JSON,不再挂到 nginx 超时返回 HTML 错误页 - 新增 SpecFetchErrorNotice 组件,跨域类失败时可展开具体的 CORS 配置引导 - nginx /api/ 补 client_max_body_size 12m,避免大文档在 nginx 层被 413 挡掉 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,26 +4,47 @@ import { requireAuth } from '../middleware/auth.js';
|
||||
const router: RouterType = Router();
|
||||
router.use(requireAuth);
|
||||
|
||||
// CORS proxy: frontend calls this when direct fetch is blocked by CORS
|
||||
// 服务端兜底抓取:只有当浏览器直连被 CORS / 混合内容拦截时前端才会调用这里。
|
||||
// 目标地址若是内网 / VPN 资源,服务器通常访问不到,必须快速失败并返回 JSON,
|
||||
// 否则请求会挂到 nginx 超时,前端只能看到一个没有信息量的 502/504 HTML 页面。
|
||||
const FETCH_TIMEOUT_MS = 15_000;
|
||||
|
||||
router.get('/', async (req, res) => {
|
||||
const specUrl = req.query.url as string;
|
||||
if (!specUrl || !specUrl.startsWith('http')) {
|
||||
res.status(400).json({ success: false, error: { code: 'VALIDATION', message: 'Provide a valid URL' } });
|
||||
let target: URL;
|
||||
try {
|
||||
target = new URL(specUrl);
|
||||
if (target.protocol !== 'http:' && target.protocol !== 'https:') throw new Error('bad protocol');
|
||||
} catch {
|
||||
res.status(400).json({ success: false, error: { code: 'VALIDATION', message: 'Provide a valid http(s) URL' } });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(specUrl, {
|
||||
const response = await fetch(target.toString(), {
|
||||
headers: { Accept: 'application/json, application/yaml, text/yaml, text/plain, */*' },
|
||||
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
||||
});
|
||||
if (!response.ok) {
|
||||
res.status(502).json({ success: false, error: { code: 'FETCH_FAILED', message: `Remote server returned ${response.status}` } });
|
||||
res.status(502).json({
|
||||
success: false,
|
||||
error: { code: 'FETCH_FAILED', message: `Remote server returned ${response.status}` },
|
||||
});
|
||||
return;
|
||||
}
|
||||
const text = await response.text();
|
||||
res.json({ success: true, data: { content: text, contentType: response.headers.get('content-type') || '' } });
|
||||
} catch (err) {
|
||||
res.status(502).json({ success: false, error: { code: 'FETCH_FAILED', message: err instanceof Error ? err.message : 'Failed to fetch URL' } });
|
||||
const timedOut = err instanceof Error && (err.name === 'TimeoutError' || err.name === 'AbortError');
|
||||
res.status(502).json({
|
||||
success: false,
|
||||
error: {
|
||||
code: timedOut ? 'FETCH_TIMEOUT' : 'FETCH_FAILED',
|
||||
message: timedOut
|
||||
? `Timed out after ${FETCH_TIMEOUT_MS / 1000}s — the server cannot reach ${target.host}`
|
||||
: `${err instanceof Error ? err.message : 'Failed to fetch URL'} (${target.host})`,
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -29,10 +29,11 @@ router.post('/:id/reimport', async (req, res) => {
|
||||
await tx.endpoint.deleteMany({ where: { projectId: project.id } });
|
||||
await tx.module.deleteMany({ where: { projectId: project.id } });
|
||||
|
||||
// 只更新文档本体,项目名称与描述由用户自行维护,重新导入不覆盖
|
||||
await tx.project.update({
|
||||
where: { id: project.id },
|
||||
data: {
|
||||
name: parsed.name, description: parsed.description, baseUrl: parsed.baseUrl,
|
||||
baseUrl: parsed.baseUrl,
|
||||
openApiSpec: parsed.spec as any, openApiVersion: parsed.openApiVersion,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -88,7 +88,7 @@ router.get('/:id', async (req, res) => {
|
||||
include: { _count: { select: { endpoints: true } } },
|
||||
orderBy: { sortOrder: 'asc' },
|
||||
},
|
||||
_count: { select: { endpoints: true } },
|
||||
_count: { select: { endpoints: true, modules: true } },
|
||||
},
|
||||
});
|
||||
if (!project) {
|
||||
|
||||
Reference in New Issue
Block a user