@lytjs/common-http 6.5.0 → 6.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.ts CHANGED
@@ -1,334 +1,481 @@
1
- /**
2
- * @lytjs/common-http
3
- * Lightweight HTTP client based on native fetch API
4
- */
5
-
6
- // --- Types ---
7
-
8
- export interface HttpClientOptions {
9
- baseURL?: string;
10
- headers?: Record<string, string>;
11
- timeout?: number;
12
- withCredentials?: boolean;
13
- }
14
-
15
- export interface RequestOptions {
16
- headers?: Record<string, string>;
17
- params?: Record<string, string>;
18
- timeout?: number;
19
- signal?: AbortSignal;
20
- responseType?: 'json' | 'text' | 'blob' | 'arraybuffer';
21
- }
22
-
23
- export interface HttpResponse<T> {
24
- data: T;
25
- status: number;
26
- statusText: string;
27
- headers: Record<string, string>;
28
- ok: boolean;
29
- }
30
-
31
- export interface InternalRequestConfig {
32
- method: string;
33
- url: string;
34
- headers: Record<string, string>;
35
- body?: BodyInit | null;
36
- signal?: AbortSignal;
37
- responseType?: 'json' | 'text' | 'blob' | 'arraybuffer';
38
- withCredentials?: boolean;
39
- timeout?: number;
40
- }
41
-
42
- export interface Interceptor {
43
- request?(config: InternalRequestConfig): InternalRequestConfig | Promise<InternalRequestConfig>;
44
- response?(response: HttpResponse<unknown>): HttpResponse<unknown> | Promise<HttpResponse<unknown>>;
45
- error?(error: HttpError): unknown;
46
- }
47
-
48
- // --- HttpError ---
49
-
50
- export class HttpError extends Error {
51
- readonly response?: HttpResponse<unknown>;
52
- readonly status?: number;
53
-
54
- constructor(message: string, response?: HttpResponse<unknown>) {
55
- super(message);
56
- this.name = 'HttpError';
57
- this.response = response;
58
- this.status = response?.status;
59
- }
60
- }
61
-
62
- // --- CancellationToken ---
63
-
64
- export class CancellationToken {
65
- private readonly _controller: AbortController;
66
- private readonly _linkedTokens: CancellationToken[];
67
-
68
- constructor() {
69
- this._controller = new AbortController();
70
- this._linkedTokens = [];
71
- }
72
-
73
- get signal(): AbortSignal {
74
- return this._controller.signal;
75
- }
76
-
77
- cancel(): void {
78
- this._controller.abort();
79
- for (const token of this._linkedTokens) {
80
- token.cancel();
81
- }
82
- }
83
-
84
- get isCancelled(): boolean {
85
- return this._controller.signal.aborted;
86
- }
87
-
88
- static createLinkedToken(...tokens: CancellationToken[]): CancellationToken {
89
- const linked = new CancellationToken();
90
- for (const token of tokens) {
91
- token._linkedTokens.push(linked);
92
- if (token.isCancelled) {
93
- linked.cancel();
94
- return linked;
95
- }
96
- }
97
- return linked;
98
- }
99
- }
100
-
101
- // --- HttpClient ---
102
-
103
- export class HttpClient {
104
- private readonly _options: HttpClientOptions;
105
- private readonly _interceptors: Interceptor[];
106
-
107
- constructor(options?: HttpClientOptions) {
108
- this._options = { ...options };
109
- this._interceptors = [];
110
- }
111
-
112
- get<T = unknown>(url: string, options?: RequestOptions): Promise<HttpResponse<T>> {
113
- return this.request<T>('GET', url, options);
114
- }
115
-
116
- post<T = unknown>(url: string, data?: unknown, options?: RequestOptions): Promise<HttpResponse<T>> {
117
- return this.request<T>('POST', url, { ...options, body: data } as RequestOptions & { body?: unknown });
118
- }
119
-
120
- put<T = unknown>(url: string, data?: unknown, options?: RequestOptions): Promise<HttpResponse<T>> {
121
- return this.request<T>('PUT', url, { ...options, body: data } as RequestOptions & { body?: unknown });
122
- }
123
-
124
- delete<T = unknown>(url: string, options?: RequestOptions): Promise<HttpResponse<T>> {
125
- return this.request<T>('DELETE', url, options);
126
- }
127
-
128
- patch<T = unknown>(url: string, data?: unknown, options?: RequestOptions): Promise<HttpResponse<T>> {
129
- return this.request<T>('PATCH', url, { ...options, body: data } as RequestOptions & { body?: unknown });
130
- }
131
-
132
- async request<T = unknown>(
133
- method: string,
134
- url: string,
135
- options?: RequestOptions & { body?: unknown },
136
- ): Promise<HttpResponse<T>> {
137
- let config = this._buildConfig(method, url, options);
138
-
139
- // Apply request interceptors
140
- for (const interceptor of this._interceptors) {
141
- if (interceptor.request) {
142
- config = await interceptor.request(config);
143
- }
144
- }
145
-
146
- try {
147
- const response = await this._fetch(config);
148
- let httpResponse = await this._parseResponse<T>(response, config.responseType);
149
-
150
- // Apply response interceptors
151
- for (const interceptor of this._interceptors) {
152
- if (interceptor.response) {
153
- httpResponse = (await interceptor.response(httpResponse as HttpResponse<unknown>)) as HttpResponse<T>;
154
- }
155
- }
156
-
157
- return httpResponse;
158
- } catch (err) {
159
- // Apply error interceptors
160
- for (const interceptor of this._interceptors) {
161
- if (interceptor.error) {
162
- const result = interceptor.error(err instanceof HttpError ? err : new HttpError(String(err)));
163
- if (result !== undefined) return result as HttpResponse<T>;
164
- }
165
- }
166
- throw err;
167
- }
168
- }
169
-
170
- use(interceptor: Interceptor): () => void {
171
- this._interceptors.push(interceptor);
172
- return () => this.eject(interceptor);
173
- }
174
-
175
- eject(interceptor: Interceptor): void {
176
- const index = this._interceptors.indexOf(interceptor);
177
- if (index !== -1) {
178
- this._interceptors.splice(index, 1);
179
- }
180
- }
181
-
182
- private _buildConfig(
183
- method: string,
184
- url: string,
185
- options?: RequestOptions & { body?: unknown },
186
- ): InternalRequestConfig {
187
- const { baseURL = '', headers: defaultHeaders = {} } = this._options;
188
- const resolvedURL = this._resolveURL(baseURL, url, options?.params);
189
-
190
- const headers: Record<string, string> = { ...defaultHeaders, ...options?.headers };
191
-
192
- let body: BodyInit | null = null;
193
- if (options && 'body' in options && options.body !== undefined) {
194
- if (typeof options.body === 'object' && options.body !== null) {
195
- body = JSON.stringify(options.body);
196
- if (!headers['Content-Type']) {
197
- headers['Content-Type'] = 'application/json';
198
- }
199
- } else {
200
- body = String(options.body);
201
- }
202
- }
203
-
204
- const timeout = options?.timeout ?? this._options.timeout;
205
-
206
- return {
207
- method: method.toUpperCase(),
208
- url: resolvedURL,
209
- headers,
210
- body,
211
- signal: options?.signal,
212
- responseType: options?.responseType,
213
- withCredentials: this._options.withCredentials,
214
- timeout,
215
- };
216
- }
217
-
218
- private _resolveURL(base: string, url: string, params?: Record<string, string>): string {
219
- let resolved = base ? base.replace(/\/+$/, '') + '/' + url.replace(/^\/+/, '') : url;
220
- if (params) {
221
- const searchParams = new URLSearchParams(params);
222
- const qs = searchParams.toString();
223
- if (qs) {
224
- resolved += (resolved.includes('?') ? '&' : '?') + qs;
225
- }
226
- }
227
- return resolved;
228
- }
229
-
230
- private async _fetch(config: InternalRequestConfig): Promise<Response> {
231
- const { method, url, headers, body, signal, withCredentials, timeout } = config;
232
-
233
- // Handle timeout via AbortController
234
- let timeoutId: ReturnType<typeof setTimeout> | undefined;
235
- let timeoutController: AbortController | undefined;
236
-
237
- if (timeout && timeout > 0) {
238
- timeoutController = new AbortController();
239
- timeoutId = setTimeout(() => timeoutController!.abort(), timeout);
240
-
241
- // Link with existing signal
242
- if (signal) {
243
- if (signal.aborted) {
244
- clearTimeout(timeoutId);
245
- throw new HttpError('Request was aborted', undefined);
246
- }
247
- signal.addEventListener('abort', () => {
248
- clearTimeout(timeoutId);
249
- timeoutController!.abort();
250
- }, { once: true });
251
- }
252
- }
253
-
254
- const fetchOptions: RequestInit = {
255
- method,
256
- headers,
257
- body,
258
- credentials: withCredentials ? 'include' : undefined,
259
- signal: timeoutController ? timeoutController.signal : signal,
260
- };
261
-
262
- try {
263
- const response = await fetch(url, fetchOptions);
264
-
265
- if (!response.ok) {
266
- const errorResponse: HttpResponse<unknown> = {
267
- data: null,
268
- status: response.status,
269
- statusText: response.statusText,
270
- headers: this._parseHeaders(response.headers),
271
- ok: false,
272
- };
273
- throw new HttpError(
274
- `HTTP ${response.status}: ${response.statusText}`,
275
- errorResponse,
276
- );
277
- }
278
-
279
- return response;
280
- } finally {
281
- if (timeoutId !== undefined) {
282
- clearTimeout(timeoutId);
283
- }
284
- }
285
- }
286
-
287
- private async _parseResponse<T>(
288
- response: Response,
289
- responseType?: 'json' | 'text' | 'blob' | 'arraybuffer',
290
- ): Promise<HttpResponse<T>> {
291
- let data: T;
292
- switch (responseType) {
293
- case 'text':
294
- data = (await response.text()) as unknown as T;
295
- break;
296
- case 'blob':
297
- data = (await response.blob()) as unknown as T;
298
- break;
299
- case 'arraybuffer':
300
- data = (await response.arrayBuffer()) as unknown as T;
301
- break;
302
- case 'json':
303
- default: {
304
- const text = await response.text();
305
- data = text ? JSON.parse(text) : (null as unknown as T);
306
- break;
307
- }
308
- }
309
-
310
- return {
311
- data,
312
- status: response.status,
313
- statusText: response.statusText,
314
- headers: this._parseHeaders(response.headers),
315
- ok: response.ok,
316
- };
317
- }
318
-
319
- private _parseHeaders(headers: Headers): Record<string, string> {
320
- const result: Record<string, string> = {};
321
- headers.forEach((value, key) => {
322
- result[key] = value;
323
- });
324
- return result;
325
- }
326
- }
327
-
328
- // --- Convenience exports ---
329
-
330
- export function createHttpClient(options?: HttpClientOptions): HttpClient {
331
- return new HttpClient(options);
332
- }
333
-
334
- export const http: HttpClient = new HttpClient();
1
+ /**
2
+ * @lytjs/common-http
3
+ * Lightweight HTTP client based on native fetch API
4
+ */
5
+
6
+ // --- Imports ---
7
+ import { stringifyQueryString } from '@lytjs/common-query';
8
+
9
+ // --- Types ---
10
+
11
+ export interface HttpClientOptions {
12
+ baseURL?: string;
13
+ headers?: Record<string, string>;
14
+ timeout?: number;
15
+ withCredentials?: boolean;
16
+ }
17
+
18
+ export interface RequestOptions {
19
+ headers?: Record<string, string>;
20
+ params?: Record<string, string | number | boolean | Array<string | number | boolean>>;
21
+ timeout?: number;
22
+ signal?: AbortSignal;
23
+ responseType?: 'json' | 'text' | 'blob' | 'arraybuffer';
24
+ }
25
+
26
+ export interface HttpResponse<T> {
27
+ data: T;
28
+ status: number;
29
+ statusText: string;
30
+ headers: Record<string, string>;
31
+ ok: boolean;
32
+ }
33
+
34
+ export interface InternalRequestConfig {
35
+ method: string;
36
+ url: string;
37
+ headers: Record<string, string>;
38
+ body?: BodyInit | null;
39
+ signal?: AbortSignal;
40
+ responseType?: 'json' | 'text' | 'blob' | 'arraybuffer';
41
+ withCredentials?: boolean;
42
+ timeout?: number;
43
+ }
44
+
45
+ export interface Interceptor {
46
+ request?(config: InternalRequestConfig): InternalRequestConfig | Promise<InternalRequestConfig>;
47
+ response?(
48
+ response: HttpResponse<unknown>,
49
+ ): HttpResponse<unknown> | Promise<HttpResponse<unknown>>;
50
+ error?(error: HttpError): unknown;
51
+ }
52
+
53
+ // --- HttpError ---
54
+
55
+ export class HttpError extends Error {
56
+ readonly response?: HttpResponse<unknown>;
57
+ readonly status?: number;
58
+
59
+ constructor(message: string, response?: HttpResponse<unknown>) {
60
+ super(message);
61
+ this.name = 'HttpError';
62
+ this.response = response;
63
+ this.status = response?.status;
64
+ }
65
+ }
66
+
67
+ // --- CancellationToken ---
68
+
69
+ export class CancellationToken {
70
+ private readonly _controller: AbortController;
71
+ private readonly _linkedTokens: CancellationToken[];
72
+
73
+ constructor() {
74
+ this._controller = new AbortController();
75
+ this._linkedTokens = [];
76
+ }
77
+
78
+ get signal(): AbortSignal {
79
+ return this._controller.signal;
80
+ }
81
+
82
+ cancel(): void {
83
+ this._controller.abort();
84
+ for (const token of this._linkedTokens) {
85
+ token.cancel();
86
+ }
87
+ }
88
+
89
+ get isCancelled(): boolean {
90
+ return this._controller.signal.aborted;
91
+ }
92
+
93
+ static createLinkedToken(...tokens: CancellationToken[]): CancellationToken {
94
+ const linked = new CancellationToken();
95
+ for (const token of tokens) {
96
+ token._linkedTokens.push(linked);
97
+ if (token.isCancelled) {
98
+ linked.cancel();
99
+ return linked;
100
+ }
101
+ }
102
+ return linked;
103
+ }
104
+ }
105
+
106
+ // --- HttpClient ---
107
+
108
+ export class HttpClient {
109
+ private readonly _options: HttpClientOptions;
110
+ private readonly _interceptors: Interceptor[];
111
+
112
+ constructor(options?: HttpClientOptions) {
113
+ this._options = { ...options };
114
+ this._interceptors = [];
115
+ }
116
+
117
+ get<T = unknown>(url: string, options?: RequestOptions): Promise<HttpResponse<T>> {
118
+ return this.request<T>('GET', url, options);
119
+ }
120
+
121
+ post<T = unknown>(
122
+ url: string,
123
+ data?: unknown,
124
+ options?: RequestOptions,
125
+ ): Promise<HttpResponse<T>> {
126
+ return this.request<T>('POST', url, { ...options, body: data } as RequestOptions & {
127
+ body?: unknown;
128
+ });
129
+ }
130
+
131
+ put<T = unknown>(
132
+ url: string,
133
+ data?: unknown,
134
+ options?: RequestOptions,
135
+ ): Promise<HttpResponse<T>> {
136
+ return this.request<T>('PUT', url, { ...options, body: data } as RequestOptions & {
137
+ body?: unknown;
138
+ });
139
+ }
140
+
141
+ delete<T = unknown>(url: string, options?: RequestOptions): Promise<HttpResponse<T>> {
142
+ return this.request<T>('DELETE', url, options);
143
+ }
144
+
145
+ patch<T = unknown>(
146
+ url: string,
147
+ data?: unknown,
148
+ options?: RequestOptions,
149
+ ): Promise<HttpResponse<T>> {
150
+ return this.request<T>('PATCH', url, { ...options, body: data } as RequestOptions & {
151
+ body?: unknown;
152
+ });
153
+ }
154
+
155
+ async request<T = unknown>(
156
+ method: string,
157
+ url: string,
158
+ options?: RequestOptions & { body?: unknown },
159
+ ): Promise<HttpResponse<T>> {
160
+ let config = this._buildConfig(method, url, options);
161
+
162
+ // Apply request interceptors
163
+ for (const interceptor of this._interceptors) {
164
+ if (interceptor.request) {
165
+ config = await interceptor.request(config);
166
+ }
167
+ }
168
+
169
+ try {
170
+ const response = await this._fetch(config);
171
+ let httpResponse = await this._parseResponse<T>(response, config.responseType);
172
+
173
+ // Apply response interceptors
174
+ for (const interceptor of this._interceptors) {
175
+ if (interceptor.response) {
176
+ httpResponse = (await interceptor.response(
177
+ httpResponse as HttpResponse<unknown>,
178
+ )) as HttpResponse<T>;
179
+ }
180
+ }
181
+
182
+ return httpResponse;
183
+ } catch (err) {
184
+ // Apply error interceptors
185
+ for (const interceptor of this._interceptors) {
186
+ if (interceptor.error) {
187
+ const result = interceptor.error(
188
+ err instanceof HttpError ? err : new HttpError(String(err)),
189
+ );
190
+ if (result !== undefined) return result as HttpResponse<T>;
191
+ }
192
+ }
193
+ throw err;
194
+ }
195
+ }
196
+
197
+ use(interceptor: Interceptor): () => void {
198
+ this._interceptors.push(interceptor);
199
+ return () => this.eject(interceptor);
200
+ }
201
+
202
+ eject(interceptor: Interceptor): void {
203
+ const index = this._interceptors.indexOf(interceptor);
204
+ if (index !== -1) {
205
+ this._interceptors.splice(index, 1);
206
+ }
207
+ }
208
+
209
+ private _buildConfig(
210
+ method: string,
211
+ url: string,
212
+ options?: RequestOptions & { body?: unknown },
213
+ ): InternalRequestConfig {
214
+ const { baseURL = '', headers: defaultHeaders = {} } = this._options;
215
+ const resolvedURL = this._resolveURL(baseURL, url, options?.params);
216
+
217
+ const headers: Record<string, string> = { ...defaultHeaders, ...options?.headers };
218
+
219
+ let body: BodyInit | null = null;
220
+ if (options && 'body' in options && options.body !== undefined) {
221
+ if (typeof options.body === 'object' && options.body !== null) {
222
+ body = JSON.stringify(options.body);
223
+ if (!headers['Content-Type']) {
224
+ headers['Content-Type'] = 'application/json';
225
+ }
226
+ } else {
227
+ body = String(options.body);
228
+ }
229
+ }
230
+
231
+ const timeout = options?.timeout ?? this._options.timeout;
232
+
233
+ return {
234
+ method: method.toUpperCase(),
235
+ url: resolvedURL,
236
+ headers,
237
+ body,
238
+ signal: options?.signal,
239
+ responseType: options?.responseType,
240
+ withCredentials: this._options.withCredentials,
241
+ timeout,
242
+ };
243
+ }
244
+
245
+ private _resolveURL(
246
+ base: string,
247
+ url: string,
248
+ params?: Record<string, string | number | boolean | Array<string | number | boolean>>,
249
+ ): string {
250
+ let resolved = base ? base.replace(/\/+$/, '') + '/' + url.replace(/^\/+/, '') : url;
251
+ if (params) {
252
+ const qs = stringifyQueryString(params);
253
+ if (qs) {
254
+ resolved += (resolved.includes('?') ? '&' : '?') + qs;
255
+ }
256
+ }
257
+ return resolved;
258
+ }
259
+
260
+ private async _fetch(config: InternalRequestConfig): Promise<Response> {
261
+ const { method, url, headers, body, signal, withCredentials, timeout } = config;
262
+
263
+ // Handle timeout via AbortController
264
+ let timeoutId: ReturnType<typeof setTimeout> | undefined;
265
+ let timeoutController: AbortController | undefined;
266
+
267
+ if (timeout && timeout > 0) {
268
+ timeoutController = new AbortController();
269
+ timeoutId = setTimeout(() => timeoutController!.abort(), timeout);
270
+
271
+ // Link with existing signal
272
+ if (signal) {
273
+ if (signal.aborted) {
274
+ clearTimeout(timeoutId);
275
+ throw new HttpError('Request was aborted', undefined);
276
+ }
277
+ signal.addEventListener(
278
+ 'abort',
279
+ () => {
280
+ clearTimeout(timeoutId);
281
+ timeoutController!.abort();
282
+ },
283
+ { once: true },
284
+ );
285
+ }
286
+ }
287
+
288
+ const fetchOptions: RequestInit = {
289
+ method,
290
+ headers,
291
+ body,
292
+ credentials: withCredentials ? 'include' : undefined,
293
+ signal: timeoutController ? timeoutController.signal : signal,
294
+ };
295
+
296
+ try {
297
+ const response = await fetch(url, fetchOptions);
298
+
299
+ if (!response.ok) {
300
+ const errorResponse: HttpResponse<unknown> = {
301
+ data: null,
302
+ status: response.status,
303
+ statusText: response.statusText,
304
+ headers: this._parseHeaders(response.headers),
305
+ ok: false,
306
+ };
307
+ throw new HttpError(`HTTP ${response.status}: ${response.statusText}`, errorResponse);
308
+ }
309
+
310
+ return response;
311
+ } finally {
312
+ if (timeoutId !== undefined) {
313
+ clearTimeout(timeoutId);
314
+ }
315
+ }
316
+ }
317
+
318
+ private async _parseResponse<T>(
319
+ response: Response,
320
+ responseType?: 'json' | 'text' | 'blob' | 'arraybuffer',
321
+ ): Promise<HttpResponse<T>> {
322
+ let data: T;
323
+ switch (responseType) {
324
+ case 'text':
325
+ data = (await response.text()) as unknown as T;
326
+ break;
327
+ case 'blob':
328
+ data = (await response.blob()) as unknown as T;
329
+ break;
330
+ case 'arraybuffer':
331
+ data = (await response.arrayBuffer()) as unknown as T;
332
+ break;
333
+ case 'json':
334
+ default: {
335
+ const text = await response.text();
336
+ data = text ? JSON.parse(text) : (null as unknown as T);
337
+ break;
338
+ }
339
+ }
340
+
341
+ return {
342
+ data,
343
+ status: response.status,
344
+ statusText: response.statusText,
345
+ headers: this._parseHeaders(response.headers),
346
+ ok: response.ok,
347
+ };
348
+ }
349
+
350
+ private _parseHeaders(headers: Headers): Record<string, string> {
351
+ const result: Record<string, string> = {};
352
+ headers.forEach((value, key) => {
353
+ result[key] = value;
354
+ });
355
+ return result;
356
+ }
357
+ }
358
+
359
+ // --- Convenience exports ---
360
+
361
+ export function createHttpClient(options?: HttpClientOptions): HttpClient {
362
+ return new HttpClient(options);
363
+ }
364
+
365
+ export const http: HttpClient = new HttpClient();
366
+
367
+ // --- Convenience methods for the default http client ---
368
+
369
+ /**
370
+ * 便捷 GET 请求
371
+ */
372
+ export function get<T = unknown>(url: string, options?: RequestOptions): Promise<HttpResponse<T>> {
373
+ return http.get<T>(url, options);
374
+ }
375
+
376
+ /**
377
+ * 便捷 POST 请求
378
+ */
379
+ export function post<T = unknown>(
380
+ url: string,
381
+ data?: unknown,
382
+ options?: RequestOptions,
383
+ ): Promise<HttpResponse<T>> {
384
+ return http.post<T>(url, data, options);
385
+ }
386
+
387
+ /**
388
+ * 便捷 PUT 请求
389
+ */
390
+ export function put<T = unknown>(
391
+ url: string,
392
+ data?: unknown,
393
+ options?: RequestOptions,
394
+ ): Promise<HttpResponse<T>> {
395
+ return http.put<T>(url, data, options);
396
+ }
397
+
398
+ /**
399
+ * 便捷 DELETE 请求
400
+ */
401
+ export function del<T = unknown>(url: string, options?: RequestOptions): Promise<HttpResponse<T>> {
402
+ return http.delete<T>(url, options);
403
+ }
404
+
405
+ /**
406
+ * 便捷 PATCH 请求
407
+ */
408
+ export function patch<T = unknown>(
409
+ url: string,
410
+ data?: unknown,
411
+ options?: RequestOptions,
412
+ ): Promise<HttpResponse<T>> {
413
+ return http.patch<T>(url, data, options);
414
+ }
415
+
416
+ /**
417
+ * 发送 JSON 请求并直接获取数据
418
+ * 自动处理响应解析和错误
419
+ */
420
+ export async function requestJson<T = unknown>(
421
+ method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE',
422
+ url: string,
423
+ options?: RequestOptions & { data?: unknown },
424
+ ): Promise<T> {
425
+ const { data, ...config } = options || {};
426
+
427
+ if (method === 'GET' || method === 'DELETE') {
428
+ const response = await http.request<T>(method, url, config);
429
+ return response.data;
430
+ }
431
+
432
+ const response = await http.request<T>(method, url, { ...config, body: data });
433
+ return response.data;
434
+ }
435
+
436
+ /**
437
+ * 发送 GET JSON 请求
438
+ */
439
+ export async function getJson<T = unknown>(url: string, options?: RequestOptions): Promise<T> {
440
+ return requestJson<T>('GET', url, options);
441
+ }
442
+
443
+ /**
444
+ * 发送 POST JSON 请求
445
+ */
446
+ export async function postJson<T = unknown>(
447
+ url: string,
448
+ data?: unknown,
449
+ options?: RequestOptions,
450
+ ): Promise<T> {
451
+ return requestJson<T>('POST', url, { ...options, data });
452
+ }
453
+
454
+ /**
455
+ * 发送 PUT JSON 请求
456
+ */
457
+ export async function putJson<T = unknown>(
458
+ url: string,
459
+ data?: unknown,
460
+ options?: RequestOptions,
461
+ ): Promise<T> {
462
+ return requestJson<T>('PUT', url, { ...options, data });
463
+ }
464
+
465
+ /**
466
+ * 发送 PATCH JSON 请求
467
+ */
468
+ export async function patchJson<T = unknown>(
469
+ url: string,
470
+ data?: unknown,
471
+ options?: RequestOptions,
472
+ ): Promise<T> {
473
+ return requestJson<T>('PATCH', url, { ...options, data });
474
+ }
475
+
476
+ /**
477
+ * 发送 DELETE JSON 请求
478
+ */
479
+ export async function deleteJson<T = unknown>(url: string, options?: RequestOptions): Promise<T> {
480
+ return requestJson<T>('DELETE', url, options);
481
+ }