scrapebadger 0.24.1 → 0.25.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/dist/{index-CIZUd1Zr.d.cts → index-ZD3sVCjc.d.cts} +24 -0
- package/dist/{index-CIZUd1Zr.d.ts → index-ZD3sVCjc.d.ts} +24 -0
- package/dist/index.d.cts +28 -2
- package/dist/index.d.ts +28 -2
- package/dist/index.js +96 -3
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +96 -3
- package/dist/index.mjs.map +1 -1
- package/dist/twitter/index.d.cts +1 -1
- package/dist/twitter/index.d.ts +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import { EventEmitter } from 'events';
|
|
|
3
3
|
import WebSocket from 'ws';
|
|
4
4
|
|
|
5
5
|
// src/internal/version.ts
|
|
6
|
-
var SDK_VERSION = "0.24.
|
|
6
|
+
var SDK_VERSION = "0.24.2";
|
|
7
7
|
|
|
8
8
|
// src/internal/exceptions.ts
|
|
9
9
|
var ScrapeBadgerError = class _ScrapeBadgerError extends Error {
|
|
@@ -118,7 +118,8 @@ var WebSocketStreamError = class _WebSocketStreamError extends ScrapeBadgerError
|
|
|
118
118
|
};
|
|
119
119
|
|
|
120
120
|
// src/internal/client.ts
|
|
121
|
-
var
|
|
121
|
+
var RETRYABLE_STATUS_CODES = [500, 502, 503, 504];
|
|
122
|
+
var BaseClient = class _BaseClient {
|
|
122
123
|
config;
|
|
123
124
|
constructor(config) {
|
|
124
125
|
this.config = config;
|
|
@@ -130,6 +131,39 @@ var BaseClient = class {
|
|
|
130
131
|
const { data } = await this.requestRaw(path, options);
|
|
131
132
|
return data;
|
|
132
133
|
}
|
|
134
|
+
/**
|
|
135
|
+
* POST and return the undecoded response body.
|
|
136
|
+
*
|
|
137
|
+
* For endpoints that answer with something other than JSON — currently
|
|
138
|
+
* `/v1/web/scrape` with `raw_content: true`, which returns the scraped body
|
|
139
|
+
* itself. The normal path funnels a non-JSON response into
|
|
140
|
+
* `{ detail: await response.text() }`, which both loses the result and, for
|
|
141
|
+
* a binary payload, corrupts it: `text()` decodes bytes as UTF-8.
|
|
142
|
+
*
|
|
143
|
+
* Returns the raw bytes plus the response headers.
|
|
144
|
+
*/
|
|
145
|
+
async postBinary(path, options = {}) {
|
|
146
|
+
const url = new URL(path, this.config.baseUrl);
|
|
147
|
+
const { body, headers = {} } = options;
|
|
148
|
+
const response = await this.fetchWithTimeout(url.toString(), {
|
|
149
|
+
method: "POST",
|
|
150
|
+
headers: {
|
|
151
|
+
"Content-Type": "application/json",
|
|
152
|
+
"X-API-Key": this.config.apiKey,
|
|
153
|
+
"User-Agent": `scrapebadger-node/${SDK_VERSION}`,
|
|
154
|
+
...headers
|
|
155
|
+
},
|
|
156
|
+
body: body ? JSON.stringify(body) : void 0
|
|
157
|
+
});
|
|
158
|
+
if (!response.ok) {
|
|
159
|
+
await this.handleResponse(response);
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
bytes: new Uint8Array(await response.arrayBuffer()),
|
|
163
|
+
headers: response.headers,
|
|
164
|
+
status: response.status
|
|
165
|
+
};
|
|
166
|
+
}
|
|
133
167
|
/**
|
|
134
168
|
* Make an HTTP request and return both data and rate limit headers.
|
|
135
169
|
*/
|
|
@@ -178,7 +212,7 @@ var BaseClient = class {
|
|
|
178
212
|
return { data, rateLimit };
|
|
179
213
|
} catch (error) {
|
|
180
214
|
lastError = error;
|
|
181
|
-
if (
|
|
215
|
+
if (!_BaseClient.isRetryable(error)) {
|
|
182
216
|
throw error;
|
|
183
217
|
}
|
|
184
218
|
if (attempt === this.config.maxRetries) {
|
|
@@ -305,6 +339,22 @@ var BaseClient = class {
|
|
|
305
339
|
throw new ScrapeBadgerError(message);
|
|
306
340
|
}
|
|
307
341
|
}
|
|
342
|
+
/**
|
|
343
|
+
* Whether a failed request is worth another attempt.
|
|
344
|
+
*
|
|
345
|
+
* Retryable: transient server failures (500/502/503/504), request timeouts,
|
|
346
|
+
* rate limits, and raw network faults thrown by `fetch` itself. Everything
|
|
347
|
+
* else — auth, validation, not-found, conflict — is final.
|
|
348
|
+
*/
|
|
349
|
+
static isRetryable(error) {
|
|
350
|
+
if (error instanceof ServerError) {
|
|
351
|
+
return RETRYABLE_STATUS_CODES.includes(error.statusCode);
|
|
352
|
+
}
|
|
353
|
+
if (error instanceof RateLimitError || error instanceof TimeoutError) {
|
|
354
|
+
return true;
|
|
355
|
+
}
|
|
356
|
+
return !(error instanceof ScrapeBadgerError);
|
|
357
|
+
}
|
|
308
358
|
/**
|
|
309
359
|
* Sleep for a given duration.
|
|
310
360
|
*/
|
|
@@ -2458,11 +2508,54 @@ var WebClient = class {
|
|
|
2458
2508
|
if (options.aiPrompt !== void 0) body.ai_prompt = options.aiPrompt;
|
|
2459
2509
|
if (options.rawContent !== void 0) body.raw_content = options.rawContent;
|
|
2460
2510
|
if (options.skipBotDetection !== void 0) body.skip_bot_detection = options.skipBotDetection;
|
|
2511
|
+
if (options.rawContent) {
|
|
2512
|
+
return this.scrapeRaw(body);
|
|
2513
|
+
}
|
|
2461
2514
|
return this.client.request("/v1/web/scrape", {
|
|
2462
2515
|
method: "POST",
|
|
2463
2516
|
body
|
|
2464
2517
|
});
|
|
2465
2518
|
}
|
|
2519
|
+
/**
|
|
2520
|
+
* Run a `rawContent` scrape, whose response is not JSON.
|
|
2521
|
+
*
|
|
2522
|
+
* The normal path funnels a non-JSON response into `{ detail: text }`, so a
|
|
2523
|
+
* raw scrape returned a result with no content — and for a binary target,
|
|
2524
|
+
* `response.text()` decoded the bytes as UTF-8 and destroyed them. Read the
|
|
2525
|
+
* body as bytes and rebuild the metadata from the `X-Scrape-*` headers the
|
|
2526
|
+
* server sends in this mode.
|
|
2527
|
+
*/
|
|
2528
|
+
async scrapeRaw(body) {
|
|
2529
|
+
const { bytes, headers, status } = await this.client.postBinary("/v1/web/scrape", {
|
|
2530
|
+
body
|
|
2531
|
+
});
|
|
2532
|
+
const int = (name) => {
|
|
2533
|
+
const parsed = Number.parseInt(headers.get(name) ?? "", 10);
|
|
2534
|
+
return Number.isNaN(parsed) ? 0 : parsed;
|
|
2535
|
+
};
|
|
2536
|
+
const mediaType = ((headers.get("content-type") ?? "").split(";")[0] ?? "").trim().toLowerCase();
|
|
2537
|
+
const isText = mediaType.startsWith("text/") || ["application/json", "application/xml", "image/svg+xml"].includes(mediaType);
|
|
2538
|
+
return {
|
|
2539
|
+
success: headers.get("x-scrape-success") !== "0",
|
|
2540
|
+
url: headers.get("x-scrape-url") ?? (typeof body.url === "string" ? body.url : ""),
|
|
2541
|
+
status_code: int("x-scrape-status-code") || status,
|
|
2542
|
+
content: isText ? new TextDecoder().decode(bytes) : null,
|
|
2543
|
+
content_bytes: bytes,
|
|
2544
|
+
content_base64: null,
|
|
2545
|
+
is_binary: !isText,
|
|
2546
|
+
content_type: mediaType || null,
|
|
2547
|
+
format: headers.get("x-scrape-format") ?? "html",
|
|
2548
|
+
engine_used: headers.get("x-scrape-engine") ?? "",
|
|
2549
|
+
credits_used: int("x-credits-used"),
|
|
2550
|
+
duration_ms: int("x-scrape-duration-ms"),
|
|
2551
|
+
retries_used: int("x-scrape-retries"),
|
|
2552
|
+
content_length: int("x-scrape-content-length") || bytes.length,
|
|
2553
|
+
screenshot_url: null,
|
|
2554
|
+
video_url: null,
|
|
2555
|
+
headers: {},
|
|
2556
|
+
blocking_detected: false
|
|
2557
|
+
};
|
|
2558
|
+
}
|
|
2466
2559
|
/**
|
|
2467
2560
|
* Extract structured data from a web page using AI.
|
|
2468
2561
|
*
|