diff --git a/frontend/src/composables/__tests__/useAuthFetch.spec.js b/frontend/src/composables/__tests__/useAuthFetch.spec.js index cb5401d..370af8e 100644 --- a/frontend/src/composables/__tests__/useAuthFetch.spec.js +++ b/frontend/src/composables/__tests__/useAuthFetch.spec.js @@ -89,13 +89,37 @@ describe('useAuthFetch', () => { ) }) - it('对象 detail: stringify 而非 [object Object]', async () => { + it('对象 detail: hint/error 当作 message · 原对象挂到 err.detail', async () => { + fetchMock.mockResolvedValueOnce( + new Response( + JSON.stringify({ detail: { error: 'fetch_failed', hint: '抓取失败,请改用粘贴正文模式' } }), + { status: 400 } + ) + ) + + const { authFetchJson } = useAuthFetch() + try { + await authFetchJson('/library/articles', { url: 'https://x.example' }) + throw new Error('should have thrown') + } catch (err) { + expect(err.message).toBe('抓取失败,请改用粘贴正文模式') + expect(err.detail).toEqual({ error: 'fetch_failed', hint: '抓取失败,请改用粘贴正文模式' }) + } + }) + + it('对象 detail 无 hint/error: 退回 JSON 串当 message · 仍挂 err.detail', async () => { fetchMock.mockResolvedValueOnce( new Response(JSON.stringify({ detail: { code: 'X', reason: 'Y' } }), { status: 500 }) ) const { authFetchJson } = useAuthFetch() - await expect(authFetchJson('/api/weird', {})).rejects.toThrow(/"code":"X"/) + try { + await authFetchJson('/api/weird', {}) + throw new Error('should have thrown') + } catch (err) { + expect(err.message).toMatch(/"code":"X"/) + expect(err.detail).toEqual({ code: 'X', reason: 'Y' }) + } }) it('network 错误路径: 透传 fetch 抛出的错误', async () => { diff --git a/frontend/src/composables/useAuthFetch.js b/frontend/src/composables/useAuthFetch.js index e75342d..32b658c 100644 --- a/frontend/src/composables/useAuthFetch.js +++ b/frontend/src/composables/useAuthFetch.js @@ -90,6 +90,7 @@ export function useAuthFetch() { }) if (!resp.ok) { let detail = `HTTP ${resp.status}` + let structured = null try { const body = await resp.json() const raw = body?.detail ?? body?.error @@ -100,12 +101,17 @@ export function useAuthFetch() { const parts = raw.map((d) => (d && typeof d === 'object' && d.msg) || JSON.stringify(d)) if (parts.length) detail = parts.join('; ') } else if (raw != null && typeof raw === 'object') { - detail = JSON.stringify(raw) + // 结构化 detail(如 {error, hint}):message 取 hint/error 作可读文案, + // 同时把原对象挂到 err.detail,让调用方能按业务字段降级处理 + structured = raw + detail = raw.hint || raw.error || JSON.stringify(raw) } } catch { /* ignore parse error */ } - throw new Error(detail) + const err = new Error(detail) + if (structured) err.detail = structured + throw err } return resp.json() }