Merge pull request 'fix(admin): /health 被误加 /api/v1 前缀,健康状态永远「系统异常」' (#17) from fix-healthcheck-url into main
This commit was merged in pull request #17.
This commit is contained in:
@@ -21,8 +21,10 @@ globalThis.window = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 捕获每次 fetch 的入参供断言
|
// 捕获每次 fetch 的入参供断言
|
||||||
|
let lastUrl = ''
|
||||||
let lastInit: RequestInit | undefined
|
let lastInit: RequestInit | undefined
|
||||||
globalThis.fetch = async (_url: string, init?: RequestInit) => {
|
globalThis.fetch = async (url: string | URL | Request, init?: RequestInit) => {
|
||||||
|
lastUrl = String(url)
|
||||||
lastInit = init
|
lastInit = init
|
||||||
return new Response(JSON.stringify({ ok: true }), { status: 200, headers: { 'content-type': 'application/json' } })
|
return new Response(JSON.stringify({ ok: true }), { status: 200, headers: { 'content-type': 'application/json' } })
|
||||||
}
|
}
|
||||||
@@ -63,3 +65,19 @@ test('DELETE: method=DELETE, 无 body, 无 Content-Type', async () => {
|
|||||||
assert.equal(lastInit?.body, undefined)
|
assert.equal(lastInit?.body, undefined)
|
||||||
assert.equal((lastInit?.headers as Record<string, string>)?.['Content-Type'], undefined)
|
assert.equal((lastInit?.headers as Record<string, string>)?.['Content-Type'], undefined)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// ── 健康检查豁免: /health* 挂在根路径(后端 app.py 不在 /api/v1 下),
|
||||||
|
// 加前缀会 404 → 管理后台右上角永远「系统异常」 ──
|
||||||
|
test('build_url: /health 与 /health/ready 不加 /api/v1 前缀', async () => {
|
||||||
|
await http.get('/health')
|
||||||
|
assert.equal(lastUrl, '/health')
|
||||||
|
await http.get('/health/ready')
|
||||||
|
assert.equal(lastUrl, '/health/ready')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('build_url: 常规 API 路径仍统一加 /api/v1', async () => {
|
||||||
|
await http.get('/admin/stats')
|
||||||
|
assert.equal(lastUrl, '/api/v1/admin/stats')
|
||||||
|
await http.get('/matches')
|
||||||
|
assert.equal(lastUrl, '/api/v1/matches')
|
||||||
|
})
|
||||||
|
|||||||
@@ -17,22 +17,30 @@ export const UNAUTHORIZED_EVENT = 'profeto:unauthorized'
|
|||||||
const API_BASE = '/api/v1'
|
const API_BASE = '/api/v1'
|
||||||
const DEFAULT_TIMEOUT = 30_000
|
const DEFAULT_TIMEOUT = 30_000
|
||||||
|
|
||||||
|
/** 健康检查端点挂在根路径(app.py 不在 /api/v1 下,与 compose healthcheck 一致);
|
||||||
|
* 加前缀会 404,导致管理后台健康状态永远显示「系统异常」 */
|
||||||
|
const ROOT_ONLY_PREFIXES = ['/health']
|
||||||
|
|
||||||
/** 所有 API 路径统一走 /api/v1,避免浏览器直接请求 /matches 被 nginx 当 SPA 回退 */
|
/** 所有 API 路径统一走 /api/v1,避免浏览器直接请求 /matches 被 nginx 当 SPA 回退 */
|
||||||
function build_url(path: string): string {
|
function build_url(path: string): string {
|
||||||
if (path.startsWith('http')) return path
|
if (path.startsWith('http')) return path
|
||||||
|
if (ROOT_ONLY_PREFIXES.some(p => path === p || path.startsWith(`${p}/`))) return path
|
||||||
if (path.startsWith(API_BASE)) return path
|
if (path.startsWith(API_BASE)) return path
|
||||||
if (path.startsWith('/')) return `${API_BASE}${path}`
|
if (path.startsWith('/')) return `${API_BASE}${path}`
|
||||||
return `${API_BASE}/${path}`
|
return `${API_BASE}/${path}`
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ApiError extends Error {
|
export class ApiError extends Error {
|
||||||
constructor(
|
status: number
|
||||||
message: string,
|
data?: unknown
|
||||||
public status: number,
|
|
||||||
public data?: unknown,
|
constructor(message: string, status: number, data?: unknown) {
|
||||||
) {
|
|
||||||
super(message)
|
super(message)
|
||||||
this.name = 'ApiError'
|
this.name = 'ApiError'
|
||||||
|
// 显式赋值而非构造函数参数属性(public x):strip-types 不支持后者,
|
||||||
|
// 会让 node --experimental-strip-types 跑 lib/http.test.ts 直接失败
|
||||||
|
this.status = status
|
||||||
|
this.data = data
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user