fix(api): harden response contracts
This commit is contained in:
@@ -7,6 +7,7 @@ type RequestOptions = {
|
||||
method?: string
|
||||
body?: unknown
|
||||
token?: string
|
||||
responseType?: 'json' | 'void'
|
||||
}
|
||||
|
||||
type ErrorEnvelope = {
|
||||
@@ -54,7 +55,7 @@ export async function apiRequest<T>(path: string, options: RequestOptions = {}):
|
||||
response = await fetch(`${configuredBaseUrl}${path}`, {
|
||||
method: options.method ?? 'GET',
|
||||
headers: {
|
||||
...(isFormData ? {} : { 'Content-Type': 'application/json' }),
|
||||
...(options.body !== undefined && !isFormData ? { 'Content-Type': 'application/json' } : {}),
|
||||
...(options.token ? { Authorization: `Bearer ${options.token}` } : {}),
|
||||
},
|
||||
body: requestBody as BodyInit | undefined,
|
||||
@@ -75,15 +76,28 @@ export async function apiRequest<T>(path: string, options: RequestOptions = {}):
|
||||
throw new ApiError(response.status, fallbackErrorCode(response.status), fallbackErrorMessage(response.status))
|
||||
}
|
||||
|
||||
if (response.status === 204) {
|
||||
if (response.status === 204 || options.responseType === 'void') {
|
||||
return undefined as T
|
||||
}
|
||||
|
||||
const text = await response.text()
|
||||
if (text.trim() === '') {
|
||||
return undefined as T
|
||||
let text: string
|
||||
try {
|
||||
text = await response.text()
|
||||
} catch {
|
||||
throw invalidResponse(response.status)
|
||||
}
|
||||
return JSON.parse(text) as T
|
||||
if (text.trim() === '') {
|
||||
throw invalidResponse(response.status)
|
||||
}
|
||||
try {
|
||||
return JSON.parse(text) as T
|
||||
} catch {
|
||||
throw invalidResponse(response.status)
|
||||
}
|
||||
}
|
||||
|
||||
function invalidResponse(status: number) {
|
||||
return new ApiError(status, 'invalid_response', '服务器返回了无法识别的数据')
|
||||
}
|
||||
|
||||
function isErrorEnvelope(value: unknown): value is ErrorEnvelope {
|
||||
@@ -119,7 +133,7 @@ function fallbackErrorMessage(status: number) {
|
||||
return '请求失败,请稍后重试'
|
||||
}
|
||||
|
||||
function normalizeBaseUrl(value: string) {
|
||||
export function normalizeBaseUrl(value: string) {
|
||||
const trimmed = value.trim()
|
||||
return trimmed.endsWith('/') ? trimmed.slice(0, -1) : trimmed
|
||||
return trimmed.replace(/\/+$/, '')
|
||||
}
|
||||
|
||||
@@ -47,5 +47,6 @@ export async function confirmInboxItem(session: ApiSession, inboxId: string, sug
|
||||
method: 'POST',
|
||||
token: session.token,
|
||||
body: { suggestions },
|
||||
responseType: 'void',
|
||||
})
|
||||
}
|
||||
|
||||
@@ -26,8 +26,8 @@ export type WorkspaceChannelDTO = {
|
||||
type: string
|
||||
title: string
|
||||
icon: string
|
||||
count: number | undefined
|
||||
url: string | undefined
|
||||
count: number
|
||||
url: string
|
||||
sortOrder: number
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { apiRequest, type ApiSession } from './client'
|
||||
|
||||
export type SearchResultDTO = {
|
||||
type: 'project' | 'task' | 'note' | 'source' | 'inbox' | 'ai_session'
|
||||
type: 'project' | 'task' | 'note'
|
||||
id: string
|
||||
projectId: string
|
||||
title: string
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
IconSafe,
|
||||
IconUserGroup,
|
||||
} from '@arco-design/web-react/icon'
|
||||
import { normalizeBaseUrl } from '../api/client'
|
||||
|
||||
const { Title, Text, Paragraph } = Typography
|
||||
|
||||
@@ -129,11 +130,6 @@ async function checkServerStatus(
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeBaseUrl(value: string) {
|
||||
const trimmed = value.trim()
|
||||
return trimmed.endsWith('/') ? trimmed.slice(0, -1) : trimmed
|
||||
}
|
||||
|
||||
function ValuePoint({ icon, title, desc }: { icon: ReactElement; title: string; desc: string }) {
|
||||
return (
|
||||
<div className="value-point">
|
||||
|
||||
Reference in New Issue
Block a user