5668 Commits

Author SHA1 Message Date
Harry 69a824b77a refactor(core): retire TS-private test seams and convert to native private fields (#4043)
Co-authored-by: Jan Buchar <jan@buchar.dev>
2026-08-21 15:57:50 +02:00
Jan Buchar 73ee2b87db fix: Deflake tests under high concurrency (#4059) 2026-08-21 14:28:59 +02:00
Martin Adámek 3c2dd5cc37 chore: use typescript 7 for the packages, keep 6 for the docs build (#4058)
Bumps typescript to `^7.0.2` (native compiler) for building and
type-aware linting, together with `oxlint-tsgolint@^7.0.2001`, which
version-tracks TS 7.

The docs build stays on TS 6: typedoc needs the TypeScript JS API, which
the native compiler no longer exposes. The website keeps its own
`typescript: ^6.0.0` pin and pnpm nests that copy under
`node_modules/typedoc`, so typedoc resolves 6.x while the packages
compile with TS 7.

TS 7 also caught one real issue in the tests: `RequestList#initialize()`
is private, and the call in `request_list.test.ts` now needs the same
`@ts-expect-error` the surrounding test already uses for the private
constructor.

The api-extractor runner script also imports `typescript` for its report
parsing; that import now goes through a `typescript-v6` alias
(`npm:typescript@^6.0.0`), since the parsing needs the classic JS API
(api-extractor itself bundles its own TypeScript and is unaffected). The
regenerated reports in `docs/public-api/` pick up TS 7's single-quoted
string literal types in three packages, 4 lines total.

Same change as apify/apify-sdk-js#695. Build, docs build, type-aware
lint, test typecheck and the unit suite all pass.
2026-08-21 09:07:53 +02:00
Martin Adámek d57a413371 ci: pin third-party actions to commit SHAs (#4051)
This pins every third-party action in our workflows to a full commit
SHA, keeping the resolved version tag as a trailing comment. Renovate
understands that convention and updates the SHA and comment together.

The trigger: yesterday the `v11` tag of `EndBug/add-and-commit` moved to
the broken v11.1.0 release, whose `action.yml` fails to load
(`Unrecognized named-value: 'github'`), which killed our publish
workflow ([failed
run](https://github.com/apify/crawlee/actions/runs/32255557318)). With
SHA pins, a tag moving under us, by accident or by compromise, can't
break or hijack CI anymore. `EndBug/add-and-commit` is pinned to
v11.0.0, the last working release; the upstream fix is pending in
EndBug/add-and-commit#783.

Own-org references (`apify/workflows`, `apify/actions`,
`apify/setup-apify-cli-action`) stay on floating refs on purpose, since
we control those repos.
2026-08-20 14:30:29 +02:00
apify-deps[bot] 6d77689eb6 chore(deps): security bumps + in-range freshening (#4052)
## Summary
Dependency sweep — security bumps + in-range freshening (all within
existing ranges):
- `@hono/node-server` → 1.19.17, `hono` → 4.13.3 (medium/low, multiple
alerts)
- `fast-uri` → 3.1.5 (high, multiple alerts)
- `dompurify` → 3.4.13 (medium/low, multiple alerts)
- `js-yaml` → 3.15.1 / 4.3.1 (high, multiple alerts)
- `nx` → 22.7.8 (medium, multiple alerts)
- `mermaid` → 11.16.1 (medium/low, multiple alerts)
- `ip-address` → 10.5.0, via an in-range `express-rate-limit` 8.3.2 →
8.6.2 bump (high/medium, multiple alerts)
- `undici` → 7.29.0 (medium/high/low, multiple alerts)
- `postcss` → 8.5.26 (medium/high, multiple alerts)
- `svgo` → 3.3.4 (high)
- `markdown-it` → 14.3.0, `linkify-it` → 5.0.2 (medium/high, multiple
alerts)
- `body-parser` → 1.20.6 (low)
- `axios` → 1.18.1 / 1.19.0 (medium/high, multiple alerts)
- `protobufjs` → 7.6.5 (medium/high, multiple alerts)
- `webpack-dev-server` → 5.2.6 (medium, multiple alerts)
- `shell-quote` → 1.10.0 (critical/high)
- `brace-expansion` → 1.1.18 / 2.1.4 (high, multiple alerts)
- `websocket-driver` → 0.7.5 (critical/medium)
- `sigstore` → 4.1.1, `@sigstore/core` → 3.2.1, `@sigstore/verify` →
3.1.1 (high/medium)
- `http-proxy-middleware` → 2.0.10 (medium)
- `joi` → 17.13.4 (medium)
- `launch-editor` → 2.14.1 (medium)
- `@babel/core` → 7.29.7 (low)
- `ws` → 7.5.13 (high)
- `turbo` → 2.9.14+ range bump (medium)
- `qs` → 6.15.3 (medium, one occurrence — see flagged for the other)
- `uuid` → 11.1.1 for the reachable 11.x line (medium)
- plus opportunistic in-range minor freshening across the rest of
`pnpm-lock.yaml`

##  Dismissed (don't apply to our usage)
- `uuid` (medium) — `not_used`: the only remaining pre-11.1.1 copies are
`sockjs`'s `8.3.2` (webpack-dev-server, dev-only) and
`@langchain/core`'s `10.0.0` (stagehand e2e devDependency), both of
which only ever call `v4()`. The advisory is about missing bounds-checks
in `v3()`/`v5()`/`v6()` when a caller supplies a custom output buffer —
a path neither consumer exercises. The reachable 11.x copy is bumped to
the patched `11.1.1` in this PR. ([alert
#571](https://github.com/apify/crawlee/security/dependabot/571))
- `serialize-javascript` (medium) — `not_used`: pulled only by
`copy-webpack-plugin` and `css-minimizer-webpack-plugin`, which are
webpack plugins. The docs build uses Docusaurus's rspack bundler
(`future.faster.rspackBundler: true`), so the webpack path never runs
and the vulnerable serialize step isn't executed. ([alert
#572](https://github.com/apify/crawlee/security/dependabot/572))

## 🚩 Flagged — needs review
- `vite` `8.0.8` → `8.0.16` (medium/high) — peer-only dependency of
`vitest@4.1.4` (not a direct devDependency anywhere in the repo), so
`pnpm update`/overrides can't move it in-range; reaching the patch means
pinning `vite` as a direct devDependency, a manifest change outside this
sweep's scope. ([alert
#591](https://github.com/apify/crawlee/security/dependabot/591), [alert
#593](https://github.com/apify/crawlee/security/dependabot/593))
- `esbuild` `0.27.7` → `0.28.1` (low) — pinned by
`css-minimizer-webpack-plugin`'s docusaurus/webpack toolchain and by the
same peer-pinned `vite@8.0.8` above; no existing override to bump.
([alert #588](https://github.com/apify/crawlee/security/dependabot/588))
- `qs` `6.14.2` → `6.15.2` (medium) — pinned by `express@4.22.1`'s exact
`~6.14.0` range (used by the docs `webpack-dev-server` chain); needs a
new `pnpm-workspace.yaml` override to force past `6.14.x`. ([alert
#573](https://github.com/apify/crawlee/security/dependabot/573))

No open Renovate PR references any of `vite`, `esbuild`, or `qs` at the
moment.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: apify-deps[bot] <4649591+apify-deps[bot]@users.noreply.github.com>
2026-08-20 10:25:42 +02:00
Martin Adámek b0e828ae4c chore(deps): bump pnpm to 11.22.0 (#4050)
Bumps the pinned `pnpm` from 11.0.9 to 11.22.0, keeping all three pins
in sync: `packageManager`, `devEngines.packageManager.version`, and
`volta.pnpm` (Renovate's packageManager update typically bumps only the
first, which leaves corepack failing on a version mismatch).

Routine dependency-manager hygiene; no lockfile or source changes.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-08-19 19:20:12 +02:00
dependabot[bot] 9918489820 chore(deps): bump nanoid from 5.1.9 to 5.1.16 (#4046)
Bumps [nanoid](https://github.com/ai/nanoid) from 5.1.9 to 5.1.16.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/ai/nanoid/releases">nanoid's
releases</a>.</em></p>
<blockquote>
<h2>5.1.16</h2>
<ul>
<li>Fixed forever loop on negative size (by <a
href="https://github.com/spokodev"><code>@​spokodev</code></a>).</li>
</ul>
<h2>5.1.15</h2>
<ul>
<li>Fixed random pool corruption on big ID sizes.</li>
</ul>
<h2>5.1.14</h2>
<ul>
<li>Fixed npm package size regression.</li>
</ul>
<h2>5.1.13</h2>
<ul>
<li>Fixed npm package size regression.</li>
</ul>
<h2>5.1.12</h2>
<ul>
<li>Moved to npm Provenance and Staged Publishing.</li>
</ul>
<h2>5.1.11</h2>
<ul>
<li>Fixed breaking Nano ID by requesting big ID.</li>
</ul>
<h2>5.1.10</h2>
<ul>
<li>Fixed breaking nanoid by requesting big ID (by <a
href="https://github.com/alanzabihi"><code>@​alanzabihi</code></a>).</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/ai/nanoid/blob/main/CHANGELOG.md">nanoid's
changelog</a>.</em></p>
<blockquote>
<h2>5.1.16</h2>
<ul>
<li>Fixed forever loop on negative size (by <a
href="https://github.com/spokodev"><code>@​spokodev</code></a>).</li>
</ul>
<h2>5.1.15</h2>
<ul>
<li>Fixed random pool corruption on big ID sizes.</li>
</ul>
<h2>5.1.14</h2>
<ul>
<li>Fixed npm package size regression.</li>
</ul>
<h2>5.1.13</h2>
<ul>
<li>Fixed npm package size regression.</li>
</ul>
<h2>5.1.12</h2>
<ul>
<li>Moved to npm Provenance and Staged Publishing.</li>
</ul>
<h2>5.1.11</h2>
<ul>
<li>Fixed breaking Nano ID by requesting big ID.</li>
</ul>
<h2>5.1.10</h2>
<ul>
<li>Fixed breaking Nano ID by requesting big ID (by <a
href="https://github.com/alanzabihi"><code>@​alanzabihi</code></a>).</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/ai/nanoid/commit/6ccc67bbaba71d3d77a21d9b636f4171a268ce49"><code>6ccc67b</code></a>
Release 5.1.16 version</li>
<li><a
href="https://github.com/ai/nanoid/commit/6de05d794f62eeac3f527c74c34c6af0c1d32e49"><code>6de05d7</code></a>
fix(non-secure): clamp negative size with a smaller guard (<a
href="https://redirect.github.com/ai/nanoid/issues/600">#600</a>)</li>
<li><a
href="https://github.com/ai/nanoid/commit/39259034f7c095cb36a97d6937e77905a659808a"><code>3925903</code></a>
Update dependencies</li>
<li><a
href="https://github.com/ai/nanoid/commit/0b695546fd83beaa943fbcaa90af92895dd0ae55"><code>0b69554</code></a>
Release 5.1.15 version</li>
<li><a
href="https://github.com/ai/nanoid/commit/333c5a030e2ee68ba08c43c5021d8dbf1724cef5"><code>333c5a0</code></a>
Backport changelog changes for 3.x</li>
<li><a
href="https://github.com/ai/nanoid/commit/266eb637236f654a6ec6ef58fd12213e1bafe35b"><code>266eb63</code></a>
Remove debug code</li>
<li><a
href="https://github.com/ai/nanoid/commit/a6a94d3f8dc71513864f2e9ae9c5a4130511b34c"><code>a6a94d3</code></a>
Do not allow to pollute pool</li>
<li><a
href="https://github.com/ai/nanoid/commit/93fe197416c337bb732e2665be2c60968379dc73"><code>93fe197</code></a>
Reduce ID size</li>
<li><a
href="https://github.com/ai/nanoid/commit/e4b7a9a7323006474ec939112aec68944b0da097"><code>e4b7a9a</code></a>
Release 5.1.14 version</li>
<li><a
href="https://github.com/ai/nanoid/commit/6bf3469b5b33137e385e07dc1c84779cf3e38956"><code>6bf3469</code></a>
Update release action</li>
<li>Additional commits viewable in <a
href="https://github.com/ai/nanoid/compare/5.1.9...5.1.16">compare
view</a></li>
</ul>
</details>
<details>
<summary>Maintainer changes</summary>
<p>This version was pushed to npm by <a
href="https://www.npmjs.com/~GitHub%20Actions">GitHub Actions</a>, a new
releaser for nanoid since your current version.</p>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=nanoid&package-manager=npm_and_yarn&previous-version=5.1.9&new-version=5.1.16)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts page](https://github.com/apify/crawlee/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-19 14:17:00 +02:00
Martin Adámek 452afb1408 docs: add a playbook for switching master to the next major (#4049)
Adds a "switching master to the next major" playbook to RELEASE.md while
the v4 transition is fresh. It covers the order of operations
(maintenance branch first, then rebase, validate, and fast-forward
master), the docs snapshot removal and its unversioned `/js/api` route
gotcha, which rulesets block the fast-forward push and how the
`BypassTemporary` team covers them, why open PRs have to be retargeted
by hand, and the required-checks and renovate follow-ups.
2026-08-19 12:56:13 +02:00
Martin Adámek b04de353bd chore(deps): update dependency puppeteer to v25.8.0 (#4048)
Re-applies the puppeteer 25.8.0 bump that renovate landed on the
pre-merge master (the v4 merge brought back the old 25.3.0 pin). The
version change alone is not enough: the lockfile keeps a stale
`fingerprint-injector(puppeteer@25.3.0)` peer instance around, and the
duplicated `puppeteer-core` types break `tsc-check-tests` with 82
errors. A `pnpm dedupe` collapses the graph back to a single instance.
2026-08-19 09:41:26 +02:00
Martin Adámek 2137c701e5 chore(ci): point the release workflows at master for the v4 line
Canaries publish from master under the v4 dist-tag (next once 4.0.0 is
stable), the version-docs snapshot job is guarded to master, and
RELEASE.md now documents the branch/dist-tag matrix including the 3.x
maintenance branch.
2026-08-18 18:19:35 +02:00
Martin Adámek a1512cf367 chore: restore the typed schema-router overloads on createFileRouter
Lost in the v4 rebase; matches the createHttpRouter/createCheerioRouter
overload set. Also removes the rebase reconciliation checklist, which is
fully resolved by this commit.
2026-08-18 18:05:49 +02:00
Martin Adámek 2766e87443 docs: drop the 4.0 snapshot in favor of serving v4 docs as the next version
The 4.0 snapshot only existed so the v4 branch site build had a default
version. On master, the v4 docs are the current (next) version, labeled
"4.0 (RC)", and the real 4.0 snapshot will be generated by the release
workflow when 4.0.0 ships. The default docs version stays 3.18 until then.
2026-08-18 18:05:49 +02:00
Jan Buchar 80d7010f06 chore: Add oxlint plugin to warn about TS-private class members (#4035) 2026-08-18 17:48:53 +02:00
Jan Buchar ceb936af2a refactor!: Continue public API minimization (#4036)
Co-authored-by: Martin Adámek <banan23@gmail.com>
2026-08-18 17:48:53 +02:00
Martin Adámek 6188678305 chore(e2e): retry the platform statistics lookup instead of crashing on a read-after-write race
The statistics record is persisted during crawler teardown and the platform
key-value store is eventually consistent, so reading it immediately after the
run can miss it. That crashed the whole test with a TypeError on
stats.requestsFinished (seen in cheerio-curl-impersonate-ts) even though the
actor run itself succeeded. The lookup now retries for up to ~30 seconds and
falls back to an empty object, so a genuinely missing record fails the
assertions cleanly.
2026-08-18 17:48:53 +02:00
Jindřich Bär ffd240dc9d docs: warn against --omit=optional in v4 upgrading guide (#4034)
impit and fs-storage-native ship platform binaries as
optionalDependencies, so --omit=optional (common in v3 Docker templates)
breaks the install.
2026-08-18 17:48:53 +02:00
Jan Buchar a7b2f1c74b feat: Add Symbol.asyncDispose to disposable services for DX (#4032) 2026-08-18 17:48:53 +02:00
Martin Adámek 7aea1f5c37 chore(e2e): keep 403 responses retryable in the camoufox-cloudflare fixture
Cloudflare serves its challenge pages with a 403 status. On v3,
handleCloudflareChallenge() received the session and removed 403 from the
session pool's blocked status codes itself; v4 dropped that mechanism when the
hook was redesigned, so challenged requests died in throwOnBlockedRequest()
on every retry and the solver only ever got a single attempt. Solving the
challenge is probabilistic, which is why the fixture passes on master (where
retries reach the solver) and kept failing here.

blockedStatusCodes is a public crawler option in v4, so the fixture opts out
of 403 explicitly. Whether handleCloudflareChallengeHook() should handle this
automatically again is a follow-up design question.
2026-08-18 17:48:53 +02:00
Martin Adámek af00b2a524 docs: pin 3.18 API links in content carried over from master
The v3.18 release blog post and the 3.18 versioned upgrading guide linked to
the current (unversioned) API reference. That resolved fine on master, where
the current API was 3.18, but on v4 the docs build fails: StorageClient was
renamed and RequestValidationError's page moved. Version-pinned API links are
the established pattern in versioned content (see the 3.17 guide).
2026-08-18 17:48:53 +02:00
Martin Adámek bcc97ffab7 chore(e2e): pair camoufox-js ^0.11.0 with playwright 1.60.0 in the cloudflare fixture
camoufox-js releases bundle a specific Firefox build that must match the one
expected by the pinned playwright version (0.11 ↔ 1.60). The fixture pinned
camoufox-js ^0.12.0 next to playwright 1.60.0, so the Cloudflare challenge
kept failing on the platform even with the updated challenge markup handling
(#4019) in place — master validates that fix with the 0.11/1.60 pairing.
2026-08-18 17:48:53 +02:00
Martin Adámek 5a7d0b9ca2 chore(e2e): repair E2E suite for the enqueueLinks split and the zod catalog dependency
Two upstream v4 changes landed after the last E2E run and broke the suite:

- the enqueueLinks split (#4010) changed the context helper's return value to
  the addRequestsBatched result, so the *-enqueue-links fixtures now assert on
  `addedRequests` being empty instead of deep-equality with the old shape
- the zod validation unification (#3935) introduced a `catalog:` dependency,
  which npm cannot resolve when the platform builds the actor image; the E2E
  package-copy step now rewrites catalog deps to their pinned versions from
  pnpm-workspace.yaml, the same way it already rewrites `workspace:` deps
2026-08-18 17:48:53 +02:00
Martin Adámek c324386fde chore: fix formatting and regenerate public API reports after the rebase 2026-08-18 17:48:53 +02:00
Martin Adámek 4bca1e65f7 chore(test): use crawler.statistics in the maxRequestsPerCrawl budget tests
Follow-up to the BasicCrawler.stats → statistics rename (#4028) for two
master-carried tests it could not have known about.
2026-08-18 17:48:53 +02:00
Jan Buchar 41a2584d43 refactor!: Rename BasicCrawler.stats to statistics (#4028) 2026-08-18 17:48:53 +02:00
Jan Buchar 39148e6f1a refactor!: replace browserPoolOptions with browser pool factories (#4026)
closes #3728
2026-08-18 17:48:53 +02:00
Martin Adámek 839a9b2d09 chore: re-apply master-side fixes over the enqueueLinks split (#4010)
The transplanted enqueueLinks split reverted a few master-carried behaviors in
BasicCrawler; this restores them on top of the new design:

- stop capturing statistics before teardown again, so the crawler state is
  saved before the final persistence event fires (prevents double persistence)
- teardown() only emits an explicit PERSIST_STATE event for externally-managed
  event managers, and tears the owned session pool down with persistState
  matching event manager ownership (an unset flag previously fell back to the
  `persistState = true` default, double-persisting the pool)
- the enqueue limit log distinguishes an explicit `limit` from the remaining
  maxRequestsPerCrawl budget again
- adapt the master-carried tests to the addRequests() API; drop the
  explicit-undefined override tests for options that no longer exist on it
2026-08-18 17:48:53 +02:00
Jan Buchar e00e8ce84f feat: Accept custom extensions in the IStatistics interface (#3982)
- closes #3525
2026-08-18 17:48:53 +02:00
Jan Buchar d50177c014 feat: Improve sameDomainDelaySecs implementation via ThrottlingRequestManager (#4017)
closes #3997
closes #3148
2026-08-18 17:48:52 +02:00
Jan Buchar 8e3194b91a chore: Rename client load signal to storageBackend (#4023)
closes #3979
2026-08-18 17:48:52 +02:00
Jindřich Bär 6bcd06e668 feat: split enqueueLinks into extractLinks + addRequests (#4010)
Moves the `requestManager`-bound enqueueing logic into
`BasicCrawlerContext.addRequests`, and each DOM-aware crawler now
exposes its own `extractLinks()` plus an `enqueueLinks()` that composes
`extractLinks` + `addRequests`.

This aligns the JS implementation with what Python does, to some extent.

Closes #3081
2026-08-18 17:48:52 +02:00
Martin Adámek 375b08c9d5 chore(test): finish adapting master-carried tests to v4 APIs
- markRequestHandled → markRequestAsHandled on SitemapRequestLoader
- await the now-async RequestQueue.getTotalCount()
- transformRequestFunction skips now report the dedicated 'transform' reason
- robots.txt mock needs getCrawlDelay
- statistics/session-pool single-persistence tests observe KeyValueStore.setValue
  instead of the persistState methods RecoverableState replaced
- pass an explicit logger to Sitemap.load in the aggregated-warning test
2026-08-18 17:48:52 +02:00
Martin Adámek 75412fce14 chore(test): adapt master-carried tests to v4 APIs after rebase
- requestQueue → requestManager in enqueueLinks options
- await the now-async createRequestQueueMock
- sessionPoolOptions → sessionPool instance in the redirect-cookie test
- config → configuration in purgeDefaultStorages options
- SitemapRequestList → SitemapRequestLoader in loader tests
- handleCloudflareChallenge lost its session parameter in v4
- drop duplicate imports
2026-08-18 17:48:52 +02:00
Martin Adámek bd20a49f8a chore(utils): re-add tldts dependency for enqueue-strategy URL filtering
The domain matching in the restored `filterUrl`/`matchesEnqueueStrategy`
helpers (carried over from master) uses tldts, which had been dropped from
the package manifest during the rebase.
2026-08-18 17:48:52 +02:00
Martin Adámek bbd72d8ed2 chore: restore @ts-ignore on robotsParser calls under nodenext resolution
The rebase onto master replaced the RobotsTxtFile factory bodies with master's
versions (which keep the URL for enqueue-strategy filtering), dropping the
@ts-ignore comments v4 needs because robots-parser's CJS default export is not
callable under nodenext module resolution.
2026-08-18 17:48:52 +02:00
Martin Adámek 3ce18eac8e chore(templates): keep camoufox-js ^0.11.0 paired with playwright 1.60.0 in the camoufox template
The rebase onto master carried over master's renovate bump of camoufox-js to
^0.12.0, but the template (and the repo root) still pin the Playwright version
whose bundled Firefox matches camoufox-js 0.11.
2026-08-18 17:48:52 +02:00
Vlad Frangu dd6e3b9929 chore!: unify argument validation to zod (#3935)
Replaces the remaining `ow`-based argument validation with `zod` across
all packages and reworks how validation results are consumed and
reported.

Closes #3716

- **`ow` is gone** — every argument check now goes through
`parseArgument(value, schema, label?)` from `@crawlee/utils`, backed by
shared zod schemas (`schemas`, exported via `@crawlee/utils/internal`).
The `@sapphire/shapeshift` checks in `@crawlee/fs-storage` were
converted too, so a single validation library remains.
- **Parse results are used everywhere** — option defaults moved from
destructuring into the schemas (`.default(...)`), and call sites
destructure the typed parse result. `parseArgument` returns `TValue &
z.output<TSchema>`, so call sites keep their declared TS types while
gaining the defaults.
- **Schemas are built once** — all per-call schemas are hoisted to
module scope; crawler/launcher classes build their strict options schema
once as a `static optionsSchema` next to `optionsShape`. The
`urlPatternSchema` for `include`/`exclude` lives in
`enqueue_links/shared.ts`, next to the type it validates.
- **Specific validators instead of `anyObject`** — class-typed options
use `z.instanceof(...)` (`BaseHttpClient`, `Configuration`,
`EventManager`), interface-typed ones use duck-typed `objectWithKeys`
validators (`storageBackend`, `requestManager`, `logger`, …), and
element-typed arrays use the new `schemas.arrayOf(item, 'numbers')`.

`ArgumentValidationError` (replacing ow's `ArgumentError`) renders one
line per issue: the expected type, the received type and value folded
into one clause, the offending field path, and the validated interface:

```text
// v3 (ow) — first issue only
Expected property `maxRequestRetries` to be of type `number` but received type `string` in object `HttpCrawlerOptions`

// v4 (zod) — every issue, one line each
Invalid input: expected number, received the string `many` at `maxRequestRetries` in `HttpCrawlerOptions`
Invalid input: expected an array of numbers, received the number `500` at `additionalHttpErrorStatusCodes` in `HttpCrawlerOptions`
Invalid input: expected boolean, received the string `yes` at `retryOnBlocked` in `HttpCrawlerOptions`
```

Details worth knowing:

- Union failures expand into one line per failed arm (zod's own message
is a bare "Invalid input").
- `NaN` is named as itself, an empty string renders as `''`, and arrays
name their element type (``expected an array of URL patterns``) — none
of which ow or stock zod reported.
- `new Request('https://…')` gets a targeted hint pointing at the `{ url
}` object form.
- For programmatic handling, the error exposes zod's structured output:
`error.issues` and the raw `ZodError` as a typed `cause`.

The migration is documented in the v4 upgrading guide
(`docs/upgrading/upgrading_v4.md`), including a rename-cheat-sheet
entry.

- Custom HTTP clients must now **extend `BaseHttpClient`** from
`@crawlee/http-client` rather than just implementing the interface (all
shipped clients already do; `LazyDefaultHttpClient` was converted). Same
applies to test mocks — `Object.create(BaseHttpClient.prototype)` works.
- One caveat of consuming parse results: zod object schemas return a
pruned plain copy, so options holding class instances are validated with
passthrough schemas (`z.custom`-based) to keep their prototypes — there
are comments at the relevant schemas.
- Fixes a few latent gaps surfaced along the way: `Request.state` now
accepts `RequestState.SKIPPED` (validated via `z.enum(RequestState)`),
and the publish-time catalog inlining covers `optionalDependencies`.
- `ArgumentValidationError` and its formatter are intentionally kept
close to the copy in apify/apify-client-js#986 — a follow-up may extract
them into a shared package.

---------

Co-authored-by: Martin Adámek <banan23@gmail.com>
2026-08-18 17:48:52 +02:00
Martin Adámek ea9da4c1c2 test: adapt E2E suite to v4 and fix regressions it caught (#4012)
Gets the E2E test suite running against v4. The suite hadn't been run
since the v4 rewrite and everything failed on startup. After these
changes the MEMORY run passes locally end to end, and getting there
surfaced a few real regressions in the packages themselves.

- `LinkeDOMCrawler`'s `enqueueLinks` helper referenced the global
`document` (which doesn't exist in Node) instead of the parsed window,
so every call crashed at runtime.
- `ErrorSnapshotter.saveHTMLSnapshot()` returned the record key with a
v3-style `.html` suffix, so the follow-up `getPublicUrl()` lookup missed
and `firstErrorHtmlUrl` never made it into the crawler statistics.
- `JSDOMCrawlingContext`/`LinkeDOMCrawlingContext` didn't override
`enqueueLinks`, exposing the strict urls-required signature even though
the runtime helper extracts URLs from the parsed document.
- `LinkeDOMCrawler` can now be constructed without arguments, like the
other crawlers.

- Bumped the pinned `apify` SDK to 4.0.0-beta.22 (beta.19 imports
`snakeCaseToCamelCase` from `@crawlee/utils`, which no longer exists
there).
- Adapted `tools.mjs` to the fs-storage on-disk layout (extensionless
key-value records; the short-lived `__default__` directory alias it
originally targeted was a bug, fixed in #4013) and to the
`@crawlee/utils` exports split.
- Migrated test actors to the v4 APIs: the `logger` option with
`ApifyLogAdapter` instead of `log`, hooks reading `gotoOptions` from the
crawling context, `session.setCookie()`, a custom `SessionPool` instead
of `sessionPoolOptions`, the WHATWG `Response` returned by
`sendRequest`, `registerDeferredCleanup` for dataset writes that must
survive a throwing handler, and explicit enqueue strategies now that
`include` globs are ANDed with the default same-hostname strategy.
- The ignore-ssl test now configures TLS verification on the http
client, because the crawler-level `ignoreSslErrors` option is not wired
to the default client in v4. That dangling option deserves a separate
fix or removal, since it currently does nothing.
- The impit test pins session fingerprints, since the random default
fingerprint overrides the client's browser impersonation.
- Added ES2022 to the actor tsconfigs' `lib` (a bare `["DOM"]` drops the
ES lib and broke compilation on `ErrorOptions`).
- Skipped the zero-concurrency queue test: it stages a stuck queue
through the v3 client-side `inProgress` set, which the rewritten queue
doesn't have.
- Fixed the camoufox fetch retry loop fetching 5x even on success, and
removed a duplicate `apify` dependency key that silently downgraded the
curl-impersonate actor to SDK v3.
- Commented out the LOCAL storage matrix entry in the workflow, as
`@apify/storage-local` doesn't support v4.
2026-08-18 17:48:52 +02:00
Jan Buchar f08e8d6a8c refactor: Use RecoverableState to implement Statistics persistence (#4014)
closes #3985
2026-08-18 17:48:52 +02:00
Jan Buchar 0d880420c7 feat: Extend RecoverableState API to accommodate future adopters (#4006)
closes #3984
2026-08-18 17:48:52 +02:00
Jan Buchar 4dbce0a6b0 fix: Fix purgeOnStart on aliased storages (#4013)
- closes #3998

Unnamed storages (default and aliased) are now purged on start, named
ones are not — crawlee-python's rule. The fs backend sweeps the storage
directories, so leftovers from a previous process are caught too.

Two adjacent bugs, one commit each:
- `createDatasetBackend()` / `({})` didn't open the default storage,
though `StorageIdentifier` says they do
- the `__default__` sentinel leaked into the directory name, so default
storages lived in `storage/datasets/__default__` rather than `default`

`ThrottlingRequestManager` (#3741) sub-queues are alias-keyed, so they
now only survive a restart with `purgeOnStart` off — as its docs already
said.
2026-08-18 17:48:52 +02:00
Martin Adámek bb2f26743f chore: support RC releases in the publish workflow (#4009)
Adds an `rc` dist-tag option to the `publish-to-npm.yml` workflow so we
can publish v4 release candidates, same setup as in mikro-orm.

Dispatching the workflow with `dist-tag: rc` (from the `v4` branch)
will:

- bump versions via `copy --canary=major --preid=rc`, resolving the next
free `4.0.0-rc.N` from npm (first run produces `4.0.0-rc.0`)
- publish all packages under the `rc` dist-tag (new `publish:rc` script)
- create and push a `v4.0.0-rc.N` git tag pointing at the released
commit

Unlike stable releases, no changelog is generated and no commit is
pushed to the branch. The version bump lives only on npm, and the only
trace in git is the tag (same as mikro-orm's RC flow).

The regular beta canary flow (`dist-tag: v4`/`next`) is unchanged. This
only needs to live on the `v4` branch: `workflow_dispatch` uses the
workflow file from the branch selected in the run dropdown, so `master`
needs no changes.
2026-08-18 17:48:52 +02:00
Martin Adámek d48cd5b855 chore: wire the ignoreTlsErrors crawler option (formerly ignoreSslErrors) through to the HTTP client (#4011)
The `ignoreSslErrors` option stopped working during the v4 HTTP client
interface rework: it was still folded into got-style request options,
but those never reach `httpClient.sendRequest()`. In v3 the option
works, and actors commonly expose it in their input schemas and pass it
into crawler options (e.g. actor-scraper), so this keeps it working
instead of removing it.

The option is renamed to `ignoreTlsErrors`, matching
`session.proxyInfo.ignoreTlsErrors`, the browser pool, and the impit
client (the old name is dropped, documented in the upgrading guide;
actors migrating to v4 rename it in their own code, the SDK does not
touch this option). The crawler forwards it (still defaulting to `true`,
same as v3) as a new `SendRequestOptions.ignoreTlsErrors` flag, which
`BaseHttpClient` also enables for MITM proxy sessions (previously
equally dead). The impit client honors the flag; for custom clients it
is best effort, and `FetchHttpClient` cannot disable TLS verification at
all. The dead plumbing in `getRequestOptions()` is removed and unit
tests cover the forwarding chain.
2026-08-18 17:48:52 +02:00
Vlad Frangu 400fadf408 chore: convert const enums to regular enums (#4008)
Converts all `const enum` declarations (`EventType`,
`BROWSER_POOL_EVENTS`, `BROWSER_CONTROLLER_EVENTS`,
`OperatingSystemsName`, `DeviceCategory`) to regular enums. `const enum`
breaks downstream consumers using `isolatedModules` or transpilers that
don't inline them (Babel, esbuild, vitest).

Closes #3125
2026-08-18 17:48:52 +02:00
Jindřich Bär ea5b8c22df perf(http-client): lazy-load tough-cookie (#4007)
Types the default cookie jar against `@crawlee/types`' `CookieJar`
interface and dynamically imports `tough-cookie` only when a default jar
actually needs constructing.
2026-08-18 17:48:52 +02:00
Jindřich Bär e09444c88f perf(utils): lazily import sax (#4001)
Relates to #3549. `sax` was imported eagerly at module scope in
`packages/utils/src/internals/sitemap.ts`, so every consumer of
`@crawlee/utils` paid its load cost even if sitemap parsing was never
used.

`sax.SAXParser` is now loaded via a dynamic `import()` only when an XML
sitemap parser is actually constructed.

No public API changes (this class is only used from the `parseSitemap`
generator).
2026-08-18 17:48:51 +02:00
Harry 7bca635795 feat: implement per-domain request throttling (ThrottlingRequestManager) (#3741)
Co-authored-by: Jan Buchar <jan@buchar.dev>
2026-08-18 17:48:51 +02:00
Jindřich Bär 6a94b86a38 feat(utils): split exports into public and /internal entry points (#3991)
Adds a `/internal` subpath export to `@crawlee/utils` for symbols that
are only shared between crawlee packages (CSS selectors, type guards,
URL helpers, `CheerioRoot`, etc.), keeping the root entry point for
user-facing utilities. The `crawlee` meta-package naturally only
re-exports the public surface.

Closes #3079
2026-08-18 17:48:51 +02:00
Jindřich Bär 60c4b55491 perf: lazy-load file-type and csv-stringify (#3989)
Both are only used on specific code paths (sitemap parsing, CSV export)
but were loaded eagerly by every crawler import, costing ~110ms of the
~500ms `import('@crawlee/http')` on my machine.

Related: #3549
2026-08-18 17:48:51 +02:00
Martin Adámek d95cf20a67 fix(browser-pool): make PlaywrightBrowser.browserType() return the browser type (#3981)
`PlaywrightBrowser._setBrowserType()` has existed since the initial
crawlee commit but was never called, so the `_browserType` field stayed
`undefined` and `browserType()` returned `undefined` at runtime despite
its non-optional `BrowserType` signature. Persistent contexts are the
default (`useIncognitoPages: false`), so a call like
`browser.browserType().name()` on the wrapper threw a `TypeError`. The
incognito and remote-connection paths return the native Playwright
`Browser` and were not affected.

This wires the existing setter in `PlaywrightPlugin._launch()` where the
wrapper is created. `this.library` is the `BrowserType` that launched
the context, so the wrapper now provides the consistent API with
Playwright's `Browser` that its docblock describes. Also adds a test
covering both the persistent-context wrapper and the native incognito
browser.
2026-08-18 17:48:51 +02:00
Jindřich Bär dd7748d649 fix: declare own CookieJar types in @crawlee/types (#3978)
Closes #3945. 

`@crawlee/types` no longer imports `CookieJar`/`SerializedCookieJar`
from `tough-cookie` - it declares its own structurally-compatible
interfaces instead, so `tough-cookie` is no longer part of its
dependency tree. `tough-cookie` stays a direct dependency everywhere
it's actually used (`core`, `http-client`, `impit-client`).
2026-08-18 17:48:51 +02:00