2026-07-18 08:35:36 +08:00
|
|
|
import type { AxiosError, AxiosResponse } from 'axios'
|
2026-07-18 08:59:40 +08:00
|
|
|
import { isAxiosError } from 'axios'
|
2026-07-18 08:35:36 +08:00
|
|
|
import type { z } from 'zod'
|
2026-07-18 08:59:40 +08:00
|
|
|
import type { RequestError } from './errors'
|
2026-07-18 08:35:36 +08:00
|
|
|
|
|
|
|
|
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'
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-18 08:59:40 +08:00
|
|
|
export type SafeApiErrorReport = {
|
|
|
|
|
diagnostic?: SafeApiDiagnostic
|
|
|
|
|
requestType: RequestError['type']
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-18 08:35:36 +08:00
|
|
|
/**
|
|
|
|
|
* 网络诊断只能描述请求形态,不得复制请求/响应 body、headers 或完整 URL。
|
|
|
|
|
* 这些字段可能包含手机号、userId、sessionId、签名和其它业务数据。
|
|
|
|
|
*/
|
2026-07-26 23:47:30 +08:00
|
|
|
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]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-18 08:35:36 +08:00
|
|
|
function requestShape(response: AxiosResponse<unknown>) {
|
|
|
|
|
return {
|
|
|
|
|
httpStatus: response.status,
|
|
|
|
|
method: response.config.method?.toUpperCase(),
|
2026-07-26 23:47:30 +08:00
|
|
|
path: safeRequestPath(response.config.url),
|
2026-07-18 08:35:36 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function createResponseDiagnostic(response: AxiosResponse<unknown>): SafeApiDiagnostic {
|
|
|
|
|
return { type: 'Response', ...requestShape(response) }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function createValidationDiagnostic(
|
|
|
|
|
response: AxiosResponse<unknown>,
|
|
|
|
|
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(),
|
2026-07-26 23:47:30 +08:00
|
|
|
path: safeRequestPath(error.config?.url),
|
2026-07-18 08:35:36 +08:00
|
|
|
}
|
|
|
|
|
}
|
2026-07-18 08:59:40 +08:00
|
|
|
|
2026-07-21 01:10:41 +08:00
|
|
|
/**
|
|
|
|
|
* 非 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
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-18 08:59:40 +08:00
|
|
|
type ValidationCause = {
|
|
|
|
|
issues: z.ZodIssue[]
|
|
|
|
|
response: AxiosResponse<unknown>
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function isValidationCause(value: unknown): value is ValidationCause {
|
|
|
|
|
if (!value || typeof value !== 'object') return false
|
|
|
|
|
const candidate = value as Partial<ValidationCause>
|
|
|
|
|
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 }
|
|
|
|
|
}
|