@clickup/rest-client 2.10.294 → 2.11.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/.eslintrc.base.js +12 -2
- package/.eslintrc.js +4 -4
- package/.github/workflows/ci.yml +26 -0
- package/.github/workflows/semgrep.yml +36 -0
- package/.prettierrc +8 -0
- package/.vscode/extensions.json +8 -0
- package/.vscode/tasks.json +20 -0
- package/README.md +2 -0
- package/dist/.eslintcache +1 -1
- package/dist/RestClient.js +2 -2
- package/dist/RestClient.js.map +1 -1
- package/dist/RestOptions.d.ts +3 -0
- package/dist/RestOptions.d.ts.map +1 -1
- package/dist/RestOptions.js +1 -0
- package/dist/RestOptions.js.map +1 -1
- package/dist/RestRequest.d.ts.map +1 -1
- package/dist/RestRequest.js +1 -0
- package/dist/RestRequest.js.map +1 -1
- package/dist/errors/RestRateLimitError.d.ts.map +1 -1
- package/dist/errors/RestRateLimitError.js.map +1 -1
- package/dist/errors/RestRetriableError.d.ts.map +1 -1
- package/dist/errors/RestRetriableError.js.map +1 -1
- package/dist/errors/RestTokenInvalidError.d.ts.map +1 -1
- package/dist/errors/RestTokenInvalidError.js.map +1 -1
- package/dist/index.d.ts +2 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -5
- package/dist/index.js.map +1 -1
- package/dist/internal/RestFetchReader.d.ts +4 -2
- package/dist/internal/RestFetchReader.d.ts.map +1 -1
- package/dist/internal/RestFetchReader.js +4 -4
- package/dist/internal/RestFetchReader.js.map +1 -1
- package/dist/internal/ellipsis.d.ts +6 -0
- package/dist/internal/ellipsis.d.ts.map +1 -0
- package/dist/internal/ellipsis.js +17 -0
- package/dist/internal/ellipsis.js.map +1 -0
- package/dist/internal/inferResBodyEncoding.js.map +1 -1
- package/dist/internal/inspectPossibleJSON.js +5 -8
- package/dist/internal/inspectPossibleJSON.js.map +1 -1
- package/dist/internal/throwIfErrorResponse.js +1 -1
- package/dist/internal/throwIfErrorResponse.js.map +1 -1
- package/dist/middlewares/paceRequests.d.ts +21 -2
- package/dist/middlewares/paceRequests.d.ts.map +1 -1
- package/dist/middlewares/paceRequests.js +3 -2
- package/dist/middlewares/paceRequests.js.map +1 -1
- package/docs/README.md +2 -0
- package/docs/classes/RestClient.md +32 -28
- package/docs/classes/RestContentSizeOverLimitError.md +5 -1
- package/docs/classes/RestError.md +5 -1
- package/docs/classes/RestRateLimitError.md +6 -2
- package/docs/classes/RestRequest.md +22 -18
- package/docs/classes/RestResponse.md +7 -3
- package/docs/classes/RestResponseError.md +5 -1
- package/docs/classes/RestRetriableError.md +6 -2
- package/docs/classes/RestStream.md +12 -8
- package/docs/classes/RestTimeoutError.md +5 -1
- package/docs/classes/RestTokenInvalidError.md +6 -2
- package/docs/interfaces/Middleware.md +4 -4
- package/docs/interfaces/Pacer.md +7 -12
- package/docs/interfaces/PacerOutcome.md +25 -0
- package/docs/interfaces/RestLogEvent.md +1 -1
- package/docs/interfaces/RestOptions.md +42 -30
- package/docs/interfaces/TokenGetter.md +3 -3
- package/docs/modules.md +8 -11
- package/internal/clean.sh +4 -0
- package/internal/deploy.sh +7 -0
- package/internal/docs.sh +6 -0
- package/internal/lint.sh +4 -0
- package/jest.config.base.js +13 -0
- package/jest.config.js +1 -10
- package/package.json +10 -6
- package/src/RestClient.ts +21 -21
- package/src/RestOptions.ts +6 -3
- package/src/RestRequest.ts +12 -11
- package/src/RestResponse.ts +1 -1
- package/src/RestStream.ts +1 -1
- package/src/__tests__/RestClient.test.ts +53 -0
- package/src/__tests__/RestFetchReader.test.ts +150 -0
- package/src/__tests__/RestRequest.test.ts +262 -0
- package/src/__tests__/RestStream.test.ts +63 -0
- package/src/__tests__/helpers.ts +173 -0
- package/src/errors/RestRateLimitError.ts +5 -1
- package/src/errors/RestResponseError.ts +3 -3
- package/src/errors/RestRetriableError.ts +5 -1
- package/src/errors/RestTokenInvalidError.ts +4 -1
- package/src/helpers/depaginate.ts +3 -3
- package/src/index.ts +2 -7
- package/src/internal/RestFetchReader.ts +6 -3
- package/src/internal/RestRangeUploader.ts +2 -2
- package/src/internal/calcRetryDelay.ts +2 -2
- package/src/internal/ellipsis.ts +16 -0
- package/src/internal/inferResBodyEncoding.ts +13 -13
- package/src/internal/inspectPossibleJSON.ts +6 -10
- package/src/internal/substituteParams.ts +1 -1
- package/src/internal/throwIfErrorResponse.ts +8 -8
- package/src/middlewares/paceRequests.ts +28 -3
- package/tsconfig.base.json +31 -0
- package/tsconfig.json +1 -32
- package/typedoc.config.js +20 -0
- package/dist/pacers/Pacer.d.ts +0 -21
- package/dist/pacers/Pacer.d.ts.map +0 -1
- package/dist/pacers/Pacer.js +0 -3
- package/dist/pacers/Pacer.js.map +0 -1
- package/dist/pacers/PacerComposite.d.ts +0 -14
- package/dist/pacers/PacerComposite.d.ts.map +0 -1
- package/dist/pacers/PacerComposite.js +0 -32
- package/dist/pacers/PacerComposite.js.map +0 -1
- package/dist/pacers/PacerQPS.d.ts +0 -53
- package/dist/pacers/PacerQPS.d.ts.map +0 -1
- package/dist/pacers/PacerQPS.js +0 -105
- package/dist/pacers/PacerQPS.js.map +0 -1
- package/docs/classes/PacerComposite.md +0 -62
- package/docs/classes/PacerQPS.md +0 -75
- package/docs/interfaces/PacerDelay.md +0 -25
- package/docs/interfaces/PacerQPSBackend.md +0 -44
- package/docs/interfaces/PacerQPSOptions.md +0 -40
- package/src/pacers/Pacer.ts +0 -22
- package/src/pacers/PacerComposite.ts +0 -29
- package/src/pacers/PacerQPS.ts +0 -147
- package/typedoc.json +0 -22
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import delay from "delay";
|
|
2
|
+
import range from "lodash/range";
|
|
3
|
+
import {
|
|
4
|
+
BINARY_BUF,
|
|
5
|
+
consumeIterable,
|
|
6
|
+
createFetchReader,
|
|
7
|
+
server,
|
|
8
|
+
serverAssertConnectionsCount,
|
|
9
|
+
TimeoutError,
|
|
10
|
+
UTF8_BUF,
|
|
11
|
+
} from "./helpers";
|
|
12
|
+
|
|
13
|
+
let ORIGIN: string;
|
|
14
|
+
|
|
15
|
+
beforeAll(async () => {
|
|
16
|
+
ORIGIN = await new Promise((resolve) => {
|
|
17
|
+
server.listen(0, () =>
|
|
18
|
+
resolve("http://127.0.0.1:" + (server.address() as any).port),
|
|
19
|
+
);
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
beforeEach(async () => {
|
|
24
|
+
await serverAssertConnectionsCount(0);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("small_response", async () => {
|
|
28
|
+
const reader = createFetchReader(`${ORIGIN}/small`);
|
|
29
|
+
await reader.preload(1024);
|
|
30
|
+
|
|
31
|
+
expect(reader.textFetched).toEqual("ok");
|
|
32
|
+
expect(reader.textIsPartial).toBeFalsy();
|
|
33
|
+
expect(reader.charsRead).toEqual(reader.textFetched.length);
|
|
34
|
+
|
|
35
|
+
for await (const _ of reader) {
|
|
36
|
+
throw "Must not return any more data";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
await serverAssertConnectionsCount(0);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("large_response", async () => {
|
|
43
|
+
const reader = createFetchReader(`${ORIGIN}/large`);
|
|
44
|
+
await reader.preload(42);
|
|
45
|
+
await serverAssertConnectionsCount(1);
|
|
46
|
+
|
|
47
|
+
expect(reader.textFetched.length).toBeGreaterThan(1);
|
|
48
|
+
expect(reader.textFetched.length).toBeLessThan(1024 * 10);
|
|
49
|
+
expect(reader.textIsPartial).toBeTruthy();
|
|
50
|
+
expect(reader.charsRead).toEqual(reader.textFetched.length);
|
|
51
|
+
|
|
52
|
+
const rest = await consumeIterable(reader);
|
|
53
|
+
await serverAssertConnectionsCount(0);
|
|
54
|
+
expect(rest.length).toBeGreaterThan(1);
|
|
55
|
+
expect(reader.textIsPartial).toBeTruthy();
|
|
56
|
+
expect(reader.charsRead).toEqual(reader.textFetched.length + rest.length);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("slow_response", async () => {
|
|
60
|
+
const reader = createFetchReader(`${ORIGIN}/slow`);
|
|
61
|
+
await reader.preload(42);
|
|
62
|
+
await delay(300);
|
|
63
|
+
await serverAssertConnectionsCount(1);
|
|
64
|
+
await reader[Symbol.asyncIterator]().return();
|
|
65
|
+
await serverAssertConnectionsCount(0);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("small_slow_utf8_response", async () => {
|
|
69
|
+
const reader = createFetchReader(`${ORIGIN}/small_slow_utf8`);
|
|
70
|
+
await reader.preload(1);
|
|
71
|
+
await serverAssertConnectionsCount(1);
|
|
72
|
+
expect(reader.textFetched.length).toEqual(1);
|
|
73
|
+
expect(reader.charsRead).toEqual(1);
|
|
74
|
+
const rest = await consumeIterable(reader);
|
|
75
|
+
expect(reader.textFetched + rest).toEqual(UTF8_BUF.toString());
|
|
76
|
+
await serverAssertConnectionsCount(0);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("too_big_response", async () => {
|
|
80
|
+
const reader = createFetchReader(`${ORIGIN}/large`, {
|
|
81
|
+
onAfterRead: (reader) => {
|
|
82
|
+
if (reader.charsRead > 1024 * 64) {
|
|
83
|
+
throw Error("too large");
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
await reader.preload(42);
|
|
88
|
+
await serverAssertConnectionsCount(1);
|
|
89
|
+
await expect(async () => consumeIterable(reader, 10)).rejects.toThrow(
|
|
90
|
+
"too large",
|
|
91
|
+
);
|
|
92
|
+
await serverAssertConnectionsCount(0);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("timeout_response_no_preload_long_read_delay", async () => {
|
|
96
|
+
const reader = createFetchReader(`${ORIGIN}/slow`, { timeoutMs: 200 });
|
|
97
|
+
await expect(async () => consumeIterable(reader, 500)).rejects.toThrow(
|
|
98
|
+
TimeoutError,
|
|
99
|
+
);
|
|
100
|
+
await serverAssertConnectionsCount(0);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("timeout_response_no_preload_short_read_delay", async () => {
|
|
104
|
+
const reader = createFetchReader(`${ORIGIN}/slow`, { timeoutMs: 200 });
|
|
105
|
+
await expect(async () => consumeIterable(reader, 10)).rejects.toThrow(
|
|
106
|
+
TimeoutError,
|
|
107
|
+
);
|
|
108
|
+
await serverAssertConnectionsCount(0);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("timeout_in_preload", async () => {
|
|
112
|
+
const reader = createFetchReader(`${ORIGIN}/slow`, { timeoutMs: 200 });
|
|
113
|
+
await expect(async () => reader.preload(10000000)).rejects.toThrow(
|
|
114
|
+
TimeoutError,
|
|
115
|
+
);
|
|
116
|
+
await serverAssertConnectionsCount(0);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("timeout_in_stream_after_preload_succeeded", async () => {
|
|
120
|
+
const reader = createFetchReader(`${ORIGIN}/slow`, { timeoutMs: 200 });
|
|
121
|
+
await reader.preload(42);
|
|
122
|
+
await serverAssertConnectionsCount(1);
|
|
123
|
+
await expect(async () => consumeIterable(reader, 10)).rejects.toThrow(
|
|
124
|
+
TimeoutError,
|
|
125
|
+
);
|
|
126
|
+
await serverAssertConnectionsCount(0);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("timeout_after_reader_waited_for_too_long", async () => {
|
|
130
|
+
const reader = createFetchReader(`${ORIGIN}/slow`, { timeoutMs: 200 });
|
|
131
|
+
await reader.preload(42);
|
|
132
|
+
await serverAssertConnectionsCount(1);
|
|
133
|
+
await delay(500);
|
|
134
|
+
await expect(async () => consumeIterable(reader, 10)).rejects.toThrow(
|
|
135
|
+
TimeoutError,
|
|
136
|
+
);
|
|
137
|
+
await serverAssertConnectionsCount(0);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("read_binary_data", async () => {
|
|
141
|
+
const reader = createFetchReader(`${ORIGIN}/binary`, { timeoutMs: 2000 });
|
|
142
|
+
await reader.preload(42);
|
|
143
|
+
const preload = reader.textFetched;
|
|
144
|
+
expect(reader.textIsPartial).toBeTruthy();
|
|
145
|
+
const data = await consumeIterable(reader, 10);
|
|
146
|
+
expect(Buffer.from(preload + data, "binary")).toEqual(
|
|
147
|
+
Buffer.concat(range(10).map(() => BINARY_BUF)),
|
|
148
|
+
);
|
|
149
|
+
await serverAssertConnectionsCount(0);
|
|
150
|
+
});
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { FetchError, Headers } from "node-fetch";
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_OPTIONS,
|
|
4
|
+
RestClient,
|
|
5
|
+
RestRateLimitError,
|
|
6
|
+
RestResponseError,
|
|
7
|
+
RestTokenInvalidError,
|
|
8
|
+
} from "..";
|
|
9
|
+
import calcRetryDelay from "../internal/calcRetryDelay";
|
|
10
|
+
import throwIfErrorResponse from "../internal/throwIfErrorResponse";
|
|
11
|
+
import RestResponse from "../RestResponse";
|
|
12
|
+
|
|
13
|
+
const REQUEST = new RestClient().get("https://example.com");
|
|
14
|
+
const SUCCESS_RESPONSE = new RestResponse(
|
|
15
|
+
REQUEST,
|
|
16
|
+
null,
|
|
17
|
+
200,
|
|
18
|
+
new Headers(),
|
|
19
|
+
"",
|
|
20
|
+
false,
|
|
21
|
+
);
|
|
22
|
+
const ERROR_RESPONSE = new RestResponse(
|
|
23
|
+
REQUEST,
|
|
24
|
+
null,
|
|
25
|
+
500,
|
|
26
|
+
new Headers(),
|
|
27
|
+
"",
|
|
28
|
+
false,
|
|
29
|
+
);
|
|
30
|
+
const NOT_FOUND_RESPONSE = new RestResponse(
|
|
31
|
+
REQUEST,
|
|
32
|
+
null,
|
|
33
|
+
404,
|
|
34
|
+
new Headers(),
|
|
35
|
+
"",
|
|
36
|
+
false,
|
|
37
|
+
);
|
|
38
|
+
const TOO_MANY_REQUESTS_RESPONSE = new RestResponse(
|
|
39
|
+
REQUEST,
|
|
40
|
+
null,
|
|
41
|
+
429,
|
|
42
|
+
new Headers(),
|
|
43
|
+
"",
|
|
44
|
+
false,
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
test("isSuccessResponse", () => {
|
|
48
|
+
expect(
|
|
49
|
+
throwIfErrorResponse(
|
|
50
|
+
{ ...DEFAULT_OPTIONS, isSuccessResponse: () => "SUCCESS" },
|
|
51
|
+
ERROR_RESPONSE,
|
|
52
|
+
),
|
|
53
|
+
).toBeUndefined();
|
|
54
|
+
|
|
55
|
+
expect(() =>
|
|
56
|
+
throwIfErrorResponse(
|
|
57
|
+
{ ...DEFAULT_OPTIONS, isSuccessResponse: () => "BEST_EFFORT" },
|
|
58
|
+
NOT_FOUND_RESPONSE,
|
|
59
|
+
),
|
|
60
|
+
).toThrow(/404/);
|
|
61
|
+
|
|
62
|
+
expect(
|
|
63
|
+
throwIfErrorResponse(
|
|
64
|
+
{ ...DEFAULT_OPTIONS, isSuccessResponse: () => "BEST_EFFORT" },
|
|
65
|
+
SUCCESS_RESPONSE,
|
|
66
|
+
),
|
|
67
|
+
).toBeUndefined();
|
|
68
|
+
|
|
69
|
+
expect(() =>
|
|
70
|
+
throwIfErrorResponse(
|
|
71
|
+
{ ...DEFAULT_OPTIONS, isSuccessResponse: () => "THROW" },
|
|
72
|
+
SUCCESS_RESPONSE,
|
|
73
|
+
),
|
|
74
|
+
).toThrow(/isSuccessResponse/);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("isRateLimitError", () => {
|
|
78
|
+
expect(() =>
|
|
79
|
+
throwIfErrorResponse(
|
|
80
|
+
{ ...DEFAULT_OPTIONS, isRateLimitError: () => "RATE_LIMIT" },
|
|
81
|
+
SUCCESS_RESPONSE,
|
|
82
|
+
),
|
|
83
|
+
).toThrow(RestRateLimitError);
|
|
84
|
+
|
|
85
|
+
expect(
|
|
86
|
+
throwIfErrorResponse(
|
|
87
|
+
{ ...DEFAULT_OPTIONS, isRateLimitError: () => "BEST_EFFORT" },
|
|
88
|
+
SUCCESS_RESPONSE,
|
|
89
|
+
),
|
|
90
|
+
).toBeUndefined();
|
|
91
|
+
|
|
92
|
+
expect(() =>
|
|
93
|
+
throwIfErrorResponse(
|
|
94
|
+
{ ...DEFAULT_OPTIONS, isRateLimitError: () => "BEST_EFFORT" },
|
|
95
|
+
TOO_MANY_REQUESTS_RESPONSE,
|
|
96
|
+
),
|
|
97
|
+
).toThrow(RestRateLimitError);
|
|
98
|
+
|
|
99
|
+
expect(
|
|
100
|
+
throwIfErrorResponse(
|
|
101
|
+
{ ...DEFAULT_OPTIONS, isRateLimitError: () => "SOMETHING_ELSE" },
|
|
102
|
+
SUCCESS_RESPONSE,
|
|
103
|
+
),
|
|
104
|
+
).toBeUndefined();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("isTokenInvalidError", () => {
|
|
108
|
+
expect(() =>
|
|
109
|
+
throwIfErrorResponse(
|
|
110
|
+
{ ...DEFAULT_OPTIONS, isTokenInvalidError: () => true },
|
|
111
|
+
SUCCESS_RESPONSE,
|
|
112
|
+
),
|
|
113
|
+
).toThrow(RestTokenInvalidError);
|
|
114
|
+
|
|
115
|
+
expect(
|
|
116
|
+
throwIfErrorResponse(
|
|
117
|
+
{ ...DEFAULT_OPTIONS, isTokenInvalidError: () => false },
|
|
118
|
+
SUCCESS_RESPONSE,
|
|
119
|
+
),
|
|
120
|
+
).toBeUndefined();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("isRetriableError", () => {
|
|
124
|
+
expect(() =>
|
|
125
|
+
throwIfErrorResponse(
|
|
126
|
+
{ ...DEFAULT_OPTIONS, isRetriableError: () => "RETRY" },
|
|
127
|
+
SUCCESS_RESPONSE,
|
|
128
|
+
),
|
|
129
|
+
).toThrow(RestResponseError);
|
|
130
|
+
|
|
131
|
+
expect(
|
|
132
|
+
throwIfErrorResponse(
|
|
133
|
+
{ ...DEFAULT_OPTIONS, isRetriableError: () => "BEST_EFFORT" },
|
|
134
|
+
SUCCESS_RESPONSE,
|
|
135
|
+
),
|
|
136
|
+
).toBeUndefined();
|
|
137
|
+
|
|
138
|
+
expect(() =>
|
|
139
|
+
throwIfErrorResponse(
|
|
140
|
+
{ ...DEFAULT_OPTIONS, isRetriableError: () => "BEST_EFFORT" },
|
|
141
|
+
NOT_FOUND_RESPONSE,
|
|
142
|
+
),
|
|
143
|
+
).toThrow(RestResponseError);
|
|
144
|
+
|
|
145
|
+
expect(
|
|
146
|
+
throwIfErrorResponse(
|
|
147
|
+
{ ...DEFAULT_OPTIONS, isRateLimitError: () => "SOMETHING_ELSE" },
|
|
148
|
+
SUCCESS_RESPONSE,
|
|
149
|
+
),
|
|
150
|
+
).toBeUndefined();
|
|
151
|
+
|
|
152
|
+
expect(() =>
|
|
153
|
+
throwIfErrorResponse(
|
|
154
|
+
{ ...DEFAULT_OPTIONS, isRetriableError: () => "NEVER_RETRY" },
|
|
155
|
+
NOT_FOUND_RESPONSE,
|
|
156
|
+
),
|
|
157
|
+
).toThrow(RestResponseError);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("calcRetryDelay", () => {
|
|
161
|
+
expect(
|
|
162
|
+
calcRetryDelay(
|
|
163
|
+
new RestRateLimitError("", 42, SUCCESS_RESPONSE),
|
|
164
|
+
DEFAULT_OPTIONS,
|
|
165
|
+
SUCCESS_RESPONSE,
|
|
166
|
+
0,
|
|
167
|
+
),
|
|
168
|
+
).toEqual(42);
|
|
169
|
+
|
|
170
|
+
expect(
|
|
171
|
+
calcRetryDelay(
|
|
172
|
+
new RestTokenInvalidError("", ERROR_RESPONSE),
|
|
173
|
+
DEFAULT_OPTIONS,
|
|
174
|
+
ERROR_RESPONSE,
|
|
175
|
+
0,
|
|
176
|
+
),
|
|
177
|
+
).toEqual("no_retry");
|
|
178
|
+
|
|
179
|
+
expect(
|
|
180
|
+
calcRetryDelay(
|
|
181
|
+
new RestTokenInvalidError("", ERROR_RESPONSE),
|
|
182
|
+
{ ...DEFAULT_OPTIONS, isRetriableError: () => "RETRY" },
|
|
183
|
+
ERROR_RESPONSE,
|
|
184
|
+
42,
|
|
185
|
+
),
|
|
186
|
+
).toEqual(42);
|
|
187
|
+
|
|
188
|
+
expect(
|
|
189
|
+
calcRetryDelay(
|
|
190
|
+
new RestResponseError("", ERROR_RESPONSE),
|
|
191
|
+
{ ...DEFAULT_OPTIONS, isRetriableError: () => "BEST_EFFORT" },
|
|
192
|
+
ERROR_RESPONSE,
|
|
193
|
+
42,
|
|
194
|
+
),
|
|
195
|
+
).toEqual(42);
|
|
196
|
+
|
|
197
|
+
expect(
|
|
198
|
+
calcRetryDelay(
|
|
199
|
+
new RestResponseError("", NOT_FOUND_RESPONSE),
|
|
200
|
+
{ ...DEFAULT_OPTIONS, isRetriableError: () => "BEST_EFFORT" },
|
|
201
|
+
NOT_FOUND_RESPONSE,
|
|
202
|
+
42,
|
|
203
|
+
),
|
|
204
|
+
).toEqual("no_retry");
|
|
205
|
+
|
|
206
|
+
expect(
|
|
207
|
+
calcRetryDelay(
|
|
208
|
+
new RestRateLimitError("", 42, NOT_FOUND_RESPONSE),
|
|
209
|
+
{ ...DEFAULT_OPTIONS, isRetriableError: () => "BEST_EFFORT" },
|
|
210
|
+
NOT_FOUND_RESPONSE,
|
|
211
|
+
0,
|
|
212
|
+
),
|
|
213
|
+
).toEqual(42);
|
|
214
|
+
|
|
215
|
+
expect(
|
|
216
|
+
calcRetryDelay(
|
|
217
|
+
new FetchError("test", "some"),
|
|
218
|
+
{
|
|
219
|
+
...DEFAULT_OPTIONS,
|
|
220
|
+
isRetriableError: (_: any, error: any) =>
|
|
221
|
+
error instanceof FetchError ? "NEVER_RETRY" : "RETRY",
|
|
222
|
+
},
|
|
223
|
+
NOT_FOUND_RESPONSE,
|
|
224
|
+
0,
|
|
225
|
+
),
|
|
226
|
+
).toEqual("no_retry");
|
|
227
|
+
|
|
228
|
+
expect(
|
|
229
|
+
calcRetryDelay(
|
|
230
|
+
new FetchError("test", "max-size"),
|
|
231
|
+
{ ...DEFAULT_OPTIONS, isRetriableError: () => "BEST_EFFORT" },
|
|
232
|
+
NOT_FOUND_RESPONSE,
|
|
233
|
+
0,
|
|
234
|
+
),
|
|
235
|
+
).toEqual("no_retry");
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test("when isRateLimitError=YES, isRetriableError=NO doesn't matter, and the request is always retriable", () => {
|
|
239
|
+
expect(() =>
|
|
240
|
+
throwIfErrorResponse(
|
|
241
|
+
{
|
|
242
|
+
...DEFAULT_OPTIONS,
|
|
243
|
+
isRateLimitError: () => "RATE_LIMIT",
|
|
244
|
+
isRetriableError: () => "NEVER_RETRY",
|
|
245
|
+
},
|
|
246
|
+
SUCCESS_RESPONSE,
|
|
247
|
+
),
|
|
248
|
+
).toThrow(RestRateLimitError);
|
|
249
|
+
|
|
250
|
+
expect(
|
|
251
|
+
calcRetryDelay(
|
|
252
|
+
new RestRateLimitError("", 42, SUCCESS_RESPONSE),
|
|
253
|
+
{
|
|
254
|
+
...DEFAULT_OPTIONS,
|
|
255
|
+
isRateLimitError: () => "RATE_LIMIT",
|
|
256
|
+
isRetriableError: () => "NEVER_RETRY",
|
|
257
|
+
},
|
|
258
|
+
SUCCESS_RESPONSE,
|
|
259
|
+
0,
|
|
260
|
+
),
|
|
261
|
+
).toEqual(42);
|
|
262
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import Stream from "stream";
|
|
2
|
+
import {
|
|
3
|
+
consumeReadable,
|
|
4
|
+
createRestStream,
|
|
5
|
+
server,
|
|
6
|
+
serverAssertConnectionsCount,
|
|
7
|
+
} from "./helpers";
|
|
8
|
+
|
|
9
|
+
let ORIGIN: string;
|
|
10
|
+
|
|
11
|
+
beforeAll(async () => {
|
|
12
|
+
ORIGIN = await new Promise((resolve) => {
|
|
13
|
+
server.listen(0, () =>
|
|
14
|
+
resolve("http://127.0.0.1:" + (server.address() as any).port),
|
|
15
|
+
);
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
beforeEach(async () => {
|
|
20
|
+
await serverAssertConnectionsCount(0);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("prefix", async () => {
|
|
24
|
+
const stream = await createRestStream(`${ORIGIN}/large`);
|
|
25
|
+
const prefix = await stream.consumeReturningPrefix(42);
|
|
26
|
+
expect(prefix.length).toEqual(42);
|
|
27
|
+
await serverAssertConnectionsCount(0);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("stream_readable_proxy_closes_connection", async () => {
|
|
31
|
+
const stream = await createRestStream(`${ORIGIN}/large`);
|
|
32
|
+
const readable = Stream.Readable.from(stream);
|
|
33
|
+
expect((await consumeReadable(readable)).length).toBeGreaterThan(1024 * 10);
|
|
34
|
+
await serverAssertConnectionsCount(0);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("read_cancelled_after_reading_preloaded_chunk_closes_connection", async () => {
|
|
38
|
+
const stream = await createRestStream(`${ORIGIN}/large`, 42);
|
|
39
|
+
await serverAssertConnectionsCount(1);
|
|
40
|
+
|
|
41
|
+
for await (const chunk of stream) {
|
|
42
|
+
expect(chunk).toEqual(stream.res.text);
|
|
43
|
+
await serverAssertConnectionsCount(1);
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
await serverAssertConnectionsCount(0);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("read_thrown_after_reading_preloaded_chunk_closes_connection", async () => {
|
|
51
|
+
const stream = await createRestStream(`${ORIGIN}/large`, 42);
|
|
52
|
+
await serverAssertConnectionsCount(1);
|
|
53
|
+
|
|
54
|
+
await expect(async () => {
|
|
55
|
+
for await (const chunk of stream) {
|
|
56
|
+
await serverAssertConnectionsCount(1);
|
|
57
|
+
expect(chunk).toEqual(stream.res.text);
|
|
58
|
+
throw Error("bailout");
|
|
59
|
+
}
|
|
60
|
+
}).rejects.toThrowError("bailout");
|
|
61
|
+
|
|
62
|
+
await serverAssertConnectionsCount(0);
|
|
63
|
+
});
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import http from "http";
|
|
2
|
+
import type { Readable } from "stream";
|
|
3
|
+
import delay from "delay";
|
|
4
|
+
import range from "lodash/range";
|
|
5
|
+
import { Headers } from "node-fetch";
|
|
6
|
+
import { DEFAULT_OPTIONS, RestRequest, RestResponse, RestStream } from "..";
|
|
7
|
+
import type { RestFetchReaderOptions } from "../internal/RestFetchReader";
|
|
8
|
+
import RestFetchReader from "../internal/RestFetchReader";
|
|
9
|
+
|
|
10
|
+
// Two European Euro symbols in UTF-8.
|
|
11
|
+
export const UTF8_BUF = Buffer.from([0xe2, 0x82, 0xac, 0xe2, 0x82, 0xac]);
|
|
12
|
+
|
|
13
|
+
// Binary data, all possible bytes.
|
|
14
|
+
export const BINARY_BUF = Buffer.from(range(256));
|
|
15
|
+
|
|
16
|
+
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
|
17
|
+
export const server = http.createServer(async (req, res) => {
|
|
18
|
+
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
|
19
|
+
|
|
20
|
+
// In node-fetch 2.6.13+, they stopped sending "Connection: close" by default:
|
|
21
|
+
// https://github.com/node-fetch/node-fetch/pull/1736 - which is a good thing:
|
|
22
|
+
// we can now send multiple requests through the same connection sequentially
|
|
23
|
+
// (it seems to be broken before). Since RestFetchReader calls
|
|
24
|
+
// controller.abort() when the client wants to interrupt the request, the
|
|
25
|
+
// connection is physically closed only if there is currently an active
|
|
26
|
+
// request (e.g. "/slow" below), and it's not closed when the request is
|
|
27
|
+
// successfully processed (which is, again, a good thing). So to be able to
|
|
28
|
+
// use serverAssertConnectionsCount() helper, we force the server to close the
|
|
29
|
+
// connection after each request, so serverAssertConnectionsCount() can work.
|
|
30
|
+
res.setHeader("Connection", "close");
|
|
31
|
+
|
|
32
|
+
// Returns a small response.
|
|
33
|
+
if (req.url === "/small") {
|
|
34
|
+
res.writeHead(200);
|
|
35
|
+
return res.end("ok");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Returns a large, but quick response.
|
|
39
|
+
if (req.url === "/large") {
|
|
40
|
+
res.writeHead(200);
|
|
41
|
+
await write(res, 1024);
|
|
42
|
+
await delay(100);
|
|
43
|
+
await write(res, 1024 * 100);
|
|
44
|
+
return res.end("x".repeat(1024 * 200));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Returns a large response, does it slowly.
|
|
48
|
+
if (req.url === "/slow") {
|
|
49
|
+
res.writeHead(200);
|
|
50
|
+
for (let timeStart = Date.now(); Date.now() - timeStart < 20000; ) {
|
|
51
|
+
const error = await write(res, 1024);
|
|
52
|
+
if (error) {
|
|
53
|
+
break;
|
|
54
|
+
} else {
|
|
55
|
+
await delay(10);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return res.end();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Returns slow response as UTF-8 bytes, one after another.
|
|
63
|
+
if (req.url === "/small_slow_utf8") {
|
|
64
|
+
res.writeHead(200);
|
|
65
|
+
for (const byte of UTF8_BUF) {
|
|
66
|
+
await write(res, Buffer.from([byte]));
|
|
67
|
+
await delay(500);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return res.end();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Returns a binary response.
|
|
74
|
+
if (req.url === "/binary") {
|
|
75
|
+
res.setHeader("Content-Type", "application/octet-stream");
|
|
76
|
+
res.writeHead(200);
|
|
77
|
+
for (const _ in range(10)) {
|
|
78
|
+
await write(res, BINARY_BUF);
|
|
79
|
+
await delay(50);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return res.end();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
res.writeHead(404);
|
|
86
|
+
return res.end("not found");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
export async function serverAssertConnectionsCount(expect: number) {
|
|
90
|
+
let got: number = 0;
|
|
91
|
+
for (let timeStart = Date.now(); Date.now() - timeStart < 10000; ) {
|
|
92
|
+
got = await new Promise((resolve) =>
|
|
93
|
+
server.getConnections((_, count) => resolve(count)),
|
|
94
|
+
);
|
|
95
|
+
if (got === expect) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
await delay(50);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
throw Error(`Expected ${expect} connections, but got ${got}`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export class TimeoutError extends Error {}
|
|
106
|
+
|
|
107
|
+
export function createFetchReader(
|
|
108
|
+
url: string,
|
|
109
|
+
options: RestFetchReaderOptions = {},
|
|
110
|
+
) {
|
|
111
|
+
return new RestFetchReader(
|
|
112
|
+
url,
|
|
113
|
+
{},
|
|
114
|
+
{
|
|
115
|
+
onTimeout: () => {
|
|
116
|
+
throw new TimeoutError("timed out");
|
|
117
|
+
},
|
|
118
|
+
...options,
|
|
119
|
+
},
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export async function createRestStream(url: string, preloadBytes?: number) {
|
|
124
|
+
const reader = createFetchReader(url);
|
|
125
|
+
if (preloadBytes) {
|
|
126
|
+
await reader.preload(preloadBytes);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return new RestStream(
|
|
130
|
+
new RestResponse(
|
|
131
|
+
new RestRequest(DEFAULT_OPTIONS, "GET", "dummy", new Headers(), ""),
|
|
132
|
+
reader.agent,
|
|
133
|
+
reader.status,
|
|
134
|
+
reader.headers,
|
|
135
|
+
reader.textFetched.toString(),
|
|
136
|
+
reader.textIsPartial,
|
|
137
|
+
),
|
|
138
|
+
reader[Symbol.asyncIterator](),
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export async function consumeIterable(
|
|
143
|
+
iterable: AsyncIterable<string>,
|
|
144
|
+
delayMs?: number,
|
|
145
|
+
) {
|
|
146
|
+
let rest = "";
|
|
147
|
+
for await (const chunk of iterable) {
|
|
148
|
+
rest += chunk;
|
|
149
|
+
if (delayMs) {
|
|
150
|
+
await delay(delayMs);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return rest;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export async function consumeReadable(stream: Readable) {
|
|
158
|
+
return new Promise<string>((resolve) => {
|
|
159
|
+
const chunks: string[] = [];
|
|
160
|
+
stream.on("data", (chunk) => {
|
|
161
|
+
chunks.push(chunk);
|
|
162
|
+
});
|
|
163
|
+
stream.on("end", () => {
|
|
164
|
+
resolve(chunks.join(""));
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function write(res: http.ServerResponse, bytes: number | Buffer) {
|
|
170
|
+
return new Promise((r) =>
|
|
171
|
+
res.write(typeof bytes === "number" ? "x".repeat(bytes) : bytes, r),
|
|
172
|
+
);
|
|
173
|
+
}
|
|
@@ -2,7 +2,11 @@ import type RestResponse from "../RestResponse";
|
|
|
2
2
|
import RestResponseError from "./RestResponseError";
|
|
3
3
|
|
|
4
4
|
export default class RestRateLimitError extends RestResponseError {
|
|
5
|
-
constructor(
|
|
5
|
+
constructor(
|
|
6
|
+
message: string,
|
|
7
|
+
public delayMs: number,
|
|
8
|
+
res: RestResponse,
|
|
9
|
+
) {
|
|
6
10
|
super(message, res);
|
|
7
11
|
}
|
|
8
12
|
}
|
|
@@ -21,8 +21,8 @@ export default class RestResponseError extends RestError {
|
|
|
21
21
|
`HTTP ${res.status}: ` +
|
|
22
22
|
(message ? `${message}: ` : "") +
|
|
23
23
|
prependNewlineIfMultiline(
|
|
24
|
-
inspectPossibleJSON(res.headers, res.text, RESPONSE_MAX_LEN_IN_ERROR)
|
|
25
|
-
)
|
|
24
|
+
inspectPossibleJSON(res.headers, res.text, RESPONSE_MAX_LEN_IN_ERROR),
|
|
25
|
+
),
|
|
26
26
|
);
|
|
27
27
|
Object.defineProperty(this, "res", { value: res, enumerable: false }); // hidden from inspect()
|
|
28
28
|
const url = new URL(res.req.url);
|
|
@@ -34,7 +34,7 @@ export default class RestResponseError extends RestError {
|
|
|
34
34
|
this.requestBody = inspectPossibleJSON(
|
|
35
35
|
res.headers,
|
|
36
36
|
res.req.body,
|
|
37
|
-
RESPONSE_MAX_LEN_IN_ERROR
|
|
37
|
+
RESPONSE_MAX_LEN_IN_ERROR,
|
|
38
38
|
);
|
|
39
39
|
this.responseHeaders = [...res.headers]
|
|
40
40
|
.map(([name, value]) => `${name}: ${value}`)
|
|
@@ -2,7 +2,11 @@ import type RestResponse from "../RestResponse";
|
|
|
2
2
|
import RestResponseError from "./RestResponseError";
|
|
3
3
|
|
|
4
4
|
export default class RestRetriableError extends RestResponseError {
|
|
5
|
-
constructor(
|
|
5
|
+
constructor(
|
|
6
|
+
message: string,
|
|
7
|
+
public delayMs: number,
|
|
8
|
+
res: RestResponse,
|
|
9
|
+
) {
|
|
6
10
|
super(message, res);
|
|
7
11
|
}
|
|
8
12
|
}
|
|
@@ -2,7 +2,10 @@ import type RestResponse from "../RestResponse";
|
|
|
2
2
|
import RestResponseError from "./RestResponseError";
|
|
3
3
|
|
|
4
4
|
export default class RestTokenInvalidError extends RestResponseError {
|
|
5
|
-
constructor(
|
|
5
|
+
constructor(
|
|
6
|
+
public readonly humanReason: string,
|
|
7
|
+
res: RestResponse,
|
|
8
|
+
) {
|
|
6
9
|
super(humanReason, res);
|
|
7
10
|
}
|
|
8
11
|
}
|