@nitpicker/crawler 0.9.0 → 0.12.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/lib/archive/archive-accessor.d.ts +87 -14
- package/lib/archive/archive-accessor.js +162 -36
- package/lib/archive/archive.d.ts +262 -24
- package/lib/archive/archive.js +397 -86
- package/lib/archive/cache/compute-archive-cache-key.d.ts +39 -0
- package/lib/archive/cache/compute-archive-cache-key.js +95 -0
- package/lib/archive/cache/extract-archive-to-cache.d.ts +43 -0
- package/lib/archive/cache/extract-archive-to-cache.js +309 -0
- package/lib/archive/cache/get-archive-cache-root.d.ts +20 -0
- package/lib/archive/cache/get-archive-cache-root.js +53 -0
- package/lib/archive/cache/is-archive-cache-disabled.d.ts +24 -0
- package/lib/archive/cache/is-archive-cache-disabled.js +34 -0
- package/lib/archive/cache/resolve-archive-cache-dir.d.ts +26 -0
- package/lib/archive/cache/resolve-archive-cache-dir.js +32 -0
- package/lib/archive/database.d.ts +411 -26
- package/lib/archive/database.js +1881 -540
- package/lib/archive/derive-lineage-from-parent.d.ts +37 -0
- package/lib/archive/derive-lineage-from-parent.js +42 -0
- package/lib/archive/filesystem/peek-tar-top-dir.d.ts +28 -0
- package/lib/archive/filesystem/peek-tar-top-dir.js +65 -0
- package/lib/archive/get-failed-page-messages.d.ts +43 -0
- package/lib/archive/get-failed-page-messages.js +131 -0
- package/lib/archive/init-schema.d.ts +53 -2
- package/lib/archive/init-schema.js +399 -15
- package/lib/archive/is-inventory-source.d.ts +21 -0
- package/lib/archive/is-inventory-source.js +22 -0
- package/lib/archive/meta/assert-compatible-version.d.ts +39 -0
- package/lib/archive/meta/assert-compatible-version.js +72 -0
- package/lib/archive/meta/classify-jsonld-type.d.ts +23 -0
- package/lib/archive/meta/classify-jsonld-type.js +43 -0
- package/lib/archive/meta/compare-semver.d.ts +23 -0
- package/lib/archive/meta/compare-semver.js +51 -0
- package/lib/archive/meta/compute-page-denormalized.d.ts +21 -0
- package/lib/archive/meta/compute-page-denormalized.js +35 -0
- package/lib/archive/meta/derive-flat-from-meta.d.ts +35 -0
- package/lib/archive/meta/derive-flat-from-meta.js +158 -0
- package/lib/archive/meta/derive-meta-extras.d.ts +20 -0
- package/lib/archive/meta/derive-meta-extras.js +23 -0
- package/lib/archive/meta/extract-tags-for-archive.d.ts +18 -0
- package/lib/archive/meta/extract-tags-for-archive.js +36 -0
- package/lib/archive/meta/summarize-jsonld.d.ts +17 -0
- package/lib/archive/meta/summarize-jsonld.js +29 -0
- package/lib/archive/meta/summarize-tags.d.ts +16 -0
- package/lib/archive/meta/summarize-tags.js +33 -0
- package/lib/archive/meta/types.d.ts +207 -0
- package/lib/archive/meta/types.js +33 -0
- package/lib/archive/migrate-crawl-errors.d.ts +20 -0
- package/lib/archive/migrate-crawl-errors.js +38 -0
- package/lib/archive/migrate-html-blob-tables.d.ts +24 -0
- package/lib/archive/migrate-html-blob-tables.js +53 -0
- package/lib/archive/migrate-inventory-runs.d.ts +29 -0
- package/lib/archive/migrate-inventory-runs.js +52 -0
- package/lib/archive/migrate-page-errors.d.ts +16 -0
- package/lib/archive/migrate-page-errors.js +35 -0
- package/lib/archive/migrate-pages-resources-source.d.ts +16 -0
- package/lib/archive/migrate-pages-resources-source.js +46 -0
- package/lib/archive/page.d.ts +187 -49
- package/lib/archive/page.js +258 -63
- package/lib/archive/peek-archive-lock.d.ts +40 -0
- package/lib/archive/peek-archive-lock.js +62 -0
- package/lib/archive/resolve-redirect-chain.d.ts +33 -0
- package/lib/archive/resolve-redirect-chain.js +27 -0
- package/lib/archive/types.d.ts +168 -26
- package/lib/classify-error-kind.d.ts +19 -0
- package/lib/classify-error-kind.js +122 -0
- package/lib/crawler/build-js-redirect-edge.d.ts +68 -0
- package/lib/crawler/build-js-redirect-edge.js +57 -0
- package/lib/crawler/build-redirect-event.d.ts +24 -0
- package/lib/crawler/build-redirect-event.js +28 -0
- package/lib/crawler/clear-dns-burned-host-cache.d.ts +6 -0
- package/lib/crawler/clear-dns-burned-host-cache.js +11 -0
- package/lib/crawler/close-browser-safely.d.ts +64 -0
- package/lib/crawler/close-browser-safely.js +73 -0
- package/lib/crawler/crawler.d.ts +7 -2
- package/lib/crawler/crawler.js +919 -113
- package/lib/crawler/create-change-phase-handler.d.ts +54 -0
- package/lib/crawler/create-change-phase-handler.js +44 -0
- package/lib/crawler/derive-js-redirect-target.d.ts +68 -0
- package/lib/crawler/derive-js-redirect-target.js +129 -0
- package/lib/crawler/derive-page-source.d.ts +23 -0
- package/lib/crawler/derive-page-source.js +28 -0
- package/lib/crawler/derive-resource-source.d.ts +33 -0
- package/lib/crawler/derive-resource-source.js +37 -0
- package/lib/crawler/dns-burned-host-cache.d.ts +26 -0
- package/lib/crawler/dns-burned-host-cache.js +25 -0
- package/lib/crawler/dns-burned-host-short-circuit-counter.d.ts +13 -0
- package/lib/crawler/dns-burned-host-short-circuit-counter.js +11 -0
- package/lib/crawler/drain-phase-errors.d.ts +48 -0
- package/lib/crawler/drain-phase-errors.js +35 -0
- package/lib/crawler/fetch-destination.d.ts +12 -4
- package/lib/crawler/fetch-destination.js +132 -18
- package/lib/crawler/format-crawl-progress.d.ts +12 -3
- package/lib/crawler/format-crawl-progress.js +14 -6
- package/lib/crawler/handle-browser-close.d.ts +29 -0
- package/lib/crawler/handle-browser-close.js +28 -0
- package/lib/crawler/is-html-content-type.d.ts +17 -0
- package/lib/crawler/is-html-content-type.js +19 -0
- package/lib/crawler/is-js-redirect-error-shape.d.ts +40 -0
- package/lib/crawler/is-js-redirect-error-shape.js +53 -0
- package/lib/crawler/is-likely-html-url.d.ts +22 -0
- package/lib/crawler/is-likely-html-url.js +65 -0
- package/lib/crawler/is-puppeteer-fallback-candidate.d.ts +16 -0
- package/lib/crawler/is-puppeteer-fallback-candidate.js +63 -0
- package/lib/crawler/kill-process-tree.d.ts +94 -0
- package/lib/crawler/kill-process-tree.js +178 -0
- package/lib/crawler/link-list.d.ts +21 -1
- package/lib/crawler/link-list.js +25 -4
- package/lib/crawler/link-to-page-data.d.ts +13 -5
- package/lib/crawler/link-to-page-data.js +26 -5
- package/lib/crawler/log-undrained-phase-errors.d.ts +37 -0
- package/lib/crawler/log-undrained-phase-errors.js +34 -0
- package/lib/crawler/normalize-content-type.d.ts +14 -0
- package/lib/crawler/normalize-content-type.js +20 -0
- package/lib/crawler/partition-urls-by-html.d.ts +16 -0
- package/lib/crawler/partition-urls-by-html.js +23 -0
- package/lib/crawler/plan-sub-resource-emits.d.ts +63 -0
- package/lib/crawler/plan-sub-resource-emits.js +44 -0
- package/lib/crawler/preload-short-circuit-error.d.ts +22 -0
- package/lib/crawler/preload-short-circuit-error.js +25 -0
- package/lib/crawler/redirect-dest-key.d.ts +19 -0
- package/lib/crawler/redirect-dest-key.js +27 -0
- package/lib/crawler/resource-to-page-data.d.ts +28 -0
- package/lib/crawler/resource-to-page-data.js +59 -0
- package/lib/crawler/should-burn-host.d.ts +78 -0
- package/lib/crawler/should-burn-host.js +61 -0
- package/lib/crawler/should-get-fallback-on-head-failure.d.ts +38 -0
- package/lib/crawler/should-get-fallback-on-head-failure.js +46 -0
- package/lib/crawler/types.d.ts +229 -1
- package/lib/crawler-orchestrator.d.ts +105 -3
- package/lib/crawler-orchestrator.js +620 -20
- package/lib/crawler.d.ts +8 -2
- package/lib/crawler.js +6 -1
- package/lib/permanent-error-kinds.d.ts +43 -0
- package/lib/permanent-error-kinds.js +48 -0
- package/lib/resource-row-to-lookup-result.d.ts +13 -0
- package/lib/resource-row-to-lookup-result.js +20 -0
- package/lib/types.d.ts +95 -1
- package/lib/utils/compute-file-sha256.d.ts +23 -0
- package/lib/utils/compute-file-sha256.js +55 -0
- package/lib/utils/error/emit-error-with-retry.d.ts +40 -0
- package/lib/utils/error/emit-error-with-retry.js +44 -0
- package/lib/utils/error/emit-error.d.ts +39 -0
- package/lib/utils/error/emit-error.js +41 -0
- package/lib/utils/object/parse-response-headers.d.ts +12 -0
- package/lib/utils/object/parse-response-headers.js +26 -0
- package/package.json +11 -11
- package/lib/utils/error/error-emitter.d.ts +0 -18
- package/lib/utils/error/error-emitter.js +0 -29
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { BufferedPhaseError } from './drain-phase-errors.js';
|
|
2
|
+
import type { ChangePhaseEvent } from '@d-zero/beholder';
|
|
3
|
+
/**
|
|
4
|
+
* Options for {@link createChangePhaseHandler}. Declared structurally so
|
|
5
|
+
* tests can pass plain `vi.fn()` stubs without spinning up a real Crawler.
|
|
6
|
+
*/
|
|
7
|
+
export interface ChangePhaseHandlerOptions {
|
|
8
|
+
/**
|
|
9
|
+
* Forwards the raw {@link ChangePhaseEvent} so external listeners on the
|
|
10
|
+
* Crawler still see every transition (typically `this.emit.bind(this)`
|
|
11
|
+
* narrowed to the `changePhase` channel).
|
|
12
|
+
*/
|
|
13
|
+
emit: (event: ChangePhaseEvent) => void;
|
|
14
|
+
/** Receives the formatted progress log line. Skipped when empty. */
|
|
15
|
+
update: (log: string) => void;
|
|
16
|
+
/**
|
|
17
|
+
* Renders the human-readable progress message for an event. Injected so
|
|
18
|
+
* the handler stays free of the Crawler's internal log formatter.
|
|
19
|
+
* Returns `null` for events that should not surface to `update`.
|
|
20
|
+
*/
|
|
21
|
+
formatLog: (event: ChangePhaseEvent) => string | null;
|
|
22
|
+
/**
|
|
23
|
+
* Per-URL buffer of `retryExhausted` failures. The handler appends to
|
|
24
|
+
* this map; it does not drain (that is `drainPhaseErrors`'s job).
|
|
25
|
+
*/
|
|
26
|
+
buffer: Map<string, BufferedPhaseError[]>;
|
|
27
|
+
/** URL href used as the buffer key for this scrape. */
|
|
28
|
+
urlHref: string;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Builds the `scraper.on('changePhase', ...)` listener used by
|
|
32
|
+
* {@link Crawler.#launchBrowserAndScrape}.
|
|
33
|
+
*
|
|
34
|
+
* Three responsibilities:
|
|
35
|
+
* 1. Render the phase log via the injected `formatLog` and pipe it to `update`.
|
|
36
|
+
* 2. Forward the raw event so external consumers (CLI progress UI etc.) see
|
|
37
|
+
* every transition.
|
|
38
|
+
* 3. Buffer `retryExhausted` events into the per-URL phase-error map so they
|
|
39
|
+
* can be drained as `pageError` events AFTER the `page` event fires.
|
|
40
|
+
*
|
|
41
|
+
* WHY a factory: the listener captures per-scrape state (`buffer`, `urlHref`,
|
|
42
|
+
* `update`). Extracting the factory makes the wiring directly unit-testable
|
|
43
|
+
* with plain stubs, instead of requiring a mocked Puppeteer + beholder + dealer
|
|
44
|
+
* stack to drive the worker.
|
|
45
|
+
*
|
|
46
|
+
* **Caller contract**: register the returned handler at most once per
|
|
47
|
+
* `scraper` instance. The Crawler creates a fresh Scraper per URL so this
|
|
48
|
+
* holds today; if scraper pooling is ever introduced, register exactly one
|
|
49
|
+
* handler per scrape and unregister it on completion to avoid duplicate
|
|
50
|
+
* buffer entries.
|
|
51
|
+
* @param options - Wiring dependencies for the handler.
|
|
52
|
+
* @returns A function suitable for `scraper.on('changePhase', ...)`.
|
|
53
|
+
*/
|
|
54
|
+
export declare function createChangePhaseHandler(options: ChangePhaseHandlerOptions): (event: ChangePhaseEvent) => void;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds the `scraper.on('changePhase', ...)` listener used by
|
|
3
|
+
* {@link Crawler.#launchBrowserAndScrape}.
|
|
4
|
+
*
|
|
5
|
+
* Three responsibilities:
|
|
6
|
+
* 1. Render the phase log via the injected `formatLog` and pipe it to `update`.
|
|
7
|
+
* 2. Forward the raw event so external consumers (CLI progress UI etc.) see
|
|
8
|
+
* every transition.
|
|
9
|
+
* 3. Buffer `retryExhausted` events into the per-URL phase-error map so they
|
|
10
|
+
* can be drained as `pageError` events AFTER the `page` event fires.
|
|
11
|
+
*
|
|
12
|
+
* WHY a factory: the listener captures per-scrape state (`buffer`, `urlHref`,
|
|
13
|
+
* `update`). Extracting the factory makes the wiring directly unit-testable
|
|
14
|
+
* with plain stubs, instead of requiring a mocked Puppeteer + beholder + dealer
|
|
15
|
+
* stack to drive the worker.
|
|
16
|
+
*
|
|
17
|
+
* **Caller contract**: register the returned handler at most once per
|
|
18
|
+
* `scraper` instance. The Crawler creates a fresh Scraper per URL so this
|
|
19
|
+
* holds today; if scraper pooling is ever introduced, register exactly one
|
|
20
|
+
* handler per scrape and unregister it on completion to avoid duplicate
|
|
21
|
+
* buffer entries.
|
|
22
|
+
* @param options - Wiring dependencies for the handler.
|
|
23
|
+
* @returns A function suitable for `scraper.on('changePhase', ...)`.
|
|
24
|
+
*/
|
|
25
|
+
export function createChangePhaseHandler(options) {
|
|
26
|
+
const { emit, update, formatLog, buffer, urlHref } = options;
|
|
27
|
+
return (event) => {
|
|
28
|
+
const msg = formatLog(event);
|
|
29
|
+
if (msg) {
|
|
30
|
+
update(msg);
|
|
31
|
+
}
|
|
32
|
+
emit(event);
|
|
33
|
+
// retryExhausted fires when beholder's @retryable gives up on a
|
|
34
|
+
// secondary scrape step (e.g. a viewport switch detaching the frame
|
|
35
|
+
// in #fetchImages). The page itself still completes, so we buffer
|
|
36
|
+
// the failure here and emit it as a pageError after the page event
|
|
37
|
+
// has been emitted.
|
|
38
|
+
if (event.name === 'retryExhausted') {
|
|
39
|
+
const list = buffer.get(urlHref) ?? [];
|
|
40
|
+
list.push({ phase: event.name, message: event.message });
|
|
41
|
+
buffer.set(urlHref, list);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decide whether a puppeteer post-navigation URL represents a real
|
|
3
|
+
* client-side redirect (JS / meta-refresh) or just noise.
|
|
4
|
+
*
|
|
5
|
+
* **Why this helper exists:** when `page.goto()` returns `null`, the upstream
|
|
6
|
+
* scraper throws `The method Page.goto returned null` — classified as
|
|
7
|
+
* `protocol`, neither permanent nor a puppeteer-fallback kind, so
|
|
8
|
+
* `--retry-failed` never converges. Reading `page.url()` after the throw is
|
|
9
|
+
* the only way to recover the destination Chromium actually navigated to (the
|
|
10
|
+
* thrown error carries no URL), so we can record the source as a redirect
|
|
11
|
+
* edge instead of a hard `status = -1`. But `page.url()` can also report
|
|
12
|
+
* uninformative values — `about:blank` before the first navigation completes,
|
|
13
|
+
* the original URL when nothing happened — and a naive "different ?" check
|
|
14
|
+
* would create a phantom redirect every time the navigation simply failed at
|
|
15
|
+
* the same URL.
|
|
16
|
+
*
|
|
17
|
+
* The filter is intentionally narrow: anything that does not look like an
|
|
18
|
+
* `http(s):` URL semantically distinct from the originally requested
|
|
19
|
+
* location is discarded. Edge cases the test pins:
|
|
20
|
+
*
|
|
21
|
+
* - identity after URL canonicalisation (case / trailing-slash / default
|
|
22
|
+
* port / credentials / fragment) → null. WHATWG URL parsing handles
|
|
23
|
+
* `https://Example.COM` vs `https://example.com/`, `https://host:443/`
|
|
24
|
+
* vs `https://host/`, `https://u:p@host/x` vs `https://host/x`, etc.
|
|
25
|
+
* - `about:blank` / `chrome-error://...` / `data:` / `file:` → null
|
|
26
|
+
* (browser-internal sentinels, never a legitimate destination)
|
|
27
|
+
* - empty / whitespace string → null (defensive against
|
|
28
|
+
* `page.url()` returning `''` on a brand-new context)
|
|
29
|
+
* - any genuine `http(s):` URL semantically different from the source →
|
|
30
|
+
* returned credential-/fragment-stripped (defence-in-depth against a
|
|
31
|
+
* pre-RFC server issuing `Location: https://user:pass@host/path`, which
|
|
32
|
+
* would otherwise persist credentials into the `.nitpicker` archive
|
|
33
|
+
* — exactly the leak class the scope-auth-leak guard at the navigation
|
|
34
|
+
* side blocks)
|
|
35
|
+
*
|
|
36
|
+
* The caller does NOT need to pre-normalise URLs — this helper normalises
|
|
37
|
+
* both sides via the WHATWG URL parser before comparing. The production
|
|
38
|
+
* call site passes `url.withoutHashAndAuth` for `originalUrl`, but the
|
|
39
|
+
* normalisation here is idempotent so passing `url.href` would also work.
|
|
40
|
+
* @param originalUrl - The URL puppeteer was asked to navigate to.
|
|
41
|
+
* @param postNavigationUrl - The URL reported by `page.url()` after the
|
|
42
|
+
* throw. May be `null` / `undefined` when reading the URL itself failed.
|
|
43
|
+
* @returns The credential-/fragment-stripped post-navigation URL when it
|
|
44
|
+
* represents a real JS redirect, otherwise `null`.
|
|
45
|
+
* @example
|
|
46
|
+
* ```ts
|
|
47
|
+
* deriveJsRedirectTarget(
|
|
48
|
+
* 'https://www.example.com/old',
|
|
49
|
+
* 'https://www.example.com/new',
|
|
50
|
+
* ); // → 'https://www.example.com/new'
|
|
51
|
+
*
|
|
52
|
+
* deriveJsRedirectTarget('https://www.example.com/old', 'about:blank');
|
|
53
|
+
* // → null
|
|
54
|
+
*
|
|
55
|
+
* // Case / trailing-slash noise — no phantom self-redirect:
|
|
56
|
+
* deriveJsRedirectTarget(
|
|
57
|
+
* 'https://www.example.com',
|
|
58
|
+
* 'https://www.example.com/',
|
|
59
|
+
* ); // → null
|
|
60
|
+
*
|
|
61
|
+
* // Credentials in destination — stripped before return:
|
|
62
|
+
* deriveJsRedirectTarget(
|
|
63
|
+
* 'https://www.example.com/',
|
|
64
|
+
* 'https://user:pass@www.example.com/dest',
|
|
65
|
+
* ); // → 'https://www.example.com/dest'
|
|
66
|
+
* ```
|
|
67
|
+
*/
|
|
68
|
+
export declare function deriveJsRedirectTarget(originalUrl: string, postNavigationUrl: string | null | undefined): string | null;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/** Matches only the `http:` and `https:` schemes (case-insensitive). */
|
|
2
|
+
const HTTP_SCHEME_PATTERN = /^https?:\/\//i;
|
|
3
|
+
/**
|
|
4
|
+
* Canonicalise a URL for the JS-redirect identity check and return value:
|
|
5
|
+
* strip credentials and fragment, normalise case / default-ports /
|
|
6
|
+
* trailing-slash via the WHATWG URL parser. Returns `null` when the input
|
|
7
|
+
* is unparseable.
|
|
8
|
+
*
|
|
9
|
+
* Used for both sides of the comparison so case-only or trailing-slash-only
|
|
10
|
+
* differences (`https://Example.com` vs `https://example.com/`) do not
|
|
11
|
+
* produce phantom self-redirects, and for the returned target so a pre-RFC
|
|
12
|
+
* `Location: https://user:pass@host/path` is never persisted into the
|
|
13
|
+
* archive with credentials (same threat model the scope-auth-leak guard
|
|
14
|
+
* mitigates on the navigation side).
|
|
15
|
+
* @param rawUrl - The URL string to canonicalise.
|
|
16
|
+
* @returns The canonical URL string, or `null` when unparseable.
|
|
17
|
+
*/
|
|
18
|
+
function canonicaliseForComparison(rawUrl) {
|
|
19
|
+
try {
|
|
20
|
+
const parsed = new URL(rawUrl);
|
|
21
|
+
parsed.username = '';
|
|
22
|
+
parsed.password = '';
|
|
23
|
+
parsed.hash = '';
|
|
24
|
+
return parsed.href;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Decide whether a puppeteer post-navigation URL represents a real
|
|
32
|
+
* client-side redirect (JS / meta-refresh) or just noise.
|
|
33
|
+
*
|
|
34
|
+
* **Why this helper exists:** when `page.goto()` returns `null`, the upstream
|
|
35
|
+
* scraper throws `The method Page.goto returned null` — classified as
|
|
36
|
+
* `protocol`, neither permanent nor a puppeteer-fallback kind, so
|
|
37
|
+
* `--retry-failed` never converges. Reading `page.url()` after the throw is
|
|
38
|
+
* the only way to recover the destination Chromium actually navigated to (the
|
|
39
|
+
* thrown error carries no URL), so we can record the source as a redirect
|
|
40
|
+
* edge instead of a hard `status = -1`. But `page.url()` can also report
|
|
41
|
+
* uninformative values — `about:blank` before the first navigation completes,
|
|
42
|
+
* the original URL when nothing happened — and a naive "different ?" check
|
|
43
|
+
* would create a phantom redirect every time the navigation simply failed at
|
|
44
|
+
* the same URL.
|
|
45
|
+
*
|
|
46
|
+
* The filter is intentionally narrow: anything that does not look like an
|
|
47
|
+
* `http(s):` URL semantically distinct from the originally requested
|
|
48
|
+
* location is discarded. Edge cases the test pins:
|
|
49
|
+
*
|
|
50
|
+
* - identity after URL canonicalisation (case / trailing-slash / default
|
|
51
|
+
* port / credentials / fragment) → null. WHATWG URL parsing handles
|
|
52
|
+
* `https://Example.COM` vs `https://example.com/`, `https://host:443/`
|
|
53
|
+
* vs `https://host/`, `https://u:p@host/x` vs `https://host/x`, etc.
|
|
54
|
+
* - `about:blank` / `chrome-error://...` / `data:` / `file:` → null
|
|
55
|
+
* (browser-internal sentinels, never a legitimate destination)
|
|
56
|
+
* - empty / whitespace string → null (defensive against
|
|
57
|
+
* `page.url()` returning `''` on a brand-new context)
|
|
58
|
+
* - any genuine `http(s):` URL semantically different from the source →
|
|
59
|
+
* returned credential-/fragment-stripped (defence-in-depth against a
|
|
60
|
+
* pre-RFC server issuing `Location: https://user:pass@host/path`, which
|
|
61
|
+
* would otherwise persist credentials into the `.nitpicker` archive
|
|
62
|
+
* — exactly the leak class the scope-auth-leak guard at the navigation
|
|
63
|
+
* side blocks)
|
|
64
|
+
*
|
|
65
|
+
* The caller does NOT need to pre-normalise URLs — this helper normalises
|
|
66
|
+
* both sides via the WHATWG URL parser before comparing. The production
|
|
67
|
+
* call site passes `url.withoutHashAndAuth` for `originalUrl`, but the
|
|
68
|
+
* normalisation here is idempotent so passing `url.href` would also work.
|
|
69
|
+
* @param originalUrl - The URL puppeteer was asked to navigate to.
|
|
70
|
+
* @param postNavigationUrl - The URL reported by `page.url()` after the
|
|
71
|
+
* throw. May be `null` / `undefined` when reading the URL itself failed.
|
|
72
|
+
* @returns The credential-/fragment-stripped post-navigation URL when it
|
|
73
|
+
* represents a real JS redirect, otherwise `null`.
|
|
74
|
+
* @example
|
|
75
|
+
* ```ts
|
|
76
|
+
* deriveJsRedirectTarget(
|
|
77
|
+
* 'https://www.example.com/old',
|
|
78
|
+
* 'https://www.example.com/new',
|
|
79
|
+
* ); // → 'https://www.example.com/new'
|
|
80
|
+
*
|
|
81
|
+
* deriveJsRedirectTarget('https://www.example.com/old', 'about:blank');
|
|
82
|
+
* // → null
|
|
83
|
+
*
|
|
84
|
+
* // Case / trailing-slash noise — no phantom self-redirect:
|
|
85
|
+
* deriveJsRedirectTarget(
|
|
86
|
+
* 'https://www.example.com',
|
|
87
|
+
* 'https://www.example.com/',
|
|
88
|
+
* ); // → null
|
|
89
|
+
*
|
|
90
|
+
* // Credentials in destination — stripped before return:
|
|
91
|
+
* deriveJsRedirectTarget(
|
|
92
|
+
* 'https://www.example.com/',
|
|
93
|
+
* 'https://user:pass@www.example.com/dest',
|
|
94
|
+
* ); // → 'https://www.example.com/dest'
|
|
95
|
+
* ```
|
|
96
|
+
*/
|
|
97
|
+
export function deriveJsRedirectTarget(originalUrl, postNavigationUrl) {
|
|
98
|
+
if (typeof postNavigationUrl !== 'string') {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
const trimmed = postNavigationUrl.trim();
|
|
102
|
+
if (trimmed === '') {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
// Browser-internal sentinels: `about:blank` appears before the first
|
|
106
|
+
// navigation completes, `chrome-error://...` after a network error
|
|
107
|
+
// renders the Chromium error page, and `data:` / `file:` / `javascript:`
|
|
108
|
+
// can never be the destination of an off-page redirect we want to record.
|
|
109
|
+
// `http:` / `https:` is the only safe positive match.
|
|
110
|
+
if (!HTTP_SCHEME_PATTERN.test(trimmed)) {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
const destinationCanonical = canonicaliseForComparison(trimmed);
|
|
114
|
+
if (destinationCanonical === null) {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
const originalCanonical = canonicaliseForComparison(originalUrl);
|
|
118
|
+
// `originalCanonical === null` only happens for a degenerate (unparseable)
|
|
119
|
+
// `originalUrl` — fall back to a raw string check so we never claim a
|
|
120
|
+
// redirect we cannot prove. The production call site always supplies a
|
|
121
|
+
// parseable form.
|
|
122
|
+
if (originalCanonical === null) {
|
|
123
|
+
return trimmed === originalUrl ? null : destinationCanonical;
|
|
124
|
+
}
|
|
125
|
+
if (destinationCanonical === originalCanonical) {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
return destinationCanonical;
|
|
129
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { InventoryMode } from './types.js';
|
|
2
|
+
import type { PageSource } from '../archive/types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Decide which {@link PageSource} label a newly-scraped page row should carry.
|
|
5
|
+
*
|
|
6
|
+
* When the crawler is NOT in inventory mode (`inventoryMode === null`),
|
|
7
|
+
* returns `undefined` — the caller emits no `source` and the DB DEFAULT
|
|
8
|
+
* `'crawled'` ends up on the row. This keeps the normal crawl path
|
|
9
|
+
* untouched.
|
|
10
|
+
*
|
|
11
|
+
* When inventory mode is active, the URL is matched against
|
|
12
|
+
* `inventoryMode.seedUrls`. A hit means the URL came straight from the
|
|
13
|
+
* user-supplied list (`'inventory-seed'`); a miss means the URL was found
|
|
14
|
+
* by following links from a seed page (`'inventory-discovered'`).
|
|
15
|
+
*
|
|
16
|
+
* Sub-resources captured by puppeteer during inventory-mode rendering are
|
|
17
|
+
* NEVER seeds — the caller for those events always passes
|
|
18
|
+
* `'inventory-discovered'` directly without consulting this helper.
|
|
19
|
+
* @param inventoryMode - Inventory-mode config from `CrawlerOptions.inventoryMode`, or `null` outside `--inventory`.
|
|
20
|
+
* @param pageUrlWithoutHashAndAuth - The page URL keyed by `withoutHashAndAuth` (auth credentials stripped, hash dropped).
|
|
21
|
+
* @returns The label to write to `pages.source`, or `undefined` for the DB default.
|
|
22
|
+
*/
|
|
23
|
+
export declare function derivePageSource(inventoryMode: InventoryMode | null, pageUrlWithoutHashAndAuth: string): PageSource | undefined;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decide which {@link PageSource} label a newly-scraped page row should carry.
|
|
3
|
+
*
|
|
4
|
+
* When the crawler is NOT in inventory mode (`inventoryMode === null`),
|
|
5
|
+
* returns `undefined` — the caller emits no `source` and the DB DEFAULT
|
|
6
|
+
* `'crawled'` ends up on the row. This keeps the normal crawl path
|
|
7
|
+
* untouched.
|
|
8
|
+
*
|
|
9
|
+
* When inventory mode is active, the URL is matched against
|
|
10
|
+
* `inventoryMode.seedUrls`. A hit means the URL came straight from the
|
|
11
|
+
* user-supplied list (`'inventory-seed'`); a miss means the URL was found
|
|
12
|
+
* by following links from a seed page (`'inventory-discovered'`).
|
|
13
|
+
*
|
|
14
|
+
* Sub-resources captured by puppeteer during inventory-mode rendering are
|
|
15
|
+
* NEVER seeds — the caller for those events always passes
|
|
16
|
+
* `'inventory-discovered'` directly without consulting this helper.
|
|
17
|
+
* @param inventoryMode - Inventory-mode config from `CrawlerOptions.inventoryMode`, or `null` outside `--inventory`.
|
|
18
|
+
* @param pageUrlWithoutHashAndAuth - The page URL keyed by `withoutHashAndAuth` (auth credentials stripped, hash dropped).
|
|
19
|
+
* @returns The label to write to `pages.source`, or `undefined` for the DB default.
|
|
20
|
+
*/
|
|
21
|
+
export function derivePageSource(inventoryMode, pageUrlWithoutHashAndAuth) {
|
|
22
|
+
if (inventoryMode === null) {
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
return inventoryMode.seedUrls.has(pageUrlWithoutHashAndAuth)
|
|
26
|
+
? 'inventory-seed'
|
|
27
|
+
: 'inventory-discovered';
|
|
28
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { PageSource } from '../archive/types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Decide which {@link PageSource} label a newly-captured sub-resource row
|
|
4
|
+
* (CSS / JS / image / fetch response) should carry, based on the lineage of
|
|
5
|
+
* the page that is currently being rendered.
|
|
6
|
+
*
|
|
7
|
+
* Sub-resources are NEVER themselves seeds — even when puppeteer is
|
|
8
|
+
* rendering a page that *is* an inventory seed, the assets it pulls in are
|
|
9
|
+
* downstream and must be labelled `'inventory-discovered'`. The seed label
|
|
10
|
+
* is reserved for URLs that were explicitly handed in by the user via the
|
|
11
|
+
* `--inventory` file. Likewise, when a sub-resource is captured during a
|
|
12
|
+
* render of an `'inventory-discovered'` page (a page reached transitively
|
|
13
|
+
* through the inventory chain), the asset is still inventory-discovered —
|
|
14
|
+
* not a new seed.
|
|
15
|
+
*
|
|
16
|
+
* Outside the inventory chain (parent is `'crawled'` or has no source
|
|
17
|
+
* record) this returns `undefined` so the caller emits no `source` and the
|
|
18
|
+
* DB DEFAULT (`'crawled'`) lands on the row, leaving the normal crawl path
|
|
19
|
+
* untouched.
|
|
20
|
+
*
|
|
21
|
+
* `parentSource` must reflect the MERGED source of the page being scraped:
|
|
22
|
+
* for an active `--inventory` session that comes from
|
|
23
|
+
* {@link import('./derive-page-source.js').derivePageSource}, for a
|
|
24
|
+
* `--resume` / `--retry-failed` session it comes from a DB lookup
|
|
25
|
+
* (`PageSourceLookup`) because `inventoryMode` is not persisted across
|
|
26
|
+
* sessions. The two-stage resolution is what keeps sub-resource labels
|
|
27
|
+
* correct on resume: even though the orchestrator no longer carries the
|
|
28
|
+
* inventory seed set in memory, the DB still records the parent's lineage
|
|
29
|
+
* and we propagate it forward.
|
|
30
|
+
* @param parentSource - Merged source of the page that is producing this sub-resource (or `undefined` for an unknown / `'crawled'` parent).
|
|
31
|
+
* @returns The label to write to `resources.source`, or `undefined` for the DB default.
|
|
32
|
+
*/
|
|
33
|
+
export declare function deriveResourceSource(parentSource: PageSource | undefined): PageSource | undefined;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decide which {@link PageSource} label a newly-captured sub-resource row
|
|
3
|
+
* (CSS / JS / image / fetch response) should carry, based on the lineage of
|
|
4
|
+
* the page that is currently being rendered.
|
|
5
|
+
*
|
|
6
|
+
* Sub-resources are NEVER themselves seeds — even when puppeteer is
|
|
7
|
+
* rendering a page that *is* an inventory seed, the assets it pulls in are
|
|
8
|
+
* downstream and must be labelled `'inventory-discovered'`. The seed label
|
|
9
|
+
* is reserved for URLs that were explicitly handed in by the user via the
|
|
10
|
+
* `--inventory` file. Likewise, when a sub-resource is captured during a
|
|
11
|
+
* render of an `'inventory-discovered'` page (a page reached transitively
|
|
12
|
+
* through the inventory chain), the asset is still inventory-discovered —
|
|
13
|
+
* not a new seed.
|
|
14
|
+
*
|
|
15
|
+
* Outside the inventory chain (parent is `'crawled'` or has no source
|
|
16
|
+
* record) this returns `undefined` so the caller emits no `source` and the
|
|
17
|
+
* DB DEFAULT (`'crawled'`) lands on the row, leaving the normal crawl path
|
|
18
|
+
* untouched.
|
|
19
|
+
*
|
|
20
|
+
* `parentSource` must reflect the MERGED source of the page being scraped:
|
|
21
|
+
* for an active `--inventory` session that comes from
|
|
22
|
+
* {@link import('./derive-page-source.js').derivePageSource}, for a
|
|
23
|
+
* `--resume` / `--retry-failed` session it comes from a DB lookup
|
|
24
|
+
* (`PageSourceLookup`) because `inventoryMode` is not persisted across
|
|
25
|
+
* sessions. The two-stage resolution is what keeps sub-resource labels
|
|
26
|
+
* correct on resume: even though the orchestrator no longer carries the
|
|
27
|
+
* inventory seed set in memory, the DB still records the parent's lineage
|
|
28
|
+
* and we propagate it forward.
|
|
29
|
+
* @param parentSource - Merged source of the page that is producing this sub-resource (or `undefined` for an unknown / `'crawled'` parent).
|
|
30
|
+
* @returns The label to write to `resources.source`, or `undefined` for the DB default.
|
|
31
|
+
*/
|
|
32
|
+
export function deriveResourceSource(parentSource) {
|
|
33
|
+
if (parentSource === 'inventory-seed' || parentSource === 'inventory-discovered') {
|
|
34
|
+
return 'inventory-discovered';
|
|
35
|
+
}
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ErrorKind } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* In-memory set of hostnames known to be unreachable due to DNS errors
|
|
4
|
+
* (e.g. `getaddrinfo ENOTFOUND`).
|
|
5
|
+
*
|
|
6
|
+
* Lives for one crawl session — cleared by {@link clearDnsBurnedHostCache} at
|
|
7
|
+
* the same four orchestrator sites as {@link destinationCache}. Hosts are
|
|
8
|
+
* marked in two ways:
|
|
9
|
+
*
|
|
10
|
+
* - **Session learning**: the `onGiveUp` callback of `#sendHeadRequest`
|
|
11
|
+
* classifies the final retry error and stores `'dns'` when the matcher
|
|
12
|
+
* fires. The next URL on the same hostname short-circuits before retry.
|
|
13
|
+
* - **Session preload**: re-open paths (`append` / `inventory` / `retryFailed`
|
|
14
|
+
* / `resume`) call {@link Archive.listDnsBurnedHostCandidates} and seed the
|
|
15
|
+
* map from `crawl_errors`, so previously-burned hosts cost zero retries on
|
|
16
|
+
* the next crawl.
|
|
17
|
+
*
|
|
18
|
+
* Keys are always `url.hostname.toLowerCase()` — WHATWG URL has already
|
|
19
|
+
* Punycoded IDNs and stripped the port, so no extra normalization is needed
|
|
20
|
+
* for IPv4 / IPv6 literals or international hostnames.
|
|
21
|
+
*
|
|
22
|
+
* The value records the originating {@link ErrorKind}. Only `'dns'` is set
|
|
23
|
+
* today; the union shape is preserved so future error classes
|
|
24
|
+
* (e.g. `tls`, `connection-refused`) can extend the same cache.
|
|
25
|
+
*/
|
|
26
|
+
export declare const dnsBurnedHostCache: Map<string, ErrorKind>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory set of hostnames known to be unreachable due to DNS errors
|
|
3
|
+
* (e.g. `getaddrinfo ENOTFOUND`).
|
|
4
|
+
*
|
|
5
|
+
* Lives for one crawl session — cleared by {@link clearDnsBurnedHostCache} at
|
|
6
|
+
* the same four orchestrator sites as {@link destinationCache}. Hosts are
|
|
7
|
+
* marked in two ways:
|
|
8
|
+
*
|
|
9
|
+
* - **Session learning**: the `onGiveUp` callback of `#sendHeadRequest`
|
|
10
|
+
* classifies the final retry error and stores `'dns'` when the matcher
|
|
11
|
+
* fires. The next URL on the same hostname short-circuits before retry.
|
|
12
|
+
* - **Session preload**: re-open paths (`append` / `inventory` / `retryFailed`
|
|
13
|
+
* / `resume`) call {@link Archive.listDnsBurnedHostCandidates} and seed the
|
|
14
|
+
* map from `crawl_errors`, so previously-burned hosts cost zero retries on
|
|
15
|
+
* the next crawl.
|
|
16
|
+
*
|
|
17
|
+
* Keys are always `url.hostname.toLowerCase()` — WHATWG URL has already
|
|
18
|
+
* Punycoded IDNs and stripped the port, so no extra normalization is needed
|
|
19
|
+
* for IPv4 / IPv6 literals or international hostnames.
|
|
20
|
+
*
|
|
21
|
+
* The value records the originating {@link ErrorKind}. Only `'dns'` is set
|
|
22
|
+
* today; the union shape is preserved so future error classes
|
|
23
|
+
* (e.g. `tls`, `connection-refused`) can extend the same cache.
|
|
24
|
+
*/
|
|
25
|
+
export const dnsBurnedHostCache = new Map();
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session-scoped counter for how many URL fetches were short-circuited by the
|
|
3
|
+
* {@link dnsBurnedHostCache}. The orchestrator reads this at crawl shutdown
|
|
4
|
+
* to emit a `[preload] Short-circuited N URL(s)` summary line, then
|
|
5
|
+
* {@link clearDnsBurnedHostCache} zeroes it for the next session.
|
|
6
|
+
*
|
|
7
|
+
* Exposed as a mutable object (not a plain `let`) so that the counter remains
|
|
8
|
+
* a single shared reference across crawler / orchestrator imports — `let`
|
|
9
|
+
* bindings cannot be mutated from another module.
|
|
10
|
+
*/
|
|
11
|
+
export declare const dnsBurnedHostShortCircuitCounter: {
|
|
12
|
+
count: number;
|
|
13
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session-scoped counter for how many URL fetches were short-circuited by the
|
|
3
|
+
* {@link dnsBurnedHostCache}. The orchestrator reads this at crawl shutdown
|
|
4
|
+
* to emit a `[preload] Short-circuited N URL(s)` summary line, then
|
|
5
|
+
* {@link clearDnsBurnedHostCache} zeroes it for the next session.
|
|
6
|
+
*
|
|
7
|
+
* Exposed as a mutable object (not a plain `let`) so that the counter remains
|
|
8
|
+
* a single shared reference across crawler / orchestrator imports — `let`
|
|
9
|
+
* bindings cannot be mutated from another module.
|
|
10
|
+
*/
|
|
11
|
+
export const dnsBurnedHostShortCircuitCounter = { count: 0 };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A buffered phase-error record awaiting emission as a `pageError` event.
|
|
3
|
+
*
|
|
4
|
+
* `phase` is the beholder phase name (typically `'retryExhausted'`) and
|
|
5
|
+
* `message` is the human-readable failure text.
|
|
6
|
+
*/
|
|
7
|
+
export interface BufferedPhaseError {
|
|
8
|
+
/** Scrape phase name. */
|
|
9
|
+
phase: string;
|
|
10
|
+
/** Human-readable failure message. */
|
|
11
|
+
message: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Emitter signature accepted by {@link drainPhaseErrors} for the `pageError`
|
|
15
|
+
* event. Declared structurally so the Crawler's typed event emitter can be
|
|
16
|
+
* adapted with a thin closure at the call site without leaking through here.
|
|
17
|
+
*/
|
|
18
|
+
export type DrainPhaseErrorsEmit = (payload: {
|
|
19
|
+
/** URL of the affected page. */
|
|
20
|
+
url: string;
|
|
21
|
+
/** Scrape phase name. */
|
|
22
|
+
phase: string;
|
|
23
|
+
/** Human-readable failure message. */
|
|
24
|
+
message: string;
|
|
25
|
+
/** Whether the URL is external to the crawl scope. */
|
|
26
|
+
isExternal: boolean;
|
|
27
|
+
}) => void;
|
|
28
|
+
/**
|
|
29
|
+
* Drains the buffered phase errors for a URL: removes the entry from
|
|
30
|
+
* `buffer` and invokes `emit` once per buffered record.
|
|
31
|
+
*
|
|
32
|
+
* WHY a standalone function: the Crawler buffers `retryExhausted` events
|
|
33
|
+
* keyed by `url.href` during scrapeStart, then flushes them as
|
|
34
|
+
* `pageError` events AFTER `page` / `externalPage` has been emitted so the
|
|
35
|
+
* orchestrator's WriteQueue serialises `setPage` before `insertPageError`.
|
|
36
|
+
* Extracting the drain step here makes the flush + delete contract
|
|
37
|
+
* directly unit-testable without spinning up a real Crawler.
|
|
38
|
+
*
|
|
39
|
+
* Idempotent: calling twice for the same `urlHref` is safe — the second
|
|
40
|
+
* call sees an empty buffer and is a no-op.
|
|
41
|
+
* @param buffer - The pending-phase-errors map, keyed by URL href.
|
|
42
|
+
* @param urlHref - URL whose buffered errors should be drained.
|
|
43
|
+
* @param isExternal - Whether the URL is external to the crawl scope.
|
|
44
|
+
* @param emit - Callback invoked once per buffered phase-error record.
|
|
45
|
+
* @returns The number of phase-error events emitted (0 when the buffer
|
|
46
|
+
* had no entry for `urlHref`).
|
|
47
|
+
*/
|
|
48
|
+
export declare function drainPhaseErrors(buffer: Map<string, BufferedPhaseError[]>, urlHref: string, isExternal: boolean, emit: DrainPhaseErrorsEmit): number;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drains the buffered phase errors for a URL: removes the entry from
|
|
3
|
+
* `buffer` and invokes `emit` once per buffered record.
|
|
4
|
+
*
|
|
5
|
+
* WHY a standalone function: the Crawler buffers `retryExhausted` events
|
|
6
|
+
* keyed by `url.href` during scrapeStart, then flushes them as
|
|
7
|
+
* `pageError` events AFTER `page` / `externalPage` has been emitted so the
|
|
8
|
+
* orchestrator's WriteQueue serialises `setPage` before `insertPageError`.
|
|
9
|
+
* Extracting the drain step here makes the flush + delete contract
|
|
10
|
+
* directly unit-testable without spinning up a real Crawler.
|
|
11
|
+
*
|
|
12
|
+
* Idempotent: calling twice for the same `urlHref` is safe — the second
|
|
13
|
+
* call sees an empty buffer and is a no-op.
|
|
14
|
+
* @param buffer - The pending-phase-errors map, keyed by URL href.
|
|
15
|
+
* @param urlHref - URL whose buffered errors should be drained.
|
|
16
|
+
* @param isExternal - Whether the URL is external to the crawl scope.
|
|
17
|
+
* @param emit - Callback invoked once per buffered phase-error record.
|
|
18
|
+
* @returns The number of phase-error events emitted (0 when the buffer
|
|
19
|
+
* had no entry for `urlHref`).
|
|
20
|
+
*/
|
|
21
|
+
export function drainPhaseErrors(buffer, urlHref, isExternal, emit) {
|
|
22
|
+
const errors = buffer.get(urlHref);
|
|
23
|
+
if (!errors || errors.length === 0)
|
|
24
|
+
return 0;
|
|
25
|
+
buffer.delete(urlHref);
|
|
26
|
+
for (const err of errors) {
|
|
27
|
+
emit({
|
|
28
|
+
url: urlHref,
|
|
29
|
+
phase: err.phase,
|
|
30
|
+
message: err.message,
|
|
31
|
+
isExternal,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
return errors.length;
|
|
35
|
+
}
|
|
@@ -20,19 +20,27 @@ export interface FetchDestinationParams {
|
|
|
20
20
|
};
|
|
21
21
|
/** User-Agent string to send with the request. */
|
|
22
22
|
readonly userAgent?: string;
|
|
23
|
+
/**
|
|
24
|
+
* Race timeout for the network request in milliseconds. Defaults to
|
|
25
|
+
* {@link DEFAULT_HEAD_TIMEOUT_MS} (10s). `Crawler.#sendHeadRequest` passes
|
|
26
|
+
* a longer value on later retry attempts so a slow-but-reachable server
|
|
27
|
+
* gets another chance before being given up on.
|
|
28
|
+
*/
|
|
29
|
+
readonly timeout?: number;
|
|
23
30
|
}
|
|
24
31
|
/**
|
|
25
32
|
* Fetches the destination metadata for a URL using an HTTP HEAD request (or GET as fallback).
|
|
26
33
|
*
|
|
27
34
|
* Results are cached in memory so that repeated calls for the same URL
|
|
28
|
-
* (without hash) return immediately. The request races against a
|
|
29
|
-
* timeout
|
|
35
|
+
* (without hash) return immediately. The request races against a configurable
|
|
36
|
+
* timeout (defaults to {@link DEFAULT_HEAD_TIMEOUT_MS}, 10 seconds); if the
|
|
37
|
+
* server does not respond in time, a {@link NetTimeoutError} is thrown.
|
|
30
38
|
*
|
|
31
39
|
* If the server returns 405 (Method Not Allowed), 501 (Not Implemented), or 503
|
|
32
40
|
* (Service Unavailable) for a HEAD request, the function automatically retries with GET.
|
|
33
|
-
* @param params - Parameters containing URL, external flag, method, options,
|
|
41
|
+
* @param params - Parameters containing URL, external flag, method, options, optional User-Agent, and optional timeout.
|
|
34
42
|
* @returns The page metadata obtained from the HTTP response.
|
|
35
|
-
* @throws {NetTimeoutError} If the request exceeds the
|
|
43
|
+
* @throws {NetTimeoutError} If the request exceeds the configured timeout.
|
|
36
44
|
* @throws {Error} If the HTTP request fails for any other reason.
|
|
37
45
|
*/
|
|
38
46
|
export declare function fetchDestination(params: FetchDestinationParams): Promise<PageData>;
|