@crawlora-org/sdk 1.2.0-sdk.14 → 1.2.0-sdk.15
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/CHANGELOG.md +9 -0
- package/package.json +1 -1
- package/src/client.js +87 -18
- package/src/index.d.ts +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v1.2.0-sdk.15
|
|
4
|
+
|
|
5
|
+
- Added case-insensitive request header overrides across auth, user-agent, and
|
|
6
|
+
content headers.
|
|
7
|
+
- Added strict response mode validation, response headers on SDK errors, and
|
|
8
|
+
`Retry-After` aware retry delays capped at 30 seconds.
|
|
9
|
+
- Clarified timeout and external abort errors without retrying user-aborted
|
|
10
|
+
requests.
|
|
11
|
+
|
|
3
12
|
## v1.2.0-sdk.14
|
|
4
13
|
|
|
5
14
|
- Aligned the promoted SDK beta tag with the Python and Go SDKs.
|
package/package.json
CHANGED
package/src/client.js
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
import { operations, groups } from "./operations.js";
|
|
2
2
|
|
|
3
3
|
const DEFAULT_BASE_URL = "https://api.crawlora.net/api/v1";
|
|
4
|
-
export const VERSION = "1.2.0-sdk.
|
|
4
|
+
export const VERSION = "1.2.0-sdk.15";
|
|
5
5
|
const DEFAULT_USER_AGENT = `crawlora-js-sdk/${VERSION}`;
|
|
6
6
|
|
|
7
7
|
export class CrawloraError extends Error {
|
|
8
|
-
constructor(message, { status = 0, code, body, response, cause } = {}) {
|
|
8
|
+
constructor(message, { status = 0, code, body, headers = {}, response, cause, retryable } = {}) {
|
|
9
9
|
super(message);
|
|
10
10
|
this.name = "CrawloraError";
|
|
11
11
|
this.status = status;
|
|
12
12
|
this.code = code;
|
|
13
13
|
this.body = body;
|
|
14
|
+
this.headers = headers;
|
|
14
15
|
this.response = response;
|
|
15
16
|
this.cause = cause;
|
|
17
|
+
this.retryable = retryable;
|
|
16
18
|
}
|
|
17
19
|
}
|
|
18
20
|
|
|
@@ -52,34 +54,39 @@ export class CrawloraClient {
|
|
|
52
54
|
}
|
|
53
55
|
params = params ?? {};
|
|
54
56
|
options = options ?? {};
|
|
57
|
+
const responseType = validateResponseType(options.responseType || "auto");
|
|
55
58
|
|
|
56
59
|
let attempt = 0;
|
|
57
60
|
for (;;) {
|
|
58
61
|
try {
|
|
59
|
-
return await this.#send(operation, params, options);
|
|
62
|
+
return await this.#send(operation, params, options, responseType);
|
|
60
63
|
} catch (error) {
|
|
61
|
-
if (!(error instanceof CrawloraError) || attempt >= this.retries || !shouldRetry(error.status)) {
|
|
64
|
+
if (!(error instanceof CrawloraError) || error.retryable === false || attempt >= this.retries || !shouldRetry(error.status)) {
|
|
62
65
|
throw error;
|
|
63
66
|
}
|
|
64
67
|
attempt++;
|
|
65
|
-
await sleep(
|
|
68
|
+
await sleep(retryDelayForError(error, this.retryDelay, attempt), options.signal);
|
|
66
69
|
}
|
|
67
70
|
}
|
|
68
71
|
}
|
|
69
72
|
|
|
70
|
-
async #send(operation, params, options) {
|
|
73
|
+
async #send(operation, params, options, responseType) {
|
|
71
74
|
const { url, body, bodyHeaders } = buildRequest(operation, this.baseUrl, params);
|
|
72
|
-
const headers =
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
75
|
+
const headers = mergeHeaders(
|
|
76
|
+
this.headers,
|
|
77
|
+
authHeaders(operation.security, this.apiKey, this.jwtToken),
|
|
78
|
+
userAgentHeader(this.userAgent),
|
|
79
|
+
bodyHeaders,
|
|
80
|
+
options.headers
|
|
81
|
+
);
|
|
79
82
|
|
|
80
83
|
const controller = new AbortController();
|
|
81
84
|
const timeoutMs = options.timeout ?? this.timeout;
|
|
82
|
-
|
|
85
|
+
let timedOut = false;
|
|
86
|
+
const timeout = timeoutMs > 0 ? setTimeout(() => {
|
|
87
|
+
timedOut = true;
|
|
88
|
+
controller.abort();
|
|
89
|
+
}, timeoutMs) : undefined;
|
|
83
90
|
const signal = composeSignal(controller, options.signal);
|
|
84
91
|
let response;
|
|
85
92
|
try {
|
|
@@ -90,12 +97,19 @@ export class CrawloraClient {
|
|
|
90
97
|
signal
|
|
91
98
|
});
|
|
92
99
|
} catch (error) {
|
|
100
|
+
if (options.signal?.aborted) {
|
|
101
|
+
throw new CrawloraError("Crawlora request aborted", { cause: error, retryable: false });
|
|
102
|
+
}
|
|
103
|
+
if (timedOut) {
|
|
104
|
+
throw new CrawloraError("Crawlora request timed out", { cause: error, retryable: false });
|
|
105
|
+
}
|
|
93
106
|
throw new CrawloraError("Crawlora transport error", { cause: error });
|
|
94
107
|
} finally {
|
|
95
108
|
if (timeout) clearTimeout(timeout);
|
|
96
109
|
}
|
|
97
110
|
|
|
98
|
-
const
|
|
111
|
+
const headersObject = responseHeaders(response);
|
|
112
|
+
const parsed = await parseResponse(response, responseType, headersObject);
|
|
99
113
|
if (!response.ok) {
|
|
100
114
|
const code = parsed && typeof parsed === "object" ? parsed.code : undefined;
|
|
101
115
|
const message = parsed && typeof parsed === "object" && parsed.msg ? parsed.msg : response.statusText;
|
|
@@ -103,6 +117,7 @@ export class CrawloraClient {
|
|
|
103
117
|
status: response.status,
|
|
104
118
|
code,
|
|
105
119
|
body: parsed,
|
|
120
|
+
headers: headersObject,
|
|
106
121
|
response
|
|
107
122
|
});
|
|
108
123
|
}
|
|
@@ -203,6 +218,22 @@ function userAgentHeader(userAgent) {
|
|
|
203
218
|
return { "user-agent": userAgent };
|
|
204
219
|
}
|
|
205
220
|
|
|
221
|
+
function mergeHeaders(...sources) {
|
|
222
|
+
const headers = {};
|
|
223
|
+
const names = new Map();
|
|
224
|
+
for (const source of sources) {
|
|
225
|
+
for (const [name, value] of Object.entries(source || {})) {
|
|
226
|
+
if (value === undefined || value === null) continue;
|
|
227
|
+
const lower = name.toLowerCase();
|
|
228
|
+
const existing = names.get(lower);
|
|
229
|
+
if (existing && existing !== name) delete headers[existing];
|
|
230
|
+
headers[name] = String(value);
|
|
231
|
+
names.set(lower, name);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return headers;
|
|
235
|
+
}
|
|
236
|
+
|
|
206
237
|
function composeSignal(controller, signal) {
|
|
207
238
|
if (!signal) return controller.signal;
|
|
208
239
|
if (signal.aborted) controller.abort();
|
|
@@ -210,7 +241,14 @@ function composeSignal(controller, signal) {
|
|
|
210
241
|
return controller.signal;
|
|
211
242
|
}
|
|
212
243
|
|
|
213
|
-
|
|
244
|
+
function validateResponseType(responseType) {
|
|
245
|
+
if (responseType === "auto" || responseType === "json" || responseType === "text") {
|
|
246
|
+
return responseType;
|
|
247
|
+
}
|
|
248
|
+
throw new TypeError("Invalid responseType: expected one of auto, json, text");
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async function parseResponse(response, responseType, headers) {
|
|
214
252
|
if (responseType === "text") return response.text();
|
|
215
253
|
const contentType = response.headers.get("content-type") || "";
|
|
216
254
|
if (responseType === "json" || contentType.toLowerCase().includes("application/json")) {
|
|
@@ -221,6 +259,7 @@ async function parseResponse(response, responseType) {
|
|
|
221
259
|
throw new CrawloraError("Crawlora JSON parse error", {
|
|
222
260
|
status: response.status,
|
|
223
261
|
body: text,
|
|
262
|
+
headers,
|
|
224
263
|
response,
|
|
225
264
|
cause: error
|
|
226
265
|
});
|
|
@@ -240,6 +279,36 @@ function retryDelay(baseDelay, attempt) {
|
|
|
240
279
|
return delay + jitter;
|
|
241
280
|
}
|
|
242
281
|
|
|
282
|
+
function retryDelayForError(error, baseDelay, attempt) {
|
|
283
|
+
return parseRetryAfter(error.headers) ?? retryDelay(baseDelay, attempt);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function parseRetryAfter(headers) {
|
|
287
|
+
const value = headerValue(headers, "retry-after");
|
|
288
|
+
if (!value) return undefined;
|
|
289
|
+
const seconds = Number(value);
|
|
290
|
+
if (Number.isFinite(seconds) && seconds > 0) {
|
|
291
|
+
return Math.min(seconds * 1000, 30000);
|
|
292
|
+
}
|
|
293
|
+
const date = Date.parse(value);
|
|
294
|
+
if (Number.isFinite(date)) {
|
|
295
|
+
const delay = date - Date.now();
|
|
296
|
+
if (delay > 0) return Math.min(delay, 30000);
|
|
297
|
+
}
|
|
298
|
+
return undefined;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function headerValue(headers, name) {
|
|
302
|
+
for (const [key, value] of Object.entries(headers || {})) {
|
|
303
|
+
if (key.toLowerCase() === name.toLowerCase()) return value;
|
|
304
|
+
}
|
|
305
|
+
return "";
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function responseHeaders(response) {
|
|
309
|
+
return Object.fromEntries(response.headers.entries());
|
|
310
|
+
}
|
|
311
|
+
|
|
243
312
|
function normalizeNonNegativeInteger(value) {
|
|
244
313
|
const number = Number(value);
|
|
245
314
|
if (!Number.isFinite(number) || number <= 0) return 0;
|
|
@@ -256,13 +325,13 @@ function sleep(ms, signal) {
|
|
|
256
325
|
if (!ms || ms <= 0) return Promise.resolve();
|
|
257
326
|
return new Promise((resolve, reject) => {
|
|
258
327
|
if (signal?.aborted) {
|
|
259
|
-
reject(new CrawloraError("Crawlora request aborted", { cause: signal.reason }));
|
|
328
|
+
reject(new CrawloraError("Crawlora request aborted", { cause: signal.reason, retryable: false }));
|
|
260
329
|
return;
|
|
261
330
|
}
|
|
262
331
|
const timer = setTimeout(resolve, ms);
|
|
263
332
|
signal?.addEventListener("abort", () => {
|
|
264
333
|
clearTimeout(timer);
|
|
265
|
-
reject(new CrawloraError("Crawlora request aborted", { cause: signal.reason }));
|
|
334
|
+
reject(new CrawloraError("Crawlora request aborted", { cause: signal.reason, retryable: false }));
|
|
266
335
|
}, { once: true });
|
|
267
336
|
});
|
|
268
337
|
}
|
package/src/index.d.ts
CHANGED