Skip to content

fix(core): ignore explicitly undefined baseUrl in enqueueLinks helpers - #3924

Merged
B4nan merged 2 commits into
masterfrom
claude/laughing-shirley-28431d
Jul 29, 2026
Merged

fix(core): ignore explicitly undefined baseUrl in enqueueLinks helpers#3924
B4nan merged 2 commits into
masterfrom
claude/laughing-shirley-28431d

Conversation

@B4nan

@B4nan B4nan commented Jul 28, 2026

Copy link
Copy Markdown
Member

The per-crawler *CrawlerEnqueueLinks helpers built their call as { urls, baseUrl, ...enqueueLinksOptions }, so a user options object carrying an explicit baseUrl: undefined key overwrote the internally resolved base URL:

await enqueueLinks({ strategy: 'same-domain', baseUrl: cfg.baseUrl }); // cfg.baseUrl is undefined

Neither ow.optional.string nor TypeScript (without exactOptionalPropertyTypes) rejects an explicitly present undefined, so this slipped through silently. Inside enqueueLinks() a falsy baseUrl leaves enqueueStrategyPatterns empty, and an empty pattern array means "no filtering" in both createRequests and filterRequestsByPatterns — so every link on the page got enqueued regardless of the requested strategy, turning a same-domain crawl into an unbounded off-domain one.

Moving baseUrl after the spread fixes it, and is safe because resolveBaseUrlForEnqueueLinksFiltering already returns userProvidedBaseUrl first when the user did supply one, so the resolved value equals the user's whenever it is set. adaptive-playwright-crawler.ts already used this ordering.

Affected: cheerio-crawler, browser-crawler, jsdom-crawler, linkedom-crawler (both the bound options.enqueueLinks() path and the direct enqueueLinks() path).

The per-crawler enqueueLinks helpers built their call as
`{ urls, baseUrl, ...enqueueLinksOptions }`, so a user options object
carrying an explicit `baseUrl: undefined` key overwrote the internally
resolved base URL. A falsy `baseUrl` leaves `enqueueStrategyPatterns`
empty, which means "no filtering" - every link on the page got enqueued
regardless of the requested strategy, silently going off-domain.

Spreading before `baseUrl` is safe because
`resolveBaseUrlForEnqueueLinksFiltering` already returns the user's
value whenever they did supply one.
@B4nan B4nan added adhoc Ad-hoc unplanned task added during the sprint. t-tooling Issues with this label are in the ownership of the tooling team. labels Jul 28, 2026
@github-actions github-actions Bot added this to the 146th sprint - Tooling team milestone Jul 28, 2026
@github-actions github-actions Bot added the tested Temporary label used only programatically for some analytics. label Jul 28, 2026
@B4nan
B4nan requested a review from vladfrangu July 28, 2026 15:24
…rley-28431d

# Conflicts:
#	test/core/enqueue_links/enqueue_links.test.ts
@B4nan
B4nan merged commit a58a9c1 into master Jul 29, 2026
8 checks passed
@B4nan
B4nan deleted the claude/laughing-shirley-28431d branch July 29, 2026 10:15
B4nan added a commit that referenced this pull request Aug 4, 2026
…defined (#3927)

Follow-up to #3924, same bug class one level down.

`BasicCrawler`'s context-bound `enqueueLinks` built its call as `{
requestQueue, robotsTxtFile, respectRobotsTxtFile, onSkippedRequest,
limit, ...options }`, so a user options object carrying an explicitly
present `undefined` key silently discarded the computed value:

```ts
await enqueueLinks({ urls, limit: config.limit }); // config.limit is not set
```

Neither `ow.optional.number` nor TypeScript (without
`exactOptionalPropertyTypes`) rejects an explicit `undefined`, so the
`calculateEnqueuedRequestLimit()` result was dropped — the queue kept
growing past the remaining `maxRequestsPerCrawl` budget and the
`enqueueLimit` skips were never reported to `onSkippedRequest`.
`maxRequestsPerCrawl` is still enforced as a stop condition via
`isMaxPagesExceeded()`, so the effect was queue bloat and missing
reporting rather than an unbounded crawl. The same applied to
`robotsTxtFile` and `respectRobotsTxtFile` (robots.txt filtering
silently disabled at enqueue time), and to `requestQueue`, where the
explicit `undefined` made `ow` throw instead.

Unlike #3924 this isn't a pure reordering, so the semantics for each
computed key:

- **`limit`** — now `min(user limit, remaining maxRequestsPerCrawl
budget)`. This is what `addRequests()` (`maxNewRequests` after the
spread) and the per-crawler `*CrawlerEnqueueLinks` helpers have always
done; this path was the outlier. A user limit larger than the remaining
budget is now capped, which is a user-visible change.
- **`onSkippedRequest`** — composed instead of replaced: the crawler's
reporting (logging, crawler-level `onSkippedRequest`) runs, then the
user's callback. Previously passing a callback to `enqueueLinks`
silently disabled the crawler-level one.
- **`requestQueue`, `robotsTxtFile`, `respectRobotsTxtFile`** — `??`
fallbacks, so a defined user value still wins.

The enqueue-limit log message now also distinguishes the two causes,
instead of blaming the user's `limit` when the `maxRequestsPerCrawl`
clamp is what bit.

Not covered here: `JSDOMCrawler` and `LinkeDOMCrawler` never route
`context.enqueueLinks` through this wrapper at all (they call the
standalone `enqueueLinks()` directly, unlike
`CheerioCrawler`/`BrowserCrawler` which forward the bound context
function), so `crawlDepth` injection and `maxCrawlDepth` are inert
there. Separate issue, separate fix.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

adhoc Ad-hoc unplanned task added during the sprint. t-tooling Issues with this label are in the ownership of the tooling team. tested Temporary label used only programatically for some analytics.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants