@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 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
@@ -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.15",
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.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(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 {