import type { AxiosError, AxiosResponse } from 'axios' import { isAxiosError } from 'axios' import type { z } from 'zod' import type { RequestError } from './errors' type SafeIssue = { code: string message: string path: string } export type SafeApiDiagnostic = { code?: string httpStatus?: number issues?: SafeIssue[] method?: string path?: string type: 'AxiosError' | 'Response' | 'ValidationError' } export type SafeApiErrorReport = { diagnostic?: SafeApiDiagnostic requestType: RequestError['type'] } /** * 网络诊断只能描述请求形态,不得复制请求/响应 body、headers 或完整 URL。 * 这些字段可能包含手机号、userId、sessionId、签名和其它业务数据。 */ export function safeRequestPath(input?: string): string | undefined { if (!input) return undefined try { return new URL(input, 'https://xuqm.invalid').pathname } catch { return input.split(/[?#]/, 1)[0] } } function requestShape(response: AxiosResponse) { return { httpStatus: response.status, method: response.config.method?.toUpperCase(), path: safeRequestPath(response.config.url), } } export function createResponseDiagnostic(response: AxiosResponse): SafeApiDiagnostic { return { type: 'Response', ...requestShape(response) } } export function createValidationDiagnostic( response: AxiosResponse, issues: z.ZodIssue[], ): SafeApiDiagnostic { return { type: 'ValidationError', ...requestShape(response), issues: issues.map(issue => ({ code: issue.code, message: issue.message, path: issue.path.join('.'), })), } } export function createAxiosDiagnostic(error: AxiosError): SafeApiDiagnostic { return { type: 'AxiosError', code: error.code, httpStatus: error.response?.status, method: error.config?.method?.toUpperCase(), path: safeRequestPath(error.config?.url), } } /** * 非 axios 异常只允许记录构造名与截断消息。 * 不接触 cause、stack、config 或任何可能持有业务数据的对象。 */ export function createUnexpectedDiagnostic(error: unknown): string { if (error instanceof Error) { return `${error.name}: ${error.message.slice(0, 200)}` } return typeof error } type ValidationCause = { issues: z.ZodIssue[] response: AxiosResponse } function isValidationCause(value: unknown): value is ValidationCause { if (!value || typeof value !== 'object') return false const candidate = value as Partial return Array.isArray(candidate.issues) && Boolean(candidate.response?.config) } /** * 全局错误采集只接收不可逆的安全摘要,绝不暴露 RequestError.cause。 * cause 可能持有 Axios config、headers、请求体和完整响应,只能留在调用现场。 */ export function createSafeApiErrorReport(error: RequestError): SafeApiErrorReport { const cause = error.cause const diagnostic = isValidationCause(cause) ? createValidationDiagnostic(cause.response, cause.issues) : isAxiosError(cause) ? createAxiosDiagnostic(cause) : undefined return { diagnostic, requestType: error.type } }