* Execute an HTTP request and return the parsed JSON response. * * - Injects `Authorization: Bearer ` when `smartnode_token` is set * in localStorage and no `Authorization` header was provided. * - Unwraps backend envelope `{ code: 0, data: T }` transparently. * - Throws `Api
(path: string, options: RequestOptions = {})
| 101 | * - Throws `ApiError` on non-2xx responses or timeout. |
| 102 | */ |
| 103 | async request<T>(path: string, options: RequestOptions = {}): Promise<T> { |
| 104 | const { timeoutMs = this._defaultTimeoutMs, ...fetchOptions } = options; |
| 105 | |
| 106 | const headers: Record<string, string> = { |
| 107 | 'Content-Type': 'application/json', |
| 108 | ...((fetchOptions.headers as Record<string, string>) || {}), |
| 109 | }; |
| 110 | |
| 111 | const token = localStorage.getItem('smartnode_token'); |
| 112 | if (token && !headers['Authorization']) { |
| 113 | headers['Authorization'] = `Bearer ${token}`; |
| 114 | } |
| 115 | |
| 116 | let abortController: AbortController | undefined; |
| 117 | let timeoutId: ReturnType<typeof setTimeout> | undefined; |
| 118 | |
| 119 | if (timeoutMs > 0) { |
| 120 | abortController = new AbortController(); |
| 121 | timeoutId = setTimeout(() => abortController!.abort(), timeoutMs); |
| 122 | } |
| 123 | |
| 124 | let response: Response; |
| 125 | try { |
| 126 | response = await fetch(this.url(path), { |
| 127 | ...fetchOptions, |
| 128 | headers, |
| 129 | signal: abortController?.signal, |
| 130 | }); |
| 131 | } catch (err) { |
| 132 | if (abortController?.signal.aborted) { |
| 133 | throw new ApiError(`请求超时(${timeoutMs} ms)`, undefined, true); |
| 134 | } |
| 135 | throw new ApiError((err as Error).message || '网络请求失败'); |
| 136 | } finally { |
| 137 | if (timeoutId !== undefined) clearTimeout(timeoutId); |
| 138 | } |
| 139 | |
| 140 | const contentType = response.headers.get('content-type') || ''; |
| 141 | let payload: unknown; |
| 142 | if (contentType.includes('application/json')) { |
| 143 | payload = await response.json(); |
| 144 | } else { |
| 145 | payload = await response.text(); |
| 146 | } |
| 147 | |
| 148 | if (!response.ok) { |
| 149 | const message = |
| 150 | typeof payload === 'string' |
| 151 | ? payload |
| 152 | : ( |
| 153 | (payload as Record<string, unknown>).message || |
| 154 | (payload as Record<string, unknown>).reject_reason || |
| 155 | (payload as Record<string, unknown>).error || |
| 156 | '请求失败' |
| 157 | ) as string; |
| 158 | throw new ApiError(message, response.status); |
| 159 | } |
| 160 |
no test coverage detected