@crawlora-org/sdk 1.2.0-sdk.14 → 1.2.0-sdk.16
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 +16 -0
- package/README.md +11 -5
- package/docs/recipes.md +7 -1
- 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,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v1.2.0-sdk.16
|
|
4
|
+
|
|
5
|
+
- Documented response headers on `CrawloraError`, case-insensitive header
|
|
6
|
+
overrides, strict response modes, `Retry-After` retries, and abort semantics.
|
|
7
|
+
- Added docs coverage checks for the release-polish behavior.
|
|
8
|
+
- Kept the generated operation contract unchanged.
|
|
9
|
+
|
|
10
|
+
## v1.2.0-sdk.15
|
|
11
|
+
|
|
12
|
+
- Added case-insensitive request header overrides across auth, user-agent, and
|
|
13
|
+
content headers.
|
|
14
|
+
- Added strict response mode validation, response headers on SDK errors, and
|
|
15
|
+
`Retry-After` aware retry delays capped at 30 seconds.
|
|
16
|
+
- Clarified timeout and external abort errors without retrying user-aborted
|
|
17
|
+
requests.
|
|
18
|
+
|
|
3
19
|
## v1.2.0-sdk.14
|
|
4
20
|
|
|
5
21
|
- Aligned the promoted SDK beta tag with the Python and Go SDKs.
|
package/README.md
CHANGED
|
@@ -87,7 +87,9 @@ const crawlora = new CrawloraClient({
|
|
|
87
87
|
```
|
|
88
88
|
|
|
89
89
|
Per-request options can override headers, timeout, abort signal, and response
|
|
90
|
-
mode
|
|
90
|
+
mode. Header names are matched case-insensitively, so request headers can
|
|
91
|
+
override default auth, user-agent, and content headers without duplicating
|
|
92
|
+
variants such as `x-api-key` and `X-API-KEY`:
|
|
91
93
|
|
|
92
94
|
```js
|
|
93
95
|
const controller = new AbortController();
|
|
@@ -104,8 +106,9 @@ const response = await crawlora.bing.search(
|
|
|
104
106
|
|
|
105
107
|
## Text Responses
|
|
106
108
|
|
|
107
|
-
Most endpoints return JSON.
|
|
108
|
-
as YouTube transcripts, can
|
|
109
|
+
Most endpoints return JSON. `responseType` must be `auto`, `json`, or `text`.
|
|
110
|
+
Endpoints that support alternate text output, such as YouTube transcripts, can
|
|
111
|
+
opt into text mode:
|
|
109
112
|
|
|
110
113
|
```js
|
|
111
114
|
const transcript = await crawlora.youtube.transcript(
|
|
@@ -133,8 +136,11 @@ try {
|
|
|
133
136
|
}
|
|
134
137
|
```
|
|
135
138
|
|
|
136
|
-
The error includes `status`, optional API `code`, parsed `body`,
|
|
137
|
-
`response` when available, and a parser or transport
|
|
139
|
+
The error includes `status`, optional API `code`, parsed `body`, response
|
|
140
|
+
`headers`, the original `response` when available, and a parser or transport
|
|
141
|
+
`cause` when relevant. Retryable responses honor positive `Retry-After`
|
|
142
|
+
headers, capped at 30 seconds. Externally aborted requests fail with
|
|
143
|
+
`Crawlora request aborted` and are not retried.
|
|
138
144
|
|
|
139
145
|
## Examples
|
|
140
146
|
|
package/docs/recipes.md
CHANGED
|
@@ -47,8 +47,14 @@ const result = await crawlora.bing.search(
|
|
|
47
47
|
);
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
+
Request headers override default auth, user-agent, and content headers
|
|
51
|
+
case-insensitively. Retryable API responses honor positive `Retry-After`
|
|
52
|
+
headers, capped at 30 seconds. Externally aborted requests are not retried.
|
|
53
|
+
|
|
50
54
|
## Text Responses
|
|
51
55
|
|
|
56
|
+
`responseType` must be `auto`, `json`, or `text`.
|
|
57
|
+
|
|
52
58
|
```js
|
|
53
59
|
const text = await crawlora.youtube.transcript(
|
|
54
60
|
{ id: "VIDEO_ID", format: "text" },
|
|
@@ -63,7 +69,7 @@ try {
|
|
|
63
69
|
await crawlora.bing.search({ q: "coffee shops" });
|
|
64
70
|
} catch (error) {
|
|
65
71
|
if (error instanceof CrawloraError) {
|
|
66
|
-
console.log(error.status, error.code, error.body);
|
|
72
|
+
console.log(error.status, error.code, error.body, error.headers);
|
|
67
73
|
}
|
|
68
74
|
}
|
|
69
75
|
```
|
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.16";
|
|
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