@mendable/firecrawl-js 4.38.0 → 4.39.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.
Files changed (32) hide show
  1. package/README.md +7 -2
  2. package/dist/{chunk-KSTNOGOD.js → chunk-4DPCCKWG.js} +1 -1
  3. package/dist/index.cjs +39 -9
  4. package/dist/index.d.cts +152 -4
  5. package/dist/index.d.ts +152 -4
  6. package/dist/index.js +40 -10
  7. package/dist/{package-OS7RONXY.js → package-D2QZMRW7.js} +1 -1
  8. package/package.json +1 -1
  9. package/pnpm-workspace.yaml +1 -0
  10. package/src/__tests__/e2e/v2/batch.test.ts +13 -11
  11. package/src/__tests__/e2e/v2/crawl.test.ts +24 -20
  12. package/src/__tests__/e2e/v2/extract.test.ts +32 -7
  13. package/src/__tests__/e2e/v2/map.test.ts +4 -3
  14. package/src/__tests__/e2e/v2/parse.test.ts +4 -3
  15. package/src/__tests__/e2e/v2/scrape.test.ts +10 -9
  16. package/src/__tests__/e2e/v2/search.test.ts +9 -8
  17. package/src/__tests__/e2e/v2/usage.test.ts +6 -5
  18. package/src/__tests__/e2e/v2/utils/rateLimit.ts +257 -0
  19. package/src/__tests__/e2e/v2/watcher.test.ts +4 -3
  20. package/src/__tests__/unit/v2/agent-effort-trace.unit.test.ts +1 -1
  21. package/src/__tests__/unit/v2/agent-thread.unit.test.ts +164 -0
  22. package/src/__tests__/unit/v2/e2eRateLimitRetry.test.ts +501 -0
  23. package/src/__tests__/unit/v2/extract.unit.test.ts +1 -1
  24. package/src/__tests__/unit/v2/scrapeAutoResume.test.ts +1 -1
  25. package/src/__tests__/unit/v2/search-country.unit.test.ts +32 -0
  26. package/src/__tests__/unit/v2/search-data-property.test.ts +1 -0
  27. package/src/__tests__/unit/v2/v1-aliases.test.ts +1 -0
  28. package/src/v2/client.ts +13 -1
  29. package/src/v2/methods/agent.ts +23 -1
  30. package/src/v2/methods/research.ts +14 -8
  31. package/src/v2/methods/search.ts +1 -0
  32. package/src/v2/types.ts +151 -34
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  require_package
3
- } from "./chunk-KSTNOGOD.js";
3
+ } from "./chunk-4DPCCKWG.js";
4
4
 
5
5
  // src/v2/utils/httpClient.ts
6
6
  import axios from "axios";
@@ -620,6 +620,7 @@ function prepareSearchPayload(req) {
620
620
  if (req.limit != null) payload.limit = req.limit;
621
621
  if (req.tbs != null) payload.tbs = req.tbs;
622
622
  if (req.location != null) payload.location = req.location;
623
+ if (req.country != null) payload.country = req.country;
623
624
  if (req.ignoreInvalidURLs != null)
624
625
  payload.ignoreInvalidURLs = req.ignoreInvalidURLs;
625
626
  if (req.timeout != null) payload.timeout = req.timeout;
@@ -1240,6 +1241,9 @@ function prepareAgentPayload(args) {
1240
1241
  body.threatProtection = args.threatProtection;
1241
1242
  if (args.auditMetadata != null)
1242
1243
  body.auditMetadata = args.auditMetadata;
1244
+ if (args.threadId != null) body.threadId = args.threadId;
1245
+ if (args.mode != null) body.mode = args.mode;
1246
+ if (args.exchange != null) body.exchange = args.exchange;
1243
1247
  return body;
1244
1248
  }
1245
1249
  async function startAgent(http, args) {
@@ -1310,6 +1314,17 @@ async function getAgentSnapshot(http, jobId, snapshotId) {
1310
1314
  throw err;
1311
1315
  }
1312
1316
  }
1317
+ async function getAgentThread(http, threadId, options) {
1318
+ try {
1319
+ const query = options?.includeData ? "?includeData=true" : "";
1320
+ const res = await http.get(`/v2/agent/threads/${threadId}${query}`);
1321
+ if (res.status !== 200) throwForBadResponse(res, "agent thread");
1322
+ return res.data;
1323
+ } catch (err) {
1324
+ if (err?.isAxiosError) return normalizeAxiosError(err, "agent thread");
1325
+ throw err;
1326
+ }
1327
+ }
1313
1328
  async function cancelAgent(http, jobId) {
1314
1329
  try {
1315
1330
  const res = await http.delete(`/v2/agent/${jobId}`);
@@ -1550,9 +1565,7 @@ var ResearchClient = class {
1550
1565
  appendParam(params, "query", options.query);
1551
1566
  appendParam(params, "k", options.k);
1552
1567
  try {
1553
- const res = await this.http.get(
1554
- withQuery(`${BASE}/papers/${encodeURIComponent(id)}`, params)
1555
- );
1568
+ const res = await this.http.get(withQuery(`${BASE}/papers/${encodeURIComponent(id)}`, params));
1556
1569
  if (res.status !== 200) throwForBadResponse(res, "get paper");
1557
1570
  return res.data;
1558
1571
  } catch (err) {
@@ -1578,10 +1591,7 @@ var ResearchClient = class {
1578
1591
  appendParam(params, "anchor", options.anchor);
1579
1592
  try {
1580
1593
  const res = await this.http.get(
1581
- withQuery(
1582
- `${BASE}/papers/${encodeURIComponent(id)}/similar`,
1583
- params
1584
- )
1594
+ withQuery(`${BASE}/papers/${encodeURIComponent(id)}/similar`, params)
1585
1595
  );
1586
1596
  if (res.status !== 200) throwForBadResponse(res, "find similar papers");
1587
1597
  return res.data;
@@ -1590,7 +1600,16 @@ var ResearchClient = class {
1590
1600
  }
1591
1601
  }
1592
1602
  /**
1593
- * Search GitHub issue/PR history and repository readmes.
1603
+ * Search the research index GitHub slice: issue/PR history and repository
1604
+ * readmes.
1605
+ *
1606
+ * @deprecated Use `developerSearch()` on the client instead. This
1607
+ * endpoint stops responding after 2026-11-03. The developer index searches
1608
+ * GitHub issues, pull requests and readmes plus curated documentation
1609
+ * sources, returns matched passages, and adds filters for repo, language,
1610
+ * license and stars. It does not carry over the `scores` breakdown or the
1611
+ * `resultType: "web"` fallback results. See
1612
+ * https://docs.firecrawl.dev/features/developer.
1594
1613
  * @param query Search query.
1595
1614
  * @param options Optional `k`.
1596
1615
  */
@@ -2079,6 +2098,9 @@ var FirecrawlClient = class {
2079
2098
  * Access the v2 research endpoints — Firecrawl's **research paper index**
2080
2099
  * (~43M paper abstracts) plus GitHub history/readmes.
2081
2100
  *
2101
+ * `research.searchGithub()` is deprecated and stops responding after
2102
+ * 2026-11-03. Use `developerSearch()` instead.
2103
+ *
2082
2104
  * The paper corpus is roughly 90% biomedical and life sciences — PubMed,
2083
2105
  * bioRxiv and medRxiv — with arXiv covering physics, mathematics and
2084
2106
  * computer science.
@@ -2338,6 +2360,14 @@ var FirecrawlClient = class {
2338
2360
  async getAgentSnapshot(jobId, snapshotId) {
2339
2361
  return getAgentSnapshot(this.http, jobId, snapshotId);
2340
2362
  }
2363
+ /**
2364
+ * Get a thread and its runs, oldest turn first.
2365
+ * @param threadId Thread id, as returned by startAgent or getAgentStatus.
2366
+ * @param options.includeData Inline each succeeded run's data.
2367
+ */
2368
+ async getAgentThread(threadId, options) {
2369
+ return getAgentThread(this.http, threadId, options);
2370
+ }
2341
2371
  // Browser
2342
2372
  /**
2343
2373
  * Create a new browser session.
@@ -2480,7 +2510,7 @@ var FirecrawlApp = class {
2480
2510
  if (typeof process !== "undefined" && process.env && process.env.npm_package_version) {
2481
2511
  return process.env.npm_package_version;
2482
2512
  }
2483
- const packageJson = await import("./package-OS7RONXY.js");
2513
+ const packageJson = await import("./package-D2QZMRW7.js");
2484
2514
  return packageJson.default.version;
2485
2515
  } catch (error) {
2486
2516
  const isTest = typeof process !== "undefined" && (process.env.JEST_WORKER_ID != null || false);
@@ -1,4 +1,4 @@
1
1
  import {
2
2
  require_package
3
- } from "./chunk-KSTNOGOD.js";
3
+ } from "./chunk-4DPCCKWG.js";
4
4
  export default require_package();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mendable/firecrawl-js",
3
- "version": "4.38.0",
3
+ "version": "4.39.0",
4
4
  "description": "JavaScript SDK for the Firecrawl API: web scraping, crawling, web search, and scientific literature search over a research paper index of PubMed, bioRxiv, medRxiv and arXiv abstracts",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -15,3 +15,4 @@ overrides:
15
15
  "form-data@>=4.0.0 <4.0.6": "4.0.6"
16
16
  "@babel/core@>=7.0.0 <7.29.6": "7.29.7"
17
17
  "esbuild@>=0.17.0 <0.28.1": "0.28.1"
18
+ "browserslist@<=4.28.6": ">=4.28.7 <5.0.0"
@@ -4,6 +4,7 @@
4
4
  import Firecrawl from "../../../index";
5
5
  import { config } from "dotenv";
6
6
  import { getIdentity, getApiUrl } from "./utils/idmux";
7
+ import { testTimeoutMs, waitForJob, withRateLimitRetry } from "./utils/rateLimit";
7
8
  import { describe, test, expect, beforeAll } from "@jest/globals";
8
9
 
9
10
  config();
@@ -13,7 +14,7 @@ let client: Firecrawl;
13
14
 
14
15
  beforeAll(async () => {
15
16
  const { apiKey } = await getIdentity({ name: "js-e2e-batch" });
16
- client = new Firecrawl({ apiKey, apiUrl: API_URL });
17
+ client = withRateLimitRetry(new Firecrawl({ apiKey, apiUrl: API_URL }));
17
18
  });
18
19
 
19
20
  describe("v2.batch e2e", () => {
@@ -22,19 +23,21 @@ describe("v2.batch e2e", () => {
22
23
  "https://docs.firecrawl.dev",
23
24
  "https://firecrawl.dev",
24
25
  ];
25
- const job = await client.batchScrape(urls, { options: { formats: ["markdown"] }, pollInterval: 1, timeout: 180 });
26
+ const start = await client.startBatchScrape(urls, { options: { formats: ["markdown"] } });
27
+ const job = await waitForJob(() => client.getBatchScrapeStatus(start.id), { pollInterval: 1, timeout: 210 });
26
28
  expect(["completed", "failed"]).toContain(job.status);
27
29
  expect(job.completed).toBeGreaterThanOrEqual(0);
28
30
  expect(job.total).toBeGreaterThanOrEqual(0);
29
31
  expect(Array.isArray(job.data)).toBe(true);
30
- }, 240_000);
32
+ }, testTimeoutMs(362_000));
31
33
 
32
34
  test("batch scrape with wait returns job id for error retrieval", async () => {
33
35
  const urls = [
34
36
  "https://docs.firecrawl.dev",
35
37
  "https://firecrawl.dev",
36
38
  ];
37
- const job = await client.batchScrape(urls, { options: { formats: ["markdown"] }, pollInterval: 1, timeout: 180 });
39
+ const start = await client.startBatchScrape(urls, { options: { formats: ["markdown"] } });
40
+ const job = await waitForJob(() => client.getBatchScrapeStatus(start.id), { pollInterval: 1, timeout: 210 });
38
41
  // Verify job has id field
39
42
  expect(job.id).toBeDefined();
40
43
  expect(typeof job.id).toBe("string");
@@ -44,7 +47,7 @@ describe("v2.batch e2e", () => {
44
47
  expect(errors).toHaveProperty("robotsBlocked");
45
48
  expect(Array.isArray(errors.errors)).toBe(true);
46
49
  expect(Array.isArray(errors.robotsBlocked)).toBe(true);
47
- }, 240_000);
50
+ }, testTimeoutMs(512_000));
48
51
 
49
52
  test("start batch minimal and status", async () => {
50
53
  const urls = ["https://docs.firecrawl.dev", "https://firecrawl.dev"];
@@ -57,11 +60,11 @@ describe("v2.batch e2e", () => {
57
60
  // Verify status includes id field
58
61
  expect(status.id).toBeDefined();
59
62
  expect(status.id).toBe(start.id);
60
- }, 120_000);
63
+ }, testTimeoutMs(151_000));
61
64
 
62
65
  test("wait batch with all params", async () => {
63
66
  const urls = ["https://docs.firecrawl.dev", "https://firecrawl.dev"];
64
- const job = await client.batchScrape(urls, {
67
+ const start = await client.startBatchScrape(urls, {
65
68
  options: {
66
69
  formats: [
67
70
  "markdown",
@@ -74,14 +77,13 @@ describe("v2.batch e2e", () => {
74
77
  ignoreInvalidURLs: true,
75
78
  maxConcurrency: 2,
76
79
  zeroDataRetention: false,
77
- pollInterval: 1,
78
- timeout: 180,
79
80
  });
81
+ const job = await waitForJob(() => client.getBatchScrapeStatus(start.id), { pollInterval: 1, timeout: 210 });
80
82
  expect(["completed", "failed", "cancelled"]).toContain(job.status);
81
83
  expect(job.completed).toBeGreaterThanOrEqual(0);
82
84
  expect(job.total).toBeGreaterThanOrEqual(0);
83
85
  expect(Array.isArray(job.data)).toBe(true);
84
- }, 300_000);
86
+ }, testTimeoutMs(362_000));
85
87
 
86
88
  test("cancel batch", async () => {
87
89
  const urls = ["https://docs.firecrawl.dev", "https://firecrawl.dev"];
@@ -89,6 +91,6 @@ describe("v2.batch e2e", () => {
89
91
  expect(typeof start.id).toBe("string");
90
92
  const cancelled = await client.cancelBatchScrape(start.id);
91
93
  expect(cancelled).toBe(true);
92
- }, 120_000);
94
+ }, testTimeoutMs(151_000));
93
95
  });
94
96
 
@@ -4,6 +4,7 @@
4
4
  import Firecrawl from "../../../index";
5
5
  import { config } from "dotenv";
6
6
  import { getIdentity, getApiUrl } from "./utils/idmux";
7
+ import { testTimeoutMs, waitForJob, withRateLimitRetry } from "./utils/rateLimit";
7
8
  import { describe, test, expect, beforeAll } from "@jest/globals";
8
9
 
9
10
  config();
@@ -13,7 +14,7 @@ let client: Firecrawl;
13
14
 
14
15
  beforeAll(async () => {
15
16
  const { apiKey } = await getIdentity({ name: "js-e2e-crawl" });
16
- client = new Firecrawl({ apiKey, apiUrl: API_URL });
17
+ client = withRateLimitRetry(new Firecrawl({ apiKey, apiUrl: API_URL }));
17
18
  });
18
19
 
19
20
  describe("v2.crawl e2e", () => {
@@ -23,21 +24,21 @@ describe("v2.crawl e2e", () => {
23
24
  const job = await client.startCrawl("https://docs.firecrawl.dev", { limit: 3 });
24
25
  expect(typeof job.id).toBe("string");
25
26
  expect(typeof job.url).toBe("string");
26
- }, 90_000);
27
+ }, testTimeoutMs(90_000));
27
28
 
28
29
  test("start crawl with options", async () => {
29
30
  if (!client) throw new Error();
30
31
  const job = await client.startCrawl("https://docs.firecrawl.dev", { limit: 5, maxDiscoveryDepth: 2 });
31
32
  expect(typeof job.id).toBe("string");
32
33
  expect(typeof job.url).toBe("string");
33
- }, 90_000);
34
+ }, testTimeoutMs(90_000));
34
35
 
35
36
  test("start crawl with prompt", async () => {
36
37
  if (!client) throw new Error();
37
38
  const job = await client.startCrawl("https://firecrawl.dev", { prompt: "Extract all blog posts", limit: 3 });
38
39
  expect(typeof job.id).toBe("string");
39
40
  expect(typeof job.url).toBe("string");
40
- }, 90_000);
41
+ }, testTimeoutMs(90_000));
41
42
 
42
43
  test("get crawl status", async () => {
43
44
  if (!client) throw new Error();
@@ -50,14 +51,14 @@ describe("v2.crawl e2e", () => {
50
51
  expect(status.id).toBe(start.id);
51
52
  // next/expiresAt may be null/undefined depending on state; check shape
52
53
  expect(Array.isArray(status.data)).toBe(true);
53
- }, 120_000);
54
+ }, testTimeoutMs(151_000));
54
55
 
55
56
  test("cancel crawl", async () => {
56
57
  if (!client) throw new Error();
57
58
  const start = await client.startCrawl("https://docs.firecrawl.dev", { limit: 3 });
58
59
  const ok = await client.cancelCrawl(start.id);
59
60
  expect(ok).toBe(true);
60
- }, 120_000);
61
+ }, testTimeoutMs(151_000));
61
62
 
62
63
  test("get crawl errors", async () => {
63
64
  if (!client) throw new Error();
@@ -73,12 +74,12 @@ describe("v2.crawl e2e", () => {
73
74
  expect(typeof e.url === "string" || e.url == null).toBe(true);
74
75
  expect(typeof e.error === "string" || e.error == null).toBe(true);
75
76
  }
76
- }, 120_000);
77
+ }, testTimeoutMs(151_000));
77
78
 
78
79
  test("get crawl errors with invalid id should throw", async () => {
79
80
  if (!client) throw new Error();
80
81
  await expect(client.getCrawlErrors("invalid-job-id-12345")).rejects.toThrow();
81
- }, 60_000);
82
+ }, testTimeoutMs(60_000));
82
83
 
83
84
  test("get active crawls", async () => {
84
85
  if (!client) throw new Error();
@@ -93,7 +94,7 @@ describe("v2.crawl e2e", () => {
93
94
  expect(typeof c.options === "object").toBe(true);
94
95
  }
95
96
  }
96
- }, 90_000);
97
+ }, testTimeoutMs(90_000));
97
98
 
98
99
  test("get active crawls with running crawl", async () => {
99
100
  if (!client) throw new Error();
@@ -104,20 +105,22 @@ describe("v2.crawl e2e", () => {
104
105
  const ids = active.crawls.map(c => c.id);
105
106
  expect(ids.includes(start.id)).toBe(true);
106
107
  await client.cancelCrawl(start.id);
107
- }, 120_000);
108
+ }, testTimeoutMs(301_000));
108
109
 
109
110
  test("crawl with wait", async () => {
110
111
  if (!client) throw new Error();
111
- const job = await client.crawl("https://docs.firecrawl.dev", { limit: 3, maxDiscoveryDepth: 2, pollInterval: 1, timeout: 120 });
112
+ const start = await client.startCrawl("https://docs.firecrawl.dev", { limit: 3, maxDiscoveryDepth: 2 });
113
+ const job = await waitForJob(() => client.getCrawlStatus(start.id), { pollInterval: 1, timeout: 120 });
112
114
  expect(["completed", "failed"]).toContain(job.status);
113
115
  expect(job.completed).toBeGreaterThanOrEqual(0);
114
116
  expect(job.total).toBeGreaterThanOrEqual(0);
115
117
  expect(Array.isArray(job.data)).toBe(true);
116
- }, 180_000);
118
+ }, testTimeoutMs(272_000));
117
119
 
118
120
  test("crawl with wait returns job id for error retrieval", async () => {
119
121
  if (!client) throw new Error();
120
- const job = await client.crawl("https://docs.firecrawl.dev", { limit: 3, maxDiscoveryDepth: 2, pollInterval: 1, timeout: 120 });
122
+ const start = await client.startCrawl("https://docs.firecrawl.dev", { limit: 3, maxDiscoveryDepth: 2 });
123
+ const job = await waitForJob(() => client.getCrawlStatus(start.id), { pollInterval: 1, timeout: 120 });
121
124
  // Verify job has id field
122
125
  expect(job.id).toBeDefined();
123
126
  expect(typeof job.id).toBe("string");
@@ -127,16 +130,17 @@ describe("v2.crawl e2e", () => {
127
130
  expect(errors).toHaveProperty("robotsBlocked");
128
131
  expect(Array.isArray(errors.errors)).toBe(true);
129
132
  expect(Array.isArray(errors.robotsBlocked)).toBe(true);
130
- }, 180_000);
133
+ }, testTimeoutMs(422_000));
131
134
 
132
135
  test("crawl with prompt and wait", async () => {
133
136
  if (!client) throw new Error();
134
- const job = await client.crawl("https://docs.firecrawl.dev", { prompt: "Extract all blog posts", limit: 3, pollInterval: 1, timeout: 120 });
137
+ const start = await client.startCrawl("https://docs.firecrawl.dev", { prompt: "Extract all blog posts", limit: 3 });
138
+ const job = await waitForJob(() => client.getCrawlStatus(start.id), { pollInterval: 1, timeout: 120 });
135
139
  expect(["completed", "failed"]).toContain(job.status);
136
140
  expect(job.completed).toBeGreaterThanOrEqual(0);
137
141
  expect(job.total).toBeGreaterThanOrEqual(0);
138
142
  expect(Array.isArray(job.data)).toBe(true);
139
- }, 180_000);
143
+ }, testTimeoutMs(272_000));
140
144
 
141
145
  test("crawl with scrape options", async () => {
142
146
  if (!client) throw new Error();
@@ -145,7 +149,7 @@ describe("v2.crawl e2e", () => {
145
149
  scrapeOptions: { formats: ["markdown", "links"], onlyMainContent: false, mobile: true },
146
150
  });
147
151
  expect(typeof job.id).toBe("string");
148
- }, 120_000);
152
+ }, testTimeoutMs(120_000));
149
153
 
150
154
  test("crawl with json format object", async () => {
151
155
  if (!client) throw new Error();
@@ -154,7 +158,7 @@ describe("v2.crawl e2e", () => {
154
158
  scrapeOptions: { formats: [{ type: "json", prompt: "Extract page title", schema: { type: "object", properties: { title: { type: "string" } }, required: ["title"] } }] },
155
159
  });
156
160
  expect(typeof job.id).toBe("string");
157
- }, 120_000);
161
+ }, testTimeoutMs(120_000));
158
162
 
159
163
  test("crawl all parameters", async () => {
160
164
  if (!client) throw new Error();
@@ -187,13 +191,13 @@ describe("v2.crawl e2e", () => {
187
191
  zeroDataRetention: false,
188
192
  });
189
193
  expect(typeof job.id).toBe("string");
190
- }, 180_000);
194
+ }, testTimeoutMs(180_000));
191
195
 
192
196
  test("crawl params preview", async () => {
193
197
  if (!client) throw new Error();
194
198
  const params = await client.crawlParamsPreview("https://docs.firecrawl.dev", "Extract all blog posts and documentation");
195
199
  expect(params && typeof params === "object").toBe(true);
196
200
  // Optional fields may or may not be present; just assert object shape
197
- }, 60_000);
201
+ }, testTimeoutMs(60_000));
198
202
  });
199
203
 
@@ -4,6 +4,7 @@
4
4
  import Firecrawl from "../../../index";
5
5
  import { config } from "dotenv";
6
6
  import { getIdentity, getApiUrl } from "./utils/idmux";
7
+ import { testTimeoutMs, waitForJob, withRateLimitRetry } from "./utils/rateLimit";
7
8
  import { describe, test, expect, beforeAll } from "@jest/globals";
8
9
  import { z } from "zod";
9
10
 
@@ -14,14 +15,38 @@ let client: Firecrawl;
14
15
 
15
16
  beforeAll(async () => {
16
17
  const { apiKey } = await getIdentity({ name: "js-e2e-extract" });
17
- client = new Firecrawl({ apiKey, apiUrl: API_URL });
18
+ client = withRateLimitRetry(new Firecrawl({ apiKey, apiUrl: API_URL }));
18
19
  });
19
20
 
20
21
  describe("v2.extract e2e", () => {
22
+ /**
23
+ * Starts an extract, then polls it. Mirrors client.extract, but keeps every
24
+ * request retryable: the wrapper cannot run client.extract again, because a
25
+ * second run would start a second job.
26
+ *
27
+ * The bound is 180s, the time an extract needs, and the same bound batch
28
+ * uses.
29
+ *
30
+ * The bound is not the whole cost of the test. startExtract goes through
31
+ * the wrapper too, so it can spend the retry budget before the poll starts:
32
+ * 150s for the start, plus 180 + 2 + 150 = 332s for the poll, so 482s. Each
33
+ * test below therefore carries a 333_000 ms base, which gives 483s. The
34
+ * unit test "every wrapped call in a test fits its budget" holds that sum.
35
+ */
36
+ async function extractAndWait(
37
+ args: Parameters<typeof client.startExtract>[0],
38
+ ) {
39
+ const started = await client.startExtract(args);
40
+ if (!started.id) return started;
41
+ return waitForJob(() => client.getExtractStatus(started.id!), {
42
+ timeout: 180,
43
+ });
44
+ }
45
+
21
46
  test("extract minimal with prompt", async () => {
22
- const resp = await client.extract({ urls: ["https://docs.firecrawl.dev"], prompt: "Extract the main page title" });
47
+ const resp = await extractAndWait({ urls: ["https://docs.firecrawl.dev"], prompt: "Extract the main page title" });
23
48
  expect(typeof resp.success === "boolean" || resp.success == null).toBe(true);
24
- }, 120_000);
49
+ }, testTimeoutMs(333_000));
25
50
 
26
51
  test("extract with schema", async () => {
27
52
  const schema = {
@@ -29,7 +54,7 @@ describe("v2.extract e2e", () => {
29
54
  properties: { title: { type: "string" } },
30
55
  required: ["title"],
31
56
  } as const;
32
- const resp = await client.extract({
57
+ const resp = await extractAndWait({
33
58
  urls: ["https://docs.firecrawl.dev"],
34
59
  schema,
35
60
  prompt: "Extract the main page title",
@@ -44,13 +69,13 @@ describe("v2.extract e2e", () => {
44
69
  expect(typeof resp.data).toBe("object");
45
70
  expect((resp.data as any).title).toBeTruthy();
46
71
  }
47
- }, 180_000);
72
+ }, testTimeoutMs(333_000));
48
73
 
49
74
  test("extract with zod schema", async () => {
50
75
  const schema = z.object({
51
76
  title: z.string(),
52
77
  });
53
- const resp = await client.extract({
78
+ const resp = await extractAndWait({
54
79
  urls: ["https://docs.firecrawl.dev"],
55
80
  schema: schema,
56
81
  prompt: "Extract the main page title",
@@ -65,6 +90,6 @@ describe("v2.extract e2e", () => {
65
90
  expect(typeof resp.data).toBe("object");
66
91
  expect(schema.safeParse(resp.data).success).toBe(true);
67
92
  }
68
- }, 180_000);
93
+ }, testTimeoutMs(333_000));
69
94
  });
70
95
 
@@ -4,6 +4,7 @@
4
4
  import Firecrawl from "../../../index";
5
5
  import { config } from "dotenv";
6
6
  import { getIdentity, getApiUrl } from "./utils/idmux";
7
+ import { testTimeoutMs, withRateLimitRetry } from "./utils/rateLimit";
7
8
  import { describe, test, expect, beforeAll } from "@jest/globals";
8
9
 
9
10
  config();
@@ -13,7 +14,7 @@ let client: Firecrawl;
13
14
 
14
15
  beforeAll(async () => {
15
16
  const { apiKey } = await getIdentity({ name: "js-e2e-map" });
16
- client = new Firecrawl({ apiKey, apiUrl: API_URL });
17
+ client = withRateLimitRetry(new Firecrawl({ apiKey, apiUrl: API_URL }));
17
18
  });
18
19
 
19
20
  describe("v2.map e2e", () => {
@@ -30,7 +31,7 @@ describe("v2.map e2e", () => {
30
31
  expect(typeof first.url).toBe("string");
31
32
  expect(first.url.startsWith("http")).toBe(true);
32
33
  }
33
- }, 90_000);
34
+ }, testTimeoutMs(90_000));
34
35
 
35
36
  test.each(["only", "skip", "include"]) ("with options sitemap=%s", async (sitemap) => {
36
37
  if (!client) throw new Error();
@@ -50,6 +51,6 @@ describe("v2.map e2e", () => {
50
51
  expect(typeof link.url).toBe("string");
51
52
  expect(link.url.startsWith("http")).toBe(true);
52
53
  }
53
- }, 120_000);
54
+ }, testTimeoutMs(120_000));
54
55
  });
55
56
 
@@ -1,6 +1,7 @@
1
1
  import Firecrawl from "../../../index";
2
2
  import { config } from "dotenv";
3
3
  import { getIdentity, getApiUrl } from "./utils/idmux";
4
+ import { testTimeoutMs, withRateLimitRetry } from "./utils/rateLimit";
4
5
  import { describe, test, expect, beforeAll } from "@jest/globals";
5
6
 
6
7
  config();
@@ -10,7 +11,7 @@ let client: Firecrawl;
10
11
 
11
12
  beforeAll(async () => {
12
13
  const { apiKey } = await getIdentity({ name: "js-e2e-parse" });
13
- client = new Firecrawl({ apiKey, apiUrl: API_URL });
14
+ client = withRateLimitRetry(new Firecrawl({ apiKey, apiUrl: API_URL }));
14
15
  });
15
16
 
16
17
  describe("v2.parse e2e", () => {
@@ -41,7 +42,7 @@ describe("v2.parse e2e", () => {
41
42
  expect(doc.markdown).toContain("JS SDK Parse E2E");
42
43
  expect(doc.metadata?.creditsUsed).toBe(1);
43
44
  },
44
- 60_000,
45
+ testTimeoutMs(60_000),
45
46
  );
46
47
 
47
48
  test(
@@ -62,6 +63,6 @@ describe("v2.parse e2e", () => {
62
63
  ),
63
64
  ).rejects.toThrow();
64
65
  },
65
- 60_000,
66
+ testTimeoutMs(60_000),
66
67
  );
67
68
  });
@@ -5,6 +5,7 @@ import Firecrawl from "../../../index";
5
5
  import { z } from "zod";
6
6
  import { config } from "dotenv";
7
7
  import { getIdentity, getApiUrl } from "./utils/idmux";
8
+ import { testTimeoutMs, withRateLimitRetry } from "./utils/rateLimit";
8
9
  import { describe, test, expect, beforeAll } from "@jest/globals";
9
10
 
10
11
  config();
@@ -14,7 +15,7 @@ let client: Firecrawl;
14
15
 
15
16
  beforeAll(async () => {
16
17
  const { apiKey } = await getIdentity({ name: "js-e2e-scrape" });
17
- client = new Firecrawl({ apiKey, apiUrl: API_URL });
18
+ client = withRateLimitRetry(new Firecrawl({ apiKey, apiUrl: API_URL }));
18
19
  });
19
20
 
20
21
  describe("v2.scrape e2e", () => {
@@ -30,7 +31,7 @@ describe("v2.scrape e2e", () => {
30
31
  if (!client) throw new Error();
31
32
  const doc = await client.scrape("https://docs.firecrawl.dev");
32
33
  assertValidDocument(doc);
33
- }, 60_000);
34
+ }, testTimeoutMs(60_000));
34
35
 
35
36
  test("maximal: scrape with all options", async () => {
36
37
  if (!client) throw new Error();
@@ -71,7 +72,7 @@ describe("v2.scrape e2e", () => {
71
72
  maxAge: 60_000,
72
73
  });
73
74
  assertValidDocument(doc);
74
- }, 90_000);
75
+ }, testTimeoutMs(90_000));
75
76
 
76
77
  test("json format with zod schema (auto-converted internally)", async () => {
77
78
  if (!client) throw new Error();
@@ -90,14 +91,14 @@ describe("v2.scrape e2e", () => {
90
91
  ],
91
92
  });
92
93
  expect(doc).toBeTruthy();
93
- }, 90_000);
94
+ }, testTimeoutMs(90_000));
94
95
 
95
96
  test("summary format returns summary string", async () => {
96
97
  if (!client) throw new Error();
97
98
  const doc = await client.scrape("https://firecrawl.dev", { formats: ["summary"] });
98
99
  expect(typeof doc.summary).toBe("string");
99
100
  expect((doc.summary || "").length).toBeGreaterThan(10);
100
- }, 90_000);
101
+ }, testTimeoutMs(90_000));
101
102
 
102
103
  test.each([
103
104
  ["markdown", "markdown"],
@@ -119,7 +120,7 @@ describe("v2.scrape e2e", () => {
119
120
  expect(Array.isArray(doc.links)).toBe(true);
120
121
  expect((doc.links || []).length).toBeGreaterThan(0);
121
122
  }
122
- }, 90_000);
123
+ }, testTimeoutMs(90_000));
123
124
 
124
125
  test("images format: extract all images from webpage", async () => {
125
126
  if (!client) throw new Error();
@@ -131,7 +132,7 @@ describe("v2.scrape e2e", () => {
131
132
  expect(doc.images?.length).toBeGreaterThan(0);
132
133
  // Should find firecrawl logo/branding images
133
134
  expect(doc.images?.some(img => img.includes("firecrawl") || img.includes("logo"))).toBe(true);
134
- }, 60_000);
135
+ }, testTimeoutMs(60_000));
135
136
 
136
137
  test("images format: works with multiple formats", async () => {
137
138
  if (!client) throw new Error();
@@ -152,12 +153,12 @@ describe("v2.scrape e2e", () => {
152
153
 
153
154
  // Should discover additional images beyond those with obvious extensions
154
155
  expect(doc.images?.length).toBeGreaterThanOrEqual(linkImages.length);
155
- }, 60_000);
156
+ }, testTimeoutMs(60_000));
156
157
 
157
158
  test("invalid url should throw", async () => {
158
159
  if (!client) throw new Error();
159
160
  await expect(client.scrape("")).rejects.toThrow("URL cannot be empty");
160
161
  await expect(client.scrape(" ")).rejects.toThrow("URL cannot be empty");
161
- });
162
+ }, testTimeoutMs(151_000));
162
163
  });
163
164