@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
|
@@ -2,22 +2,26 @@ import { delay } from '@d-zero/shared/delay';
|
|
|
2
2
|
import redirects from 'follow-redirects';
|
|
3
3
|
import { destinationCache } from './destination-cache.js';
|
|
4
4
|
import NetTimeoutError from './net-timeout-error.js';
|
|
5
|
+
import { shouldGetFallbackOnHeadFailure } from './should-get-fallback-on-head-failure.js';
|
|
6
|
+
/** Default race timeout for the HEAD pre-flight, in milliseconds. */
|
|
7
|
+
const DEFAULT_HEAD_TIMEOUT_MS = 10 * 1000;
|
|
5
8
|
/**
|
|
6
9
|
* Fetches the destination metadata for a URL using an HTTP HEAD request (or GET as fallback).
|
|
7
10
|
*
|
|
8
11
|
* Results are cached in memory so that repeated calls for the same URL
|
|
9
|
-
* (without hash) return immediately. The request races against a
|
|
10
|
-
* timeout
|
|
12
|
+
* (without hash) return immediately. The request races against a configurable
|
|
13
|
+
* timeout (defaults to {@link DEFAULT_HEAD_TIMEOUT_MS}, 10 seconds); if the
|
|
14
|
+
* server does not respond in time, a {@link NetTimeoutError} is thrown.
|
|
11
15
|
*
|
|
12
16
|
* If the server returns 405 (Method Not Allowed), 501 (Not Implemented), or 503
|
|
13
17
|
* (Service Unavailable) for a HEAD request, the function automatically retries with GET.
|
|
14
|
-
* @param params - Parameters containing URL, external flag, method, options,
|
|
18
|
+
* @param params - Parameters containing URL, external flag, method, options, optional User-Agent, and optional timeout.
|
|
15
19
|
* @returns The page metadata obtained from the HTTP response.
|
|
16
|
-
* @throws {NetTimeoutError} If the request exceeds the
|
|
20
|
+
* @throws {NetTimeoutError} If the request exceeds the configured timeout.
|
|
17
21
|
* @throws {Error} If the HTTP request fails for any other reason.
|
|
18
22
|
*/
|
|
19
23
|
export async function fetchDestination(params) {
|
|
20
|
-
const { url, isExternal, method = 'HEAD', options, userAgent } = params;
|
|
24
|
+
const { url, isExternal, method = 'HEAD', options, userAgent, timeout } = params;
|
|
21
25
|
const titleBytesLimit = options?.titleBytesLimit;
|
|
22
26
|
const cacheKey = titleBytesLimit == null ? url.withoutHash : `${url.withoutHash}:title`;
|
|
23
27
|
if (destinationCache.has(cacheKey)) {
|
|
@@ -28,20 +32,65 @@ export async function fetchDestination(params) {
|
|
|
28
32
|
return cache;
|
|
29
33
|
}
|
|
30
34
|
const effectiveMethod = titleBytesLimit == null ? method : 'GET';
|
|
31
|
-
|
|
35
|
+
const raceTimeoutMs = timeout ?? DEFAULT_HEAD_TIMEOUT_MS;
|
|
36
|
+
// Race the fetch against the requested timeout. The losing timer is cleared
|
|
32
37
|
// explicitly so it never keeps the event loop alive after the race settles
|
|
33
38
|
// (a plain `delay()` in `Promise.race` would leak the timer until it fires).
|
|
34
39
|
let timeoutHandle;
|
|
35
40
|
const result = await Promise.race([
|
|
36
|
-
_fetchHead(url, isExternal, effectiveMethod, titleBytesLimit, userAgent).catch((error) =>
|
|
41
|
+
_fetchHead(url, isExternal, effectiveMethod, titleBytesLimit, userAgent, timeout).catch((error) => error instanceof Error ? error : new Error(String(error))),
|
|
37
42
|
new Promise((resolve) => {
|
|
38
|
-
timeoutHandle = setTimeout(() => resolve(new NetTimeoutError(url.href)),
|
|
43
|
+
timeoutHandle = setTimeout(() => resolve(new NetTimeoutError(url.href)), raceTimeoutMs);
|
|
39
44
|
}),
|
|
40
45
|
]).finally(() => {
|
|
41
46
|
if (timeoutHandle)
|
|
42
47
|
clearTimeout(timeoutHandle);
|
|
43
48
|
});
|
|
44
|
-
|
|
49
|
+
// HEAD failure fallback: a WAF / middlebox that silently drops HEAD will
|
|
50
|
+
// surface as NetTimeoutError / parse-error / connection-reset here even
|
|
51
|
+
// though the same URL serves a normal GET response. Try GET once (using
|
|
52
|
+
// the same timeout budget) before giving up on the URL. Only when
|
|
53
|
+
// `method === 'HEAD'` to avoid infinite recursion if the GET itself
|
|
54
|
+
// times out — at that point the server really is unreachable.
|
|
55
|
+
if (method === 'HEAD' &&
|
|
56
|
+
result instanceof Error &&
|
|
57
|
+
shouldGetFallbackOnHeadFailure(result)) {
|
|
58
|
+
try {
|
|
59
|
+
const getResult = await fetchDestination({
|
|
60
|
+
url,
|
|
61
|
+
isExternal,
|
|
62
|
+
method: 'GET',
|
|
63
|
+
userAgent,
|
|
64
|
+
timeout,
|
|
65
|
+
});
|
|
66
|
+
// GET succeeded — that is the canonical answer for this URL, so
|
|
67
|
+
// cache it under the HEAD cacheKey too (same key, since cacheKey
|
|
68
|
+
// only depends on URL + titleBytesLimit, not on method). The
|
|
69
|
+
// inner GET call already wrote to the cache under the same key,
|
|
70
|
+
// but a future caller hitting the HEAD path will find it there.
|
|
71
|
+
return getResult;
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// GET fallback failed too; fall through to surface the original
|
|
75
|
+
// HEAD failure so retry / classification / DNS-burned cache see
|
|
76
|
+
// the actual underlying cause.
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// Errors that are RECOVERABLE on retry — NetTimeoutError plus the kinds
|
|
80
|
+
// `shouldGetFallbackOnHeadFailure` already singles out as
|
|
81
|
+
// possibly-recoverable (parse-error, connection-reset) — are
|
|
82
|
+
// intentionally NOT cached. Caching a recoverable failure would freeze
|
|
83
|
+
// the first slow probe as the verdict for every later caller on the
|
|
84
|
+
// same host AND defeat `Crawler.#sendHeadRequest`'s
|
|
85
|
+
// HEAD_TIMEOUT_ESCALATION_MS (the 30s/60s retry would hit the cache and
|
|
86
|
+
// re-throw the stale 10s failure instead of getting the longer
|
|
87
|
+
// budget). DNS / TLS / refused / blocked are persistent within a crawl
|
|
88
|
+
// session so caching them is what keeps a doomed host from re-paying
|
|
89
|
+
// the network cost N times.
|
|
90
|
+
const isRecoverableError = result instanceof Error && shouldGetFallbackOnHeadFailure(result);
|
|
91
|
+
if (!isRecoverableError) {
|
|
92
|
+
destinationCache.set(cacheKey, result);
|
|
93
|
+
}
|
|
45
94
|
if (result instanceof Error) {
|
|
46
95
|
throw result;
|
|
47
96
|
}
|
|
@@ -58,17 +107,26 @@ export async function fetchDestination(params) {
|
|
|
58
107
|
* @param titleBytesLimit - When set, reads up to this many bytes from the response body
|
|
59
108
|
* to extract a `<title>` tag, then destroys the connection.
|
|
60
109
|
* @param userAgent - Optional User-Agent string to send with the request.
|
|
110
|
+
* @param timeout - Optional race timeout in ms, forwarded to GET fallback so the
|
|
111
|
+
* second pass keeps the same budget as the original HEAD attempt.
|
|
61
112
|
* @returns A promise resolving to {@link PageData} with response metadata.
|
|
62
113
|
*/
|
|
63
|
-
async function _fetchHead(url, isExternal, method, titleBytesLimit, userAgent) {
|
|
114
|
+
async function _fetchHead(url, isExternal, method, titleBytesLimit, userAgent, timeout) {
|
|
64
115
|
return new Promise((resolve, reject) => {
|
|
65
116
|
const hostHeader = url.port ? `${url.hostname}:${url.port}` : url.hostname;
|
|
117
|
+
// `trackRedirects` makes follow-redirects populate `res.redirects` with the
|
|
118
|
+
// chain of followed URLs. Without it that array stays empty and the
|
|
119
|
+
// pre-flight cannot tell where a URL lands — required for the redirect
|
|
120
|
+
// chain in `redirectPaths` and for the #73 convergence dedup, which decides
|
|
121
|
+
// whether a redirect destination was already rendered *before* launching
|
|
122
|
+
// the browser.
|
|
66
123
|
const request = {
|
|
67
124
|
protocol: url.protocol,
|
|
68
125
|
hostname: url.hostname,
|
|
69
126
|
port: url.port || undefined,
|
|
70
127
|
path: url.pathname,
|
|
71
128
|
method,
|
|
129
|
+
trackRedirects: true,
|
|
72
130
|
headers: {
|
|
73
131
|
host: hostHeader,
|
|
74
132
|
...(userAgent ? { 'User-Agent': userAgent } : {}),
|
|
@@ -92,7 +150,17 @@ async function _fetchHead(url, isExternal, method, titleBytesLimit, userAgent) {
|
|
|
92
150
|
let totalBytes = 0;
|
|
93
151
|
let settled = false;
|
|
94
152
|
const buildPageData = (title) => {
|
|
95
|
-
|
|
153
|
+
// `res.redirects` (populated by trackRedirects) ALWAYS starts with the
|
|
154
|
+
// originally requested URL, then each followed hop. We drop that first
|
|
155
|
+
// entry so `redirectPaths` keeps its established contract: empty when the
|
|
156
|
+
// URL did not redirect, and `[...intermediate, finalDest]` when it did
|
|
157
|
+
// (the original URL is NOT included — callers like `resolveRedirectChain`
|
|
158
|
+
// and `updatePage` re-add it). Keeping the original here would (a) make
|
|
159
|
+
// `redirectPaths` non-empty for every page, so a direct page looks like a
|
|
160
|
+
// self-redirect, and (b) leak the query-stripped request-target (the HEAD
|
|
161
|
+
// request uses `url.pathname`), collapsing query-distinguished pages.
|
|
162
|
+
// Redirect *targets* come from Location headers and keep their query.
|
|
163
|
+
const redirectPaths = res.redirects.map((r) => r.url).slice(1);
|
|
96
164
|
const _contentLength = Number.parseInt(res.headers['content-length'] || '');
|
|
97
165
|
const contentLength = Number.isFinite(_contentLength) ? _contentLength : null;
|
|
98
166
|
return {
|
|
@@ -105,7 +173,26 @@ async function _fetchHead(url, isExternal, method, titleBytesLimit, userAgent) {
|
|
|
105
173
|
contentType: res.headers['content-type']?.split(';')[0] || null,
|
|
106
174
|
contentLength,
|
|
107
175
|
responseHeaders: res.headers,
|
|
108
|
-
|
|
176
|
+
// beholder 3.0.0 made jsonLd / speculationRules / tags /
|
|
177
|
+
// others / originTrial required Meta fields. Even this
|
|
178
|
+
// HEAD-only fallback path must populate every slot so
|
|
179
|
+
// downstream insert/derive helpers iterate without crashing.
|
|
180
|
+
meta: {
|
|
181
|
+
title,
|
|
182
|
+
jsonLd: [],
|
|
183
|
+
speculationRules: [],
|
|
184
|
+
tags: { detected: {}, entries: [] },
|
|
185
|
+
others: {
|
|
186
|
+
meta: {},
|
|
187
|
+
property: {},
|
|
188
|
+
httpEquiv: {},
|
|
189
|
+
itemprop: {},
|
|
190
|
+
link: [],
|
|
191
|
+
script: [],
|
|
192
|
+
iframe: [],
|
|
193
|
+
},
|
|
194
|
+
originTrial: [],
|
|
195
|
+
},
|
|
109
196
|
imageList: [],
|
|
110
197
|
anchorList: [],
|
|
111
198
|
html: '',
|
|
@@ -118,11 +205,21 @@ async function _fetchHead(url, isExternal, method, titleBytesLimit, userAgent) {
|
|
|
118
205
|
let rep = buildPageData('');
|
|
119
206
|
if (rep.status === 405) {
|
|
120
207
|
if (method === 'GET') {
|
|
121
|
-
|
|
208
|
+
// GET fallback also returned 405 — the server really does
|
|
209
|
+
// reject both methods. Resolve with the PageData so the
|
|
210
|
+
// archive records `status: 405` instead of the `-1`
|
|
211
|
+
// sentinel a reject would land on (which would erase the
|
|
212
|
+
// only useful diagnostic the server gave us).
|
|
213
|
+
resolve(rep);
|
|
122
214
|
return;
|
|
123
215
|
}
|
|
124
216
|
try {
|
|
125
|
-
rep = await fetchDestination({
|
|
217
|
+
rep = await fetchDestination({
|
|
218
|
+
url,
|
|
219
|
+
isExternal,
|
|
220
|
+
method: 'GET',
|
|
221
|
+
timeout,
|
|
222
|
+
});
|
|
126
223
|
}
|
|
127
224
|
catch (error) {
|
|
128
225
|
reject(error);
|
|
@@ -131,12 +228,19 @@ async function _fetchHead(url, isExternal, method, titleBytesLimit, userAgent) {
|
|
|
131
228
|
}
|
|
132
229
|
if (rep.status === 501) {
|
|
133
230
|
if (method === 'GET') {
|
|
134
|
-
|
|
231
|
+
// GET fallback also returned 501 — preserve the status
|
|
232
|
+
// rather than dropping it into the `-1` bucket.
|
|
233
|
+
resolve(rep);
|
|
135
234
|
return;
|
|
136
235
|
}
|
|
137
236
|
await delay(5 * 1000);
|
|
138
237
|
try {
|
|
139
|
-
rep = await fetchDestination({
|
|
238
|
+
rep = await fetchDestination({
|
|
239
|
+
url,
|
|
240
|
+
isExternal,
|
|
241
|
+
method: 'GET',
|
|
242
|
+
timeout,
|
|
243
|
+
});
|
|
140
244
|
}
|
|
141
245
|
catch (error) {
|
|
142
246
|
reject(error);
|
|
@@ -145,12 +249,22 @@ async function _fetchHead(url, isExternal, method, titleBytesLimit, userAgent) {
|
|
|
145
249
|
}
|
|
146
250
|
if (rep.status === 503) {
|
|
147
251
|
if (method === 'GET') {
|
|
148
|
-
|
|
252
|
+
// GET fallback also returned 503 — preserve the status.
|
|
253
|
+
// A second-pass 5xx from a different method is the
|
|
254
|
+
// server's real answer, not a transient HEAD-only quirk,
|
|
255
|
+
// so the archive should remember it as 503 instead of
|
|
256
|
+
// the generic `-1` sentinel.
|
|
257
|
+
resolve(rep);
|
|
149
258
|
return;
|
|
150
259
|
}
|
|
151
260
|
await delay(5 * 1000);
|
|
152
261
|
try {
|
|
153
|
-
rep = await fetchDestination({
|
|
262
|
+
rep = await fetchDestination({
|
|
263
|
+
url,
|
|
264
|
+
isExternal,
|
|
265
|
+
method: 'GET',
|
|
266
|
+
timeout,
|
|
267
|
+
});
|
|
154
268
|
}
|
|
155
269
|
catch (error) {
|
|
156
270
|
reject(error);
|
|
@@ -12,22 +12,31 @@ interface FormatCrawlProgressParams {
|
|
|
12
12
|
readonly externalTotal: number;
|
|
13
13
|
/** Number of external URLs completed */
|
|
14
14
|
readonly externalDone: number;
|
|
15
|
+
/**
|
|
16
|
+
* Number of HTML pages actually rendered by the browser in the current session.
|
|
17
|
+
* Excludes HEAD-only resources, title-only metadata fetches, and skipped URLs.
|
|
18
|
+
*/
|
|
19
|
+
readonly pagesScraped: number;
|
|
15
20
|
/** Number of parallel workers */
|
|
16
21
|
readonly limit: number;
|
|
17
22
|
}
|
|
18
23
|
/**
|
|
19
24
|
* Formats the crawl progress header for the deal() progress display.
|
|
20
25
|
*
|
|
21
|
-
* Shows "done / found (remaining)" format instead of "done/total"
|
|
22
|
-
* to make it clearer that the total is expected to grow during crawling
|
|
26
|
+
* Shows "done(pages) / found URLs (remaining)" format instead of "done/total"
|
|
27
|
+
* to make it clearer that the total is expected to grow during crawling,
|
|
28
|
+
* that the counts are processed URLs (not resulting pages), and how many of
|
|
29
|
+
* those URLs were actually rendered by the browser as HTML pages.
|
|
30
|
+
* Counts are formatted with thousands separators (e.g. `1,234,567`).
|
|
23
31
|
* @param params - The crawl progress parameters.
|
|
24
32
|
* @param params.done - Number of URLs completed by the deal queue.
|
|
25
33
|
* @param params.total - Total number of URLs in the deal queue (including completed).
|
|
26
34
|
* @param params.resumeOffset - Offset from a previous resumed session.
|
|
27
35
|
* @param params.externalTotal - Number of external URLs discovered.
|
|
28
36
|
* @param params.externalDone - Number of external URLs completed.
|
|
37
|
+
* @param params.pagesScraped - Number of HTML pages rendered by the browser in this session.
|
|
29
38
|
* @param params.limit - Number of parallel workers.
|
|
30
39
|
* @returns The formatted progress string with ANSI color codes.
|
|
31
40
|
*/
|
|
32
|
-
export declare function formatCrawlProgress({ done, total, resumeOffset, externalTotal, externalDone, limit, }: FormatCrawlProgressParams): string;
|
|
41
|
+
export declare function formatCrawlProgress({ done, total, resumeOffset, externalTotal, externalDone, pagesScraped, limit, }: FormatCrawlProgressParams): string;
|
|
33
42
|
export {};
|
|
@@ -1,19 +1,27 @@
|
|
|
1
1
|
import c from 'ansi-colors';
|
|
2
|
+
/**
|
|
3
|
+
* Number formatter for thousands-separated count display (e.g. `1,234,567`).
|
|
4
|
+
*/
|
|
5
|
+
const countFormat = new Intl.NumberFormat('en-US');
|
|
2
6
|
/**
|
|
3
7
|
* Formats the crawl progress header for the deal() progress display.
|
|
4
8
|
*
|
|
5
|
-
* Shows "done / found (remaining)" format instead of "done/total"
|
|
6
|
-
* to make it clearer that the total is expected to grow during crawling
|
|
9
|
+
* Shows "done(pages) / found URLs (remaining)" format instead of "done/total"
|
|
10
|
+
* to make it clearer that the total is expected to grow during crawling,
|
|
11
|
+
* that the counts are processed URLs (not resulting pages), and how many of
|
|
12
|
+
* those URLs were actually rendered by the browser as HTML pages.
|
|
13
|
+
* Counts are formatted with thousands separators (e.g. `1,234,567`).
|
|
7
14
|
* @param params - The crawl progress parameters.
|
|
8
15
|
* @param params.done - Number of URLs completed by the deal queue.
|
|
9
16
|
* @param params.total - Total number of URLs in the deal queue (including completed).
|
|
10
17
|
* @param params.resumeOffset - Offset from a previous resumed session.
|
|
11
18
|
* @param params.externalTotal - Number of external URLs discovered.
|
|
12
19
|
* @param params.externalDone - Number of external URLs completed.
|
|
20
|
+
* @param params.pagesScraped - Number of HTML pages rendered by the browser in this session.
|
|
13
21
|
* @param params.limit - Number of parallel workers.
|
|
14
22
|
* @returns The formatted progress string with ANSI color codes.
|
|
15
23
|
*/
|
|
16
|
-
export function formatCrawlProgress({ done, total, resumeOffset, externalTotal, externalDone, limit, }) {
|
|
24
|
+
export function formatCrawlProgress({ done, total, resumeOffset, externalTotal, externalDone, pagesScraped, limit, }) {
|
|
17
25
|
const allDone = done + resumeOffset;
|
|
18
26
|
const allTotal = total + resumeOffset;
|
|
19
27
|
const internalDone = allDone - externalDone;
|
|
@@ -22,8 +30,8 @@ export function formatCrawlProgress({ done, total, resumeOffset, externalTotal,
|
|
|
22
30
|
const externalRemaining = externalTotal - externalDone;
|
|
23
31
|
const totalRemaining = internalRemaining + externalRemaining;
|
|
24
32
|
const pct = allTotal > 0 ? Math.round((allDone / allTotal) * 100) : 0;
|
|
25
|
-
return (c.bold(`Crawling: ${internalDone} done / ${internalTotal} found`) +
|
|
26
|
-
c.dim(` (+${externalDone}/${externalTotal} ext)`) +
|
|
27
|
-
c.bold(` (${pct}%) [${totalRemaining} remaining]`) +
|
|
33
|
+
return (c.bold(`Crawling: ${countFormat.format(internalDone)}(${countFormat.format(pagesScraped)}) done / ${countFormat.format(internalTotal)} found URLs`) +
|
|
34
|
+
c.dim(` (+${countFormat.format(externalDone)}/${countFormat.format(externalTotal)} ext)`) +
|
|
35
|
+
c.bold(` (${pct}%) [${countFormat.format(totalRemaining)} remaining]`) +
|
|
28
36
|
c.dim(` [${limit} parallel]`));
|
|
29
37
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ClosableBrowser } from './close-browser-safely.js';
|
|
2
|
+
/**
|
|
3
|
+
* Debug-style logger compatible with the `debug` package's printf-style API.
|
|
4
|
+
*
|
|
5
|
+
* Declared structurally so the function stays unit-testable with a `vi.fn()`
|
|
6
|
+
* stub and free of a `debug` import.
|
|
7
|
+
*/
|
|
8
|
+
export type BrowserCloseLogger = (
|
|
9
|
+
/** printf-style format string (e.g. `'%s'`, `'%O'`). */
|
|
10
|
+
formatter: string,
|
|
11
|
+
/** Arguments interpolated into the format string. */
|
|
12
|
+
...args: readonly unknown[]) => void;
|
|
13
|
+
/**
|
|
14
|
+
* Closes a Puppeteer browser used to scrape a single URL, recording any
|
|
15
|
+
* timeout fallback or unexpected cleanup error to the supplied logger.
|
|
16
|
+
*
|
|
17
|
+
* WHY a dedicated function: the prior inline `finally` in
|
|
18
|
+
* {@link Crawler.#launchBrowserAndScrape} mixed cleanup orchestration with
|
|
19
|
+
* production log formatting. Splitting it out makes the two observable
|
|
20
|
+
* branches (force-kill notice and unexpected-error notice) directly testable
|
|
21
|
+
* without spawning a real browser, and keeps the rule that a finally block
|
|
22
|
+
* never throws: any error from {@link closeBrowserSafely} is logged here,
|
|
23
|
+
* never re-thrown.
|
|
24
|
+
* @param browser - The browser to close.
|
|
25
|
+
* @param urlHref - URL string included in the log messages for diagnostic
|
|
26
|
+
* context.
|
|
27
|
+
* @param log - Logger used to record timeout and error events.
|
|
28
|
+
*/
|
|
29
|
+
export declare function handleBrowserClose(browser: ClosableBrowser, urlHref: string, log: BrowserCloseLogger): Promise<void>;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { closeBrowserSafely } from './close-browser-safely.js';
|
|
2
|
+
/**
|
|
3
|
+
* Closes a Puppeteer browser used to scrape a single URL, recording any
|
|
4
|
+
* timeout fallback or unexpected cleanup error to the supplied logger.
|
|
5
|
+
*
|
|
6
|
+
* WHY a dedicated function: the prior inline `finally` in
|
|
7
|
+
* {@link Crawler.#launchBrowserAndScrape} mixed cleanup orchestration with
|
|
8
|
+
* production log formatting. Splitting it out makes the two observable
|
|
9
|
+
* branches (force-kill notice and unexpected-error notice) directly testable
|
|
10
|
+
* without spawning a real browser, and keeps the rule that a finally block
|
|
11
|
+
* never throws: any error from {@link closeBrowserSafely} is logged here,
|
|
12
|
+
* never re-thrown.
|
|
13
|
+
* @param browser - The browser to close.
|
|
14
|
+
* @param urlHref - URL string included in the log messages for diagnostic
|
|
15
|
+
* context.
|
|
16
|
+
* @param log - Logger used to record timeout and error events.
|
|
17
|
+
*/
|
|
18
|
+
export async function handleBrowserClose(browser, urlHref, log) {
|
|
19
|
+
try {
|
|
20
|
+
const timedOut = await closeBrowserSafely(browser);
|
|
21
|
+
if (timedOut) {
|
|
22
|
+
log('Force-killed wedged Chromium browser for %s (close() timed out)', urlHref);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
log('closeBrowserSafely failed for %s: %O', urlHref, error);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Determine whether a Content-Type media type is HTML.
|
|
3
|
+
*
|
|
4
|
+
* MIME types are case-insensitive (RFC 2045), and values captured from
|
|
5
|
+
* Puppeteer responses preserve the server's original casing, so the
|
|
6
|
+
* comparison must normalize case — `text/HTML` is HTML. Surrounding
|
|
7
|
+
* whitespace (e.g. `text/html ` left over after parameter stripping)
|
|
8
|
+
* is also tolerated.
|
|
9
|
+
*
|
|
10
|
+
* This is the single source of truth for HTML detection — `Page.isPage()`
|
|
11
|
+
* and the link list delegate here so the classification never diverges
|
|
12
|
+
* between code paths.
|
|
13
|
+
* @param contentType - The media type portion of a Content-Type header
|
|
14
|
+
* (parameters already stripped), or `null` when unknown.
|
|
15
|
+
* @returns `true` when the media type is `text/html` in any letter case.
|
|
16
|
+
*/
|
|
17
|
+
export declare function isHtmlContentType(contentType: string | null): boolean;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Determine whether a Content-Type media type is HTML.
|
|
3
|
+
*
|
|
4
|
+
* MIME types are case-insensitive (RFC 2045), and values captured from
|
|
5
|
+
* Puppeteer responses preserve the server's original casing, so the
|
|
6
|
+
* comparison must normalize case — `text/HTML` is HTML. Surrounding
|
|
7
|
+
* whitespace (e.g. `text/html ` left over after parameter stripping)
|
|
8
|
+
* is also tolerated.
|
|
9
|
+
*
|
|
10
|
+
* This is the single source of truth for HTML detection — `Page.isPage()`
|
|
11
|
+
* and the link list delegate here so the classification never diverges
|
|
12
|
+
* between code paths.
|
|
13
|
+
* @param contentType - The media type portion of a Content-Type header
|
|
14
|
+
* (parameters already stripped), or `null` when unknown.
|
|
15
|
+
* @returns `true` when the media type is `text/html` in any letter case.
|
|
16
|
+
*/
|
|
17
|
+
export function isHtmlContentType(contentType) {
|
|
18
|
+
return contentType !== null && contentType.trim().toLowerCase() === 'text/html';
|
|
19
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decide whether a browser-scrape error message is the specific
|
|
3
|
+
* `Page.goto() returned null` shape that the JS-redirect rescue is
|
|
4
|
+
* designed to recover from.
|
|
5
|
+
*
|
|
6
|
+
* **Why this gate exists:** before the gate, the rescue fired on *any*
|
|
7
|
+
* thrown error from `scraper.scrapeStart` as long as `page.url()` happened
|
|
8
|
+
* to report a different http(s) URL. That made every browser failure
|
|
9
|
+
* (TLS, target-crashed, OOM, navigation timeout, …) that incidentally
|
|
10
|
+
* left the page on a follow-up URL look like a JS redirect, hiding the
|
|
11
|
+
* real failure mode and stamping a phantom `status = 301` on the source.
|
|
12
|
+
*
|
|
13
|
+
* The narrow trigger only fires on the upstream's exact sentinel —
|
|
14
|
+
* `Page.goto returned null` — which beholder's scraper throws *only*
|
|
15
|
+
* when puppeteer's `page.goto()` resolved to `null`. Substring match (not
|
|
16
|
+
* equality) so wrapped variants like `[Retried 3 times] The method
|
|
17
|
+
* Page.goto returned null` (which surface in `crawl_errors` after retry
|
|
18
|
+
* exhaustion at outer layers) still classify, even though the rescue
|
|
19
|
+
* sees the bare form. Case-insensitive on the marker so a future
|
|
20
|
+
* beholder bump that lowercases the message keeps working.
|
|
21
|
+
*
|
|
22
|
+
* The trigger keys off the message *string*, not the message-classifier
|
|
23
|
+
* `kind`, because the rescue runs *before* the kind decision: the kind
|
|
24
|
+
* classifier would already wash this into `protocol`, and `protocol`
|
|
25
|
+
* covers more than just goto-null (Target closed / Session closed /
|
|
26
|
+
* detached Frame …) — none of which leave puppeteer with a meaningful
|
|
27
|
+
* post-navigation URL to recover.
|
|
28
|
+
* @param message - The raw error message from
|
|
29
|
+
* `BrowserScrapeResult.error.message` (or any string that may carry
|
|
30
|
+
* the sentinel inside a wrapper). `null` / `undefined` returns `false`.
|
|
31
|
+
* @returns `true` iff the message carries the `Page.goto returned null`
|
|
32
|
+
* sentinel.
|
|
33
|
+
* @example
|
|
34
|
+
* ```ts
|
|
35
|
+
* isJsRedirectErrorShape('The method Page.goto returned null'); // → true
|
|
36
|
+
* isJsRedirectErrorShape('Navigation timeout of 60000 ms exceeded'); // → false
|
|
37
|
+
* isJsRedirectErrorShape(undefined); // → false
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export declare function isJsRedirectErrorShape(message: string | null | undefined): boolean;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sentinel string emitted by `@d-zero/beholder`'s scraper when
|
|
3
|
+
* `await page.goto(...)` resolves to `null`. Pinned here because the
|
|
4
|
+
* JS-redirect rescue in `Crawler.#scrapePage` keys off the exact text —
|
|
5
|
+
* an upstream rename would silently disable the rescue, but the
|
|
6
|
+
* spec on this helper would also break, surfacing the drift in CI.
|
|
7
|
+
*/
|
|
8
|
+
const PAGE_GOTO_NULL_MARKER = 'Page.goto returned null';
|
|
9
|
+
/**
|
|
10
|
+
* Decide whether a browser-scrape error message is the specific
|
|
11
|
+
* `Page.goto() returned null` shape that the JS-redirect rescue is
|
|
12
|
+
* designed to recover from.
|
|
13
|
+
*
|
|
14
|
+
* **Why this gate exists:** before the gate, the rescue fired on *any*
|
|
15
|
+
* thrown error from `scraper.scrapeStart` as long as `page.url()` happened
|
|
16
|
+
* to report a different http(s) URL. That made every browser failure
|
|
17
|
+
* (TLS, target-crashed, OOM, navigation timeout, …) that incidentally
|
|
18
|
+
* left the page on a follow-up URL look like a JS redirect, hiding the
|
|
19
|
+
* real failure mode and stamping a phantom `status = 301` on the source.
|
|
20
|
+
*
|
|
21
|
+
* The narrow trigger only fires on the upstream's exact sentinel —
|
|
22
|
+
* `Page.goto returned null` — which beholder's scraper throws *only*
|
|
23
|
+
* when puppeteer's `page.goto()` resolved to `null`. Substring match (not
|
|
24
|
+
* equality) so wrapped variants like `[Retried 3 times] The method
|
|
25
|
+
* Page.goto returned null` (which surface in `crawl_errors` after retry
|
|
26
|
+
* exhaustion at outer layers) still classify, even though the rescue
|
|
27
|
+
* sees the bare form. Case-insensitive on the marker so a future
|
|
28
|
+
* beholder bump that lowercases the message keeps working.
|
|
29
|
+
*
|
|
30
|
+
* The trigger keys off the message *string*, not the message-classifier
|
|
31
|
+
* `kind`, because the rescue runs *before* the kind decision: the kind
|
|
32
|
+
* classifier would already wash this into `protocol`, and `protocol`
|
|
33
|
+
* covers more than just goto-null (Target closed / Session closed /
|
|
34
|
+
* detached Frame …) — none of which leave puppeteer with a meaningful
|
|
35
|
+
* post-navigation URL to recover.
|
|
36
|
+
* @param message - The raw error message from
|
|
37
|
+
* `BrowserScrapeResult.error.message` (or any string that may carry
|
|
38
|
+
* the sentinel inside a wrapper). `null` / `undefined` returns `false`.
|
|
39
|
+
* @returns `true` iff the message carries the `Page.goto returned null`
|
|
40
|
+
* sentinel.
|
|
41
|
+
* @example
|
|
42
|
+
* ```ts
|
|
43
|
+
* isJsRedirectErrorShape('The method Page.goto returned null'); // → true
|
|
44
|
+
* isJsRedirectErrorShape('Navigation timeout of 60000 ms exceeded'); // → false
|
|
45
|
+
* isJsRedirectErrorShape(undefined); // → false
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
export function isJsRedirectErrorShape(message) {
|
|
49
|
+
if (typeof message !== 'string' || message === '') {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
return message.toLowerCase().includes(PAGE_GOTO_NULL_MARKER.toLowerCase());
|
|
53
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ExURL } from '@d-zero/shared/parse-url';
|
|
2
|
+
/**
|
|
3
|
+
* Heuristically decide whether a discovered URL is likely to resolve to an
|
|
4
|
+
* HTML page, based solely on the URL itself.
|
|
5
|
+
*
|
|
6
|
+
* WHY URL-only: this runs at enqueue time — before any HEAD/GET — so the actual
|
|
7
|
+
* `Content-Type` is unknown. The crawler uses the result to prioritise the
|
|
8
|
+
* dealer queue (likely-HTML URLs are `unshift`ed to the front so page crawling
|
|
9
|
+
* advances ahead of asset/document fetches), so a heuristic is acceptable: a
|
|
10
|
+
* misclassification only changes fetch order, never correctness.
|
|
11
|
+
*
|
|
12
|
+
* Classification rules:
|
|
13
|
+
* - Non-HTTP URLs (`mailto:`, `tel:`, …) are never HTML pages.
|
|
14
|
+
* - Extensionless / directory-style URLs (`/`, `/about/`), and bare trailing-dot
|
|
15
|
+
* URLs (`/index.`, whose `extname` is `"."`), are treated as HTML — these are
|
|
16
|
+
* the overwhelmingly common shape for navigable pages.
|
|
17
|
+
* - URLs whose extension is in {@link HTML_EXTENSIONS} are HTML; every other
|
|
18
|
+
* extension (`.jpg`, `.pdf`, `.css`, `.js`, …) is treated as non-HTML.
|
|
19
|
+
* @param url - The parsed URL to classify.
|
|
20
|
+
* @returns `true` when the URL is likely an HTML page.
|
|
21
|
+
*/
|
|
22
|
+
export declare function isLikelyHtmlUrl(url: ExURL): boolean;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File extensions (lowercased, leading dot included) that typically denote a
|
|
3
|
+
* document served as `text/html`. Both static pages (`.html`) and the common
|
|
4
|
+
* server-side template / handler extensions (`.php`, `.aspx`, `.jsp`, `.ashx`,
|
|
5
|
+
* `.jsf` …) are listed because they usually serve an HTML page.
|
|
6
|
+
*
|
|
7
|
+
* A few entries (`.cgi`, `.do`, `.action`) are ambiguous — they sometimes
|
|
8
|
+
* return JSON or binary — but are kept here because a misclassification only
|
|
9
|
+
* changes fetch order, never correctness (see {@link isLikelyHtmlUrl}).
|
|
10
|
+
*
|
|
11
|
+
* Keys keep the leading dot so they can be compared directly against
|
|
12
|
+
* `ExURL.extname` (Node `Path.extname` output) without stripping it.
|
|
13
|
+
*/
|
|
14
|
+
const HTML_EXTENSIONS = new Set([
|
|
15
|
+
'.html',
|
|
16
|
+
'.htm',
|
|
17
|
+
'.xhtml',
|
|
18
|
+
'.shtml',
|
|
19
|
+
'.mhtml',
|
|
20
|
+
'.php',
|
|
21
|
+
'.php3',
|
|
22
|
+
'.php4',
|
|
23
|
+
'.php5',
|
|
24
|
+
'.phtml',
|
|
25
|
+
'.asp',
|
|
26
|
+
'.aspx',
|
|
27
|
+
'.ashx',
|
|
28
|
+
'.jsp',
|
|
29
|
+
'.jspx',
|
|
30
|
+
'.jsf',
|
|
31
|
+
'.cfm',
|
|
32
|
+
'.cgi',
|
|
33
|
+
'.do',
|
|
34
|
+
'.action',
|
|
35
|
+
]);
|
|
36
|
+
/**
|
|
37
|
+
* Heuristically decide whether a discovered URL is likely to resolve to an
|
|
38
|
+
* HTML page, based solely on the URL itself.
|
|
39
|
+
*
|
|
40
|
+
* WHY URL-only: this runs at enqueue time — before any HEAD/GET — so the actual
|
|
41
|
+
* `Content-Type` is unknown. The crawler uses the result to prioritise the
|
|
42
|
+
* dealer queue (likely-HTML URLs are `unshift`ed to the front so page crawling
|
|
43
|
+
* advances ahead of asset/document fetches), so a heuristic is acceptable: a
|
|
44
|
+
* misclassification only changes fetch order, never correctness.
|
|
45
|
+
*
|
|
46
|
+
* Classification rules:
|
|
47
|
+
* - Non-HTTP URLs (`mailto:`, `tel:`, …) are never HTML pages.
|
|
48
|
+
* - Extensionless / directory-style URLs (`/`, `/about/`), and bare trailing-dot
|
|
49
|
+
* URLs (`/index.`, whose `extname` is `"."`), are treated as HTML — these are
|
|
50
|
+
* the overwhelmingly common shape for navigable pages.
|
|
51
|
+
* - URLs whose extension is in {@link HTML_EXTENSIONS} are HTML; every other
|
|
52
|
+
* extension (`.jpg`, `.pdf`, `.css`, `.js`, …) is treated as non-HTML.
|
|
53
|
+
* @param url - The parsed URL to classify.
|
|
54
|
+
* @returns `true` when the URL is likely an HTML page.
|
|
55
|
+
*/
|
|
56
|
+
export function isLikelyHtmlUrl(url) {
|
|
57
|
+
if (!url.isHTTP) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
const extname = url.extname;
|
|
61
|
+
if (!extname || extname === '.') {
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
return HTML_EXTENSIONS.has(extname.toLowerCase());
|
|
65
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decide whether a failed HEAD/GET pre-flight error message warrants one
|
|
3
|
+
* puppeteer fallback attempt. Pure and deterministic — the same message
|
|
4
|
+
* always gives the same answer, so the decision can be unit-tested without
|
|
5
|
+
* spinning up a browser.
|
|
6
|
+
* @param message - The pre-flight error message (typically the last rejected
|
|
7
|
+
* `retryCall` attempt).
|
|
8
|
+
* @returns `true` when puppeteer should be tried once, `false` to give up.
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* isPuppeteerFallbackCandidate('Timeout: https://slow.example.org/'); // true
|
|
12
|
+
* isPuppeteerFallbackCandidate('getaddrinfo ENOTFOUND host.invalid'); // false
|
|
13
|
+
* isPuppeteerFallbackCandidate('net::ERR_CERT_DATE_INVALID'); // false
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export declare function isPuppeteerFallbackCandidate(message: string): boolean;
|