@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 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. Endpoints that support alternate text output, such
108
- as YouTube transcripts, can opt into text mode:
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`, the original
137
- `response` when available, and a parser or transport `cause` when relevant.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crawlora-org/sdk",
3
- "version": "1.2.0-sdk.14",
3
+ "version": "1.2.0-sdk.16",
4
4
  "description": "JavaScript and TypeScript SDK for the public Crawlora API.",
5
5
  "keywords": [
6
6
  "crawlora",
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.14";
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(retryDelay(this.retryDelay, attempt), options.signal);
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
- ...this.headers,
74
- ...authHeaders(operation.security, this.apiKey, this.jwtToken),
75
- ...userAgentHeader(this.userAgent),
76
- ...bodyHeaders,
77
- ...(options.headers || {})
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
- const timeout = timeoutMs > 0 ? setTimeout(() => controller.abort(), timeoutMs) : undefined;
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 parsed = await parseResponse(response, options.responseType || "auto");
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
- async function parseResponse(response, responseType) {
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
@@ -49,8 +49,10 @@ export class CrawloraError extends Error {
49
49
  status: number;
50
50
  code?: number;
51
51
  body: unknown;
52
+ headers: Record<string, string>;
52
53
  response?: Response;
53
54
  cause?: unknown;
55
+ retryable?: boolean;
54
56
  }
55
57
 
56
58
  export class CrawloraClient {