@stdex/request 1.0.1 → 1.2.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/README.md +1 -1
- package/dist/client.d.ts +2 -6
- package/dist/client.js +14 -20
- package/dist/instance.d.ts +1 -1
- package/dist/options.d.ts +5 -2
- package/package.json +9 -10
- package/src/client.ts +15 -12
- package/src/options.ts +7 -2
package/README.md
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
#
|
|
1
|
+
# 可靠、可定制的 HTTP 请求库
|
package/dist/client.d.ts
CHANGED
|
@@ -2,17 +2,13 @@
|
|
|
2
2
|
* 实现一个功能完善、异常处理逻辑全面的请求发起器。
|
|
3
3
|
* 可通过继承子类来扩展其功能。
|
|
4
4
|
*/
|
|
5
|
-
import { type Logger } from '@stdex/logging';
|
|
6
5
|
import { type Result, type Success, type Failure } from '@stdex/utils/result.js';
|
|
7
6
|
import { type RequestFailure } from './error.js';
|
|
8
7
|
import type { Options, PredefinedOptions, FormattedOptions } from './options.js';
|
|
9
8
|
/** 此基类不可直接使用,因其未对错误格式进行具体约定 */
|
|
10
9
|
export declare abstract class BaseRequestClient<FailureT extends Failure> {
|
|
11
|
-
logger: Logger;
|
|
12
10
|
prefefinedOptions: PredefinedOptions;
|
|
13
|
-
constructor(options?: PredefinedOptions
|
|
14
|
-
logger?: string | Logger;
|
|
15
|
-
});
|
|
11
|
+
constructor(options?: PredefinedOptions);
|
|
16
12
|
/** 生成一个快捷方式函数,调用它相当于调用 client.request() */
|
|
17
13
|
asFunction(): <T>(inputUrl: string, inputOptions?: Options<T>) => Promise<Result<T, FailureT>>;
|
|
18
14
|
request<T>(inputUrl: string, inputOptions?: Options<T>): Promise<Result<T, FailureT>>;
|
|
@@ -30,7 +26,7 @@ export declare abstract class BaseRequestClient<FailureT extends Failure> {
|
|
|
30
26
|
* - 其他情况,返回纯文本结果
|
|
31
27
|
*/
|
|
32
28
|
protected parseResponse(options: FormattedOptions, response: Response): Promise<Result<unknown, FailureT>>;
|
|
33
|
-
protected handleError(result: RequestFailure): FailureT;
|
|
29
|
+
protected handleError(result: RequestFailure, options: FormattedOptions): FailureT;
|
|
34
30
|
/** 生成符合 FailureT 约定的失败结果 */
|
|
35
31
|
protected abstract makeFailureResult(result: RequestFailure): FailureT;
|
|
36
32
|
/**
|
package/dist/client.js
CHANGED
|
@@ -2,21 +2,14 @@
|
|
|
2
2
|
* 实现一个功能完善、异常处理逻辑全面的请求发起器。
|
|
3
3
|
* 可通过继承子类来扩展其功能。
|
|
4
4
|
*/
|
|
5
|
-
import { getLogger } from '@stdex/logging';
|
|
6
5
|
import { success, failure, formatFailure, exceptionToFailure, } from '@stdex/utils/result.js';
|
|
7
6
|
import { combineUrl } from '@stdex/utils/url.js';
|
|
8
7
|
import isPlainObject from 'lodash/isPlainObject.js';
|
|
9
8
|
import pick from 'lodash/pick.js';
|
|
10
9
|
/** 此基类不可直接使用,因其未对错误格式进行具体约定 */
|
|
11
10
|
export class BaseRequestClient {
|
|
12
|
-
logger;
|
|
13
11
|
prefefinedOptions;
|
|
14
12
|
constructor(options = {}) {
|
|
15
|
-
this.logger = !options.logger
|
|
16
|
-
? getLogger('request')
|
|
17
|
-
: typeof options.logger === 'string'
|
|
18
|
-
? getLogger(options.logger)
|
|
19
|
-
: options.logger;
|
|
20
13
|
this.prefefinedOptions = options;
|
|
21
14
|
}
|
|
22
15
|
/** 生成一个快捷方式函数,调用它相当于调用 client.request() */
|
|
@@ -36,7 +29,7 @@ export class BaseRequestClient {
|
|
|
36
29
|
do {
|
|
37
30
|
tryCount += 1;
|
|
38
31
|
if (tryCount > 1) {
|
|
39
|
-
|
|
32
|
+
options.logger?.warn('Retrying Request', { ...options, tryCount });
|
|
40
33
|
}
|
|
41
34
|
result = await this.realRequest(options);
|
|
42
35
|
} while (!result.success && tryCount < options.retry + 1);
|
|
@@ -57,14 +50,14 @@ export class BaseRequestClient {
|
|
|
57
50
|
// 失败情形“手动取消”
|
|
58
51
|
if (manualSignal?.aborted && manualSignal.reason === error) {
|
|
59
52
|
const reason = error instanceof DOMException && error.name === 'AbortError' ? null : error;
|
|
60
|
-
return this.handleError(failure('Request Was Canceled', 'RequestAborted', { options, reason }));
|
|
53
|
+
return this.handleError(failure('Request Was Canceled', 'RequestAborted', { options, reason }), options);
|
|
61
54
|
}
|
|
62
55
|
// 失败情形“请求超时”
|
|
63
56
|
if (error instanceof DOMException && error.name === 'TimeoutError') {
|
|
64
|
-
return this.handleError(failure('Request Timed Out', 'RequestTimeout', { options }));
|
|
57
|
+
return this.handleError(failure('Request Timed Out', 'RequestTimeout', { options }), options);
|
|
65
58
|
}
|
|
66
59
|
// 失败情形“请求发起失败”
|
|
67
|
-
return this.handleError(failure("Couldn't Send Request", 'RequestSendFailed', { options, originalError: error }));
|
|
60
|
+
return this.handleError(failure("Couldn't Send Request", 'RequestSendFailed', { options, originalError: error }), options);
|
|
68
61
|
}
|
|
69
62
|
// 失败情形“失败状态码”
|
|
70
63
|
if (!response.status.toString().startsWith('2')) {
|
|
@@ -77,7 +70,7 @@ export class BaseRequestClient {
|
|
|
77
70
|
status: response.status,
|
|
78
71
|
response,
|
|
79
72
|
responseData,
|
|
80
|
-
}));
|
|
73
|
+
}), options);
|
|
81
74
|
}
|
|
82
75
|
// 解析响应内容
|
|
83
76
|
let result = await this.parseResponse(options, response);
|
|
@@ -92,7 +85,7 @@ export class BaseRequestClient {
|
|
|
92
85
|
originalError: error,
|
|
93
86
|
response,
|
|
94
87
|
responseData: result.data,
|
|
95
|
-
}));
|
|
88
|
+
}), options);
|
|
96
89
|
}
|
|
97
90
|
}
|
|
98
91
|
if (result.success && format) {
|
|
@@ -106,7 +99,7 @@ export class BaseRequestClient {
|
|
|
106
99
|
originalError: error,
|
|
107
100
|
response,
|
|
108
101
|
responseData: result.data,
|
|
109
|
-
}));
|
|
102
|
+
}), options);
|
|
110
103
|
}
|
|
111
104
|
}
|
|
112
105
|
return result;
|
|
@@ -117,7 +110,7 @@ export class BaseRequestClient {
|
|
|
117
110
|
async formatOptions(input) {
|
|
118
111
|
const predefined = this.prefefinedOptions;
|
|
119
112
|
const { urlPrefix = predefined.urlPrefix ?? '', url: rawUrl, query = {}, method = predefined.method ?? 'GET', headers: rawHeaders = {}, body: rawBody = null, data, // eslint-disable-line @typescript-eslint/no-unsafe-assignment
|
|
120
|
-
binary = false, timeout = predefined.timeout ?? 0, signal, retry = predefined.retry ?? 0, format, } = input;
|
|
113
|
+
binary = false, timeout = predefined.timeout ?? 0, signal, retry = predefined.retry ?? 0, format, logger = predefined.logger, } = input;
|
|
121
114
|
const headers = {
|
|
122
115
|
...(predefined.headers ?? {}),
|
|
123
116
|
...rawHeaders,
|
|
@@ -145,6 +138,7 @@ export class BaseRequestClient {
|
|
|
145
138
|
signal,
|
|
146
139
|
format,
|
|
147
140
|
retry,
|
|
141
|
+
logger,
|
|
148
142
|
};
|
|
149
143
|
Object.assign(options.headers, await this.getHeaders(options, input));
|
|
150
144
|
return options;
|
|
@@ -178,7 +172,7 @@ export class BaseRequestClient {
|
|
|
178
172
|
status: response.status,
|
|
179
173
|
originalError: result.data,
|
|
180
174
|
response,
|
|
181
|
-
})));
|
|
175
|
+
}), options));
|
|
182
176
|
}
|
|
183
177
|
const textResult = await exceptionToFailure(response.text());
|
|
184
178
|
if (contentType.startsWith('text/') || !textResult.success) {
|
|
@@ -187,7 +181,7 @@ export class BaseRequestClient {
|
|
|
187
181
|
status: response.status,
|
|
188
182
|
originalError: result.data,
|
|
189
183
|
response,
|
|
190
|
-
})));
|
|
184
|
+
}), options));
|
|
191
185
|
}
|
|
192
186
|
let jsonParseError;
|
|
193
187
|
try {
|
|
@@ -202,17 +196,17 @@ export class BaseRequestClient {
|
|
|
202
196
|
status: response.status,
|
|
203
197
|
originalError: jsonParseError,
|
|
204
198
|
response,
|
|
205
|
-
}));
|
|
199
|
+
}), options);
|
|
206
200
|
}
|
|
207
201
|
return textResult;
|
|
208
202
|
}
|
|
209
|
-
handleError(result) {
|
|
203
|
+
handleError(result, options) {
|
|
210
204
|
const info = pick(result.data.options, ['url', 'method']);
|
|
211
205
|
for (const key of ['originalError', 'status', 'responseData']) {
|
|
212
206
|
if (key in result.data)
|
|
213
207
|
Object.assign(info, pick(result.data, [key]));
|
|
214
208
|
}
|
|
215
|
-
|
|
209
|
+
options.logger?.error(result.code, info);
|
|
216
210
|
return this.makeFailureResult(result);
|
|
217
211
|
}
|
|
218
212
|
/**
|
package/dist/instance.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import { RequestClient } from './client.js';
|
|
2
2
|
/** 提供一个即取即用的 RequestClient 实例及其函数版本 */
|
|
3
3
|
export declare const client: RequestClient;
|
|
4
|
-
export declare const request: <T>(inputUrl: string, inputOptions?: import("./options.js").Options<T> | undefined) => Promise<import("@stdex/utils
|
|
4
|
+
export declare const request: <T>(inputUrl: string, inputOptions?: import("./options.js").Options<T> | undefined) => Promise<import("@stdex/utils").Result<T, import("./error.js").RequestFailure>>;
|
package/dist/options.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type GeneralLogger } from '@stdex/utils';
|
|
1
2
|
/**
|
|
2
3
|
* 请求参数
|
|
3
4
|
*/
|
|
@@ -26,8 +27,10 @@ export interface Options<T = unknown> {
|
|
|
26
27
|
retry?: number;
|
|
27
28
|
/** 对请求成功时得到的数据进行最终格式化 */
|
|
28
29
|
format?: (responseData: any) => T;
|
|
30
|
+
/** 记录请求相关日志 */
|
|
31
|
+
logger?: GeneralLogger;
|
|
29
32
|
}
|
|
30
33
|
/** 可预指定的请求参数 */
|
|
31
|
-
export type PredefinedOptions = Pick<Options, 'urlPrefix' | 'method' | 'headers' | 'timeout' | 'retry'>;
|
|
34
|
+
export type PredefinedOptions = Pick<Options, 'urlPrefix' | 'method' | 'headers' | 'timeout' | 'retry' | 'logger'>;
|
|
32
35
|
/** 经过整理的请求参数(client 内部使用) */
|
|
33
|
-
export type FormattedOptions<T = unknown> = Required<Pick<Options, 'url' | 'method' | 'headers' | 'body' | 'binary' | 'timeout' | 'retry'>> & Pick<Options<T>, 'signal' | 'format'>;
|
|
36
|
+
export type FormattedOptions<T = unknown> = Required<Pick<Options, 'url' | 'method' | 'headers' | 'body' | 'binary' | 'timeout' | 'retry'>> & Pick<Options<T>, 'signal' | 'format' | 'logger'>;
|
package/package.json
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stdex/request",
|
|
3
|
-
"version": "1.0
|
|
4
|
-
"description": "
|
|
5
|
-
"homepage": "https://github.com/anjianshi/stdex-request",
|
|
3
|
+
"version": "1.2.0",
|
|
4
|
+
"description": "可靠、可定制的 HTTP 请求库",
|
|
5
|
+
"homepage": "https://github.com/anjianshi/stdex-packages/request",
|
|
6
6
|
"bugs": {
|
|
7
|
-
"url": "https://github.com/anjianshi/stdex-
|
|
7
|
+
"url": "https://github.com/anjianshi/stdex-packages/issues",
|
|
8
8
|
"email": "anjianshi@gmail.com"
|
|
9
9
|
},
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"author": "anjianshi <anjianshi@gmail.com>",
|
|
12
12
|
"repository": {
|
|
13
13
|
"type": "git",
|
|
14
|
-
"url": "git+https://github.com/anjianshi/stdex-
|
|
14
|
+
"url": "git+https://github.com/anjianshi/stdex-packages.git"
|
|
15
15
|
},
|
|
16
16
|
"publishConfig": {
|
|
17
17
|
"registry": "https://registry.npmjs.org/",
|
|
@@ -25,14 +25,13 @@
|
|
|
25
25
|
"dist/"
|
|
26
26
|
],
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@stdex/
|
|
29
|
-
"@stdex/utils": "^1.0.2",
|
|
28
|
+
"@stdex/utils": "1.2.0",
|
|
30
29
|
"lodash": "^4.18.1"
|
|
31
30
|
},
|
|
32
31
|
"devDependencies": {
|
|
33
|
-
"@stdex/presets-eslint-typescript": "
|
|
34
|
-
"@stdex/presets-prettier": "
|
|
35
|
-
"@stdex/presets-typescript": "
|
|
32
|
+
"@stdex/presets-eslint-typescript": "1.2.0",
|
|
33
|
+
"@stdex/presets-prettier": "1.2.0",
|
|
34
|
+
"@stdex/presets-typescript": "1.2.0",
|
|
36
35
|
"@types/lodash": "^4.17.27",
|
|
37
36
|
"eslint": "^10.6.0",
|
|
38
37
|
"typescript": "^6.0.3"
|
package/src/client.ts
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
* 实现一个功能完善、异常处理逻辑全面的请求发起器。
|
|
3
3
|
* 可通过继承子类来扩展其功能。
|
|
4
4
|
*/
|
|
5
|
-
import { getLogger, type Logger } from '@stdex/logging'
|
|
6
5
|
import {
|
|
7
6
|
success,
|
|
8
7
|
failure,
|
|
@@ -20,15 +19,9 @@ import type { Options, PredefinedOptions, FormattedOptions } from './options.js'
|
|
|
20
19
|
|
|
21
20
|
/** 此基类不可直接使用,因其未对错误格式进行具体约定 */
|
|
22
21
|
export abstract class BaseRequestClient<FailureT extends Failure> {
|
|
23
|
-
logger: Logger
|
|
24
22
|
prefefinedOptions: PredefinedOptions
|
|
25
23
|
|
|
26
|
-
constructor(options: PredefinedOptions
|
|
27
|
-
this.logger = !options.logger
|
|
28
|
-
? getLogger('request')
|
|
29
|
-
: typeof options.logger === 'string'
|
|
30
|
-
? getLogger(options.logger)
|
|
31
|
-
: options.logger
|
|
24
|
+
constructor(options: PredefinedOptions = {}) {
|
|
32
25
|
this.prefefinedOptions = options
|
|
33
26
|
}
|
|
34
27
|
|
|
@@ -53,7 +46,7 @@ export abstract class BaseRequestClient<FailureT extends Failure> {
|
|
|
53
46
|
do {
|
|
54
47
|
tryCount += 1
|
|
55
48
|
if (tryCount > 1) {
|
|
56
|
-
|
|
49
|
+
options.logger?.warn('Retrying Request', { ...options, tryCount })
|
|
57
50
|
}
|
|
58
51
|
result = await this.realRequest<T>(options)
|
|
59
52
|
} while (!result.success && tryCount < options.retry + 1)
|
|
@@ -79,15 +72,17 @@ export abstract class BaseRequestClient<FailureT extends Failure> {
|
|
|
79
72
|
const reason = error instanceof DOMException && error.name === 'AbortError' ? null : error
|
|
80
73
|
return this.handleError(
|
|
81
74
|
failure('Request Was Canceled', 'RequestAborted', { options, reason }),
|
|
75
|
+
options
|
|
82
76
|
)
|
|
83
77
|
}
|
|
84
78
|
// 失败情形“请求超时”
|
|
85
79
|
if (error instanceof DOMException && error.name === 'TimeoutError') {
|
|
86
|
-
return this.handleError(failure('Request Timed Out', 'RequestTimeout', { options }))
|
|
80
|
+
return this.handleError(failure('Request Timed Out', 'RequestTimeout', { options }), options)
|
|
87
81
|
}
|
|
88
82
|
// 失败情形“请求发起失败”
|
|
89
83
|
return this.handleError(
|
|
90
84
|
failure("Couldn't Send Request", 'RequestSendFailed', { options, originalError: error }),
|
|
85
|
+
options
|
|
91
86
|
)
|
|
92
87
|
}
|
|
93
88
|
|
|
@@ -104,6 +99,7 @@ export abstract class BaseRequestClient<FailureT extends Failure> {
|
|
|
104
99
|
response,
|
|
105
100
|
responseData,
|
|
106
101
|
}),
|
|
102
|
+
options
|
|
107
103
|
)
|
|
108
104
|
}
|
|
109
105
|
|
|
@@ -122,6 +118,7 @@ export abstract class BaseRequestClient<FailureT extends Failure> {
|
|
|
122
118
|
response,
|
|
123
119
|
responseData: result.data,
|
|
124
120
|
}),
|
|
121
|
+
options
|
|
125
122
|
)
|
|
126
123
|
}
|
|
127
124
|
}
|
|
@@ -141,6 +138,7 @@ export abstract class BaseRequestClient<FailureT extends Failure> {
|
|
|
141
138
|
responseData: result.data,
|
|
142
139
|
},
|
|
143
140
|
),
|
|
141
|
+
options
|
|
144
142
|
)
|
|
145
143
|
}
|
|
146
144
|
}
|
|
@@ -166,6 +164,7 @@ export abstract class BaseRequestClient<FailureT extends Failure> {
|
|
|
166
164
|
signal,
|
|
167
165
|
retry = predefined.retry ?? 0,
|
|
168
166
|
format,
|
|
167
|
+
logger = predefined.logger,
|
|
169
168
|
} = input
|
|
170
169
|
|
|
171
170
|
const headers = {
|
|
@@ -197,6 +196,7 @@ export abstract class BaseRequestClient<FailureT extends Failure> {
|
|
|
197
196
|
signal,
|
|
198
197
|
format,
|
|
199
198
|
retry,
|
|
199
|
+
logger,
|
|
200
200
|
}
|
|
201
201
|
Object.assign(options.headers, await this.getHeaders(options, input))
|
|
202
202
|
return options
|
|
@@ -241,6 +241,7 @@ export abstract class BaseRequestClient<FailureT extends Failure> {
|
|
|
241
241
|
originalError: result.data,
|
|
242
242
|
response,
|
|
243
243
|
}),
|
|
244
|
+
options,
|
|
244
245
|
),
|
|
245
246
|
)
|
|
246
247
|
}
|
|
@@ -255,6 +256,7 @@ export abstract class BaseRequestClient<FailureT extends Failure> {
|
|
|
255
256
|
originalError: result.data,
|
|
256
257
|
response,
|
|
257
258
|
}),
|
|
259
|
+
options,
|
|
258
260
|
),
|
|
259
261
|
)
|
|
260
262
|
}
|
|
@@ -273,17 +275,18 @@ export abstract class BaseRequestClient<FailureT extends Failure> {
|
|
|
273
275
|
originalError: jsonParseError,
|
|
274
276
|
response,
|
|
275
277
|
}),
|
|
278
|
+
options
|
|
276
279
|
)
|
|
277
280
|
}
|
|
278
281
|
return textResult
|
|
279
282
|
}
|
|
280
283
|
|
|
281
|
-
protected handleError(result: RequestFailure) {
|
|
284
|
+
protected handleError(result: RequestFailure, options: FormattedOptions) {
|
|
282
285
|
const info: Record<string, unknown> = pick(result.data.options, ['url', 'method'])
|
|
283
286
|
for (const key of ['originalError', 'status', 'responseData'] as const) {
|
|
284
287
|
if (key in result.data) Object.assign(info, pick(result.data, [key]))
|
|
285
288
|
}
|
|
286
|
-
|
|
289
|
+
options.logger?.error(result.code, info)
|
|
287
290
|
return this.makeFailureResult(result)
|
|
288
291
|
}
|
|
289
292
|
|
package/src/options.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { type GeneralLogger } from '@stdex/utils'
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* 请求参数
|
|
3
5
|
*/
|
|
@@ -34,16 +36,19 @@ export interface Options<T = unknown> {
|
|
|
34
36
|
/** 对请求成功时得到的数据进行最终格式化 */
|
|
35
37
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
36
38
|
format?: (responseData: any) => T
|
|
39
|
+
|
|
40
|
+
/** 记录请求相关日志 */
|
|
41
|
+
logger?: GeneralLogger
|
|
37
42
|
}
|
|
38
43
|
|
|
39
44
|
/** 可预指定的请求参数 */
|
|
40
45
|
export type PredefinedOptions = Pick<
|
|
41
46
|
Options,
|
|
42
|
-
'urlPrefix' | 'method' | 'headers' | 'timeout' | 'retry'
|
|
47
|
+
'urlPrefix' | 'method' | 'headers' | 'timeout' | 'retry' | 'logger'
|
|
43
48
|
>
|
|
44
49
|
|
|
45
50
|
/** 经过整理的请求参数(client 内部使用) */
|
|
46
51
|
export type FormattedOptions<T = unknown> = Required<
|
|
47
52
|
Pick<Options, 'url' | 'method' | 'headers' | 'body' | 'binary' | 'timeout' | 'retry'>
|
|
48
53
|
> &
|
|
49
|
-
Pick<Options<T>, 'signal' | 'format'>
|
|
54
|
+
Pick<Options<T>, 'signal' | 'format' | 'logger'>
|