@nitpicker/crawler 0.15.0 → 0.16.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.d.ts +39 -1
- package/lib/archive/archive.js +49 -0
- package/lib/archive/create-adjunct-tables.d.ts +3 -0
- package/lib/archive/create-adjunct-tables.js +42 -0
- package/lib/archive/database.d.ts +31 -1
- package/lib/archive/database.js +42 -0
- package/lib/archive/db-ops/dedupe-cap/accumulate-dedupe-cap-rejected-count.d.ts +18 -0
- package/lib/archive/db-ops/dedupe-cap/accumulate-dedupe-cap-rejected-count.js +23 -0
- package/lib/archive/db-ops/dedupe-cap/finalize-dedupe-cap-event.d.ts +12 -0
- package/lib/archive/db-ops/dedupe-cap/finalize-dedupe-cap-event.js +15 -0
- package/lib/archive/db-ops/dedupe-cap/insert-dedupe-cap-event.d.ts +14 -0
- package/lib/archive/db-ops/dedupe-cap/insert-dedupe-cap-event.js +30 -0
- package/lib/archive/db-ops/dedupe-cap/list-dedupe-cap-shape-keys.d.ts +21 -0
- package/lib/archive/db-ops/dedupe-cap/list-dedupe-cap-shape-keys.js +27 -0
- package/lib/archive/types.d.ts +13 -0
- package/lib/classify-error-kind.d.ts +1 -0
- package/lib/classify-error-kind.js +14 -0
- package/lib/crawler/assert-chrome-installed.d.ts +24 -0
- package/lib/crawler/assert-chrome-installed.js +43 -0
- package/lib/crawler/crawler.d.ts +12 -0
- package/lib/crawler/crawler.js +239 -29
- package/lib/crawler/decode-auth-credential.d.ts +29 -0
- package/lib/crawler/decode-auth-credential.js +39 -0
- package/lib/crawler/dedupe/compute-meta-signature.d.ts +30 -0
- package/lib/crawler/dedupe/compute-meta-signature.js +0 -0
- package/lib/crawler/dedupe/compute-shape-key.d.ts +37 -0
- package/lib/crawler/dedupe/compute-shape-key.js +56 -0
- package/lib/crawler/dedupe/dedupe-cap-tracker.d.ts +84 -0
- package/lib/crawler/dedupe/dedupe-cap-tracker.js +185 -0
- package/lib/crawler/dedupe/is-predicted-content-duplicate.d.ts +24 -0
- package/lib/crawler/dedupe/is-predicted-content-duplicate.js +26 -0
- package/lib/crawler/dedupe/is-shape-capped.d.ts +10 -0
- package/lib/crawler/dedupe/is-shape-capped.js +12 -0
- package/lib/crawler/dedupe/resolve-og-url-mismatch.d.ts +31 -0
- package/lib/crawler/dedupe/resolve-og-url-mismatch.js +40 -0
- package/lib/crawler/dedupe/types.d.ts +42 -0
- package/lib/crawler/dedupe/types.js +1 -0
- package/lib/crawler/fetch-destination.js +14 -2
- package/lib/crawler/generate-predicted-urls.d.ts +12 -0
- package/lib/crawler/generate-predicted-urls.js +33 -2
- package/lib/crawler/is-puppeteer-fallback-candidate.js +3 -0
- package/lib/crawler/types.d.ts +38 -0
- package/lib/crawler-orchestrator.d.ts +12 -0
- package/lib/crawler-orchestrator.js +106 -1
- package/lib/crawler.d.ts +1 -0
- package/lib/crawler.js +1 -0
- package/lib/permanent-error-kinds.d.ts +9 -4
- package/lib/permanent-error-kinds.js +10 -4
- package/lib/types.d.ts +2 -1
- package/package.json +2 -2
package/lib/crawler/types.d.ts
CHANGED
|
@@ -190,6 +190,29 @@ export interface CrawlerOptions extends Required<Pick<ParseURLOptions, 'disableQ
|
|
|
190
190
|
* without touching the real network.
|
|
191
191
|
*/
|
|
192
192
|
networkProbe: NetworkProbe | null;
|
|
193
|
+
/**
|
|
194
|
+
* Same-cluster soft-cap threshold (`--dedupe-cap`), or `null` to disable
|
|
195
|
+
* the feature entirely (the default). When set, {@link Crawler} stops
|
|
196
|
+
* enqueueing newly-discovered URLs whose shape (see `computeShapeKey`)
|
|
197
|
+
* has accumulated this many matching-signature observations (see
|
|
198
|
+
* `DedupeCapTracker`).
|
|
199
|
+
*/
|
|
200
|
+
dedupeCap: number | null;
|
|
201
|
+
/**
|
|
202
|
+
* Hard cap on the number of distinct URL shapes the same-cluster soft
|
|
203
|
+
* cap tracks at once (`--dedupe-map-cap`); the least-recently-touched
|
|
204
|
+
* shape is evicted beyond this. Only relevant when {@link dedupeCap} is
|
|
205
|
+
* non-null.
|
|
206
|
+
*/
|
|
207
|
+
dedupeMapCap: number;
|
|
208
|
+
/**
|
|
209
|
+
* Shape keys already confirmed capped in a prior session
|
|
210
|
+
* (persisted as `dedupe_cap_events.shape_key`), seeded into the
|
|
211
|
+
* tracker's sticky set so `--resume` / `--append` / `--retry-failed` /
|
|
212
|
+
* `--inventory` do not re-admit a trap this crawl already paid the cost
|
|
213
|
+
* of discovering once. Ignored when {@link dedupeCap} is `null`.
|
|
214
|
+
*/
|
|
215
|
+
preloadedStickyShapeKeys: readonly string[];
|
|
193
216
|
}
|
|
194
217
|
/**
|
|
195
218
|
* Inventory-mode runtime configuration. Passed from
|
|
@@ -436,6 +459,21 @@ export interface CrawlerEventTypes {
|
|
|
436
459
|
/** Epoch ms the recovery probe first succeeded. */
|
|
437
460
|
endedAt: number;
|
|
438
461
|
};
|
|
462
|
+
/**
|
|
463
|
+
* Emitted the instant the opt-in same-cluster soft cap
|
|
464
|
+
* ({@link CrawlerOptions.dedupeCap}) confirms a URL shape as a trap (see
|
|
465
|
+
* `DedupeCapTracker`). The orchestrator persists this via
|
|
466
|
+
* `Archive.insertDedupeCapEvent` and must remember the returned row id so
|
|
467
|
+
* `rejected_count` can be finalized once at `crawlEnd` (`Crawler` itself
|
|
468
|
+
* never touches the archive).
|
|
469
|
+
*/
|
|
470
|
+
dedupeCap: {
|
|
471
|
+
shapeKey: string;
|
|
472
|
+
sampleUrl: string;
|
|
473
|
+
bodyHash: Buffer;
|
|
474
|
+
effectiveThreshold: number;
|
|
475
|
+
observedCount: number;
|
|
476
|
+
};
|
|
439
477
|
}
|
|
440
478
|
/**
|
|
441
479
|
* Tunables for `NetworkOutageDetector`.
|
|
@@ -69,6 +69,18 @@ interface CrawlConfig extends Config {
|
|
|
69
69
|
* `options` so an E2E test can inject it via the public API.
|
|
70
70
|
*/
|
|
71
71
|
networkProbe: NetworkProbe | null;
|
|
72
|
+
/** See {@link CrawlerOptions.dedupeCap}. `null`/omitted disables the feature. */
|
|
73
|
+
dedupeCap: number | null;
|
|
74
|
+
/** See {@link CrawlerOptions.dedupeMapCap}. Omitted falls through to `Crawler`'s own default. */
|
|
75
|
+
dedupeMapCap: number;
|
|
76
|
+
/**
|
|
77
|
+
* See {@link CrawlerOptions.preloadedStickyShapeKeys}. Set internally by
|
|
78
|
+
* the four resuming-session static methods
|
|
79
|
+
* (`append`/`inventory`/`retryFailed`/`resume`), each independently
|
|
80
|
+
* calling `archive.listDedupeCapShapeKeys()`; not part of the public
|
|
81
|
+
* options a caller of those methods passes directly.
|
|
82
|
+
*/
|
|
83
|
+
preloadedStickyShapeKeys: readonly string[];
|
|
72
84
|
}
|
|
73
85
|
/**
|
|
74
86
|
* Callback invoked after the CrawlerOrchestrator instance is fully initialized
|
|
@@ -64,6 +64,15 @@ export class CrawlerOrchestrator extends EventEmitter {
|
|
|
64
64
|
#archive;
|
|
65
65
|
/** The crawler engine that discovers and scrapes pages. */
|
|
66
66
|
#crawler;
|
|
67
|
+
/**
|
|
68
|
+
* `dedupe_cap_events.id` for each shape confirmed capped this session, so
|
|
69
|
+
* `crawlEnd` can look up the right row to finalize with
|
|
70
|
+
* `Crawler#getDedupeCapRejections`'s counts. A `Map` (not a single
|
|
71
|
+
* scalar like {@link #openNetworkOutageId}) because, unlike a network
|
|
72
|
+
* outage, more than one shape can be capped simultaneously within one
|
|
73
|
+
* crawl.
|
|
74
|
+
*/
|
|
75
|
+
#dedupeCapEventIds = new Map();
|
|
67
76
|
/** Whether the crawl was started from a pre-defined URL list (non-recursive mode). */
|
|
68
77
|
#fromList;
|
|
69
78
|
/**
|
|
@@ -158,6 +167,13 @@ export class CrawlerOrchestrator extends EventEmitter {
|
|
|
158
167
|
networkOutageHostThreshold: options?.networkOutageHostThreshold,
|
|
159
168
|
networkOutageProbeIntervalMs: options?.networkOutageProbeIntervalMs,
|
|
160
169
|
networkProbe: options?.networkProbe ?? null,
|
|
170
|
+
dedupeCap: options?.dedupeCap ?? null,
|
|
171
|
+
dedupeMapCap: options?.dedupeMapCap,
|
|
172
|
+
// Only the four resuming-session static methods
|
|
173
|
+
// (`append`/`inventory`/`retryFailed`/`resume`) pass this — a
|
|
174
|
+
// fresh `crawling()` has no archive history to seed from (see
|
|
175
|
+
// `CrawlConfig.preloadedStickyShapeKeys`'s JSDoc).
|
|
176
|
+
preloadedStickyShapeKeys: options?.preloadedStickyShapeKeys ?? [],
|
|
161
177
|
});
|
|
162
178
|
}
|
|
163
179
|
/**
|
|
@@ -298,6 +314,23 @@ export class CrawlerOrchestrator extends EventEmitter {
|
|
|
298
314
|
})
|
|
299
315
|
.catch((error) => reject(error));
|
|
300
316
|
});
|
|
317
|
+
this.#crawler.on('dedupeCap', ({ shapeKey, sampleUrl, bodyHash, effectiveThreshold, observedCount }) => {
|
|
318
|
+
crawlerLog('Dedupe cap reached: shapeKey=%s effectiveThreshold=%d observedCount=%d', shapeKey, effectiveThreshold, observedCount);
|
|
319
|
+
console.error(`[dedupe-cap] same-cluster trap confirmed: ${shapeKey} (sample: ${sampleUrl})`);
|
|
320
|
+
writeQueue
|
|
321
|
+
.enqueue(async () => {
|
|
322
|
+
const id = await this.#archive.insertDedupeCapEvent({
|
|
323
|
+
shapeKey,
|
|
324
|
+
sampleUrl,
|
|
325
|
+
bodyHash,
|
|
326
|
+
effectiveThreshold,
|
|
327
|
+
observedCount,
|
|
328
|
+
detectedAt: Date.now(),
|
|
329
|
+
});
|
|
330
|
+
this.#dedupeCapEventIds.set(shapeKey, id);
|
|
331
|
+
})
|
|
332
|
+
.catch((error) => reject(error));
|
|
333
|
+
});
|
|
301
334
|
this.#crawler.on('response', ({ resource, source }) => {
|
|
302
335
|
writeQueue
|
|
303
336
|
.enqueue(() => this.#archive.setResources(resource, source))
|
|
@@ -314,6 +347,51 @@ export class CrawlerOrchestrator extends EventEmitter {
|
|
|
314
347
|
.catch((error) => reject(error));
|
|
315
348
|
});
|
|
316
349
|
this.#crawler.on('crawlEnd', () => {
|
|
350
|
+
// Deferred to INSIDE a queued closure, not read synchronously
|
|
351
|
+
// here, for the same reason `networkOutageRecovered`'s handler
|
|
352
|
+
// defers reading `#openNetworkOutageId`: a `dedupeCap` event's
|
|
353
|
+
// INSERT closure may still be queued (not yet executed) at the
|
|
354
|
+
// instant `crawlEnd` fires. `WriteQueue` runs enqueued
|
|
355
|
+
// operations in submission order, so by the time THIS closure
|
|
356
|
+
// executes, every earlier-queued `dedupeCap` INSERT has
|
|
357
|
+
// already completed and `#dedupeCapEventIds` is reliably
|
|
358
|
+
// populated.
|
|
359
|
+
writeQueue
|
|
360
|
+
.enqueue(async () => {
|
|
361
|
+
const rejections = this.#crawler.getDedupeCapRejections();
|
|
362
|
+
// Finalize every shape capped THIS session (has an id in
|
|
363
|
+
// `#dedupeCapEventIds`), not just the ones with a nonzero
|
|
364
|
+
// rejection count — a shape that capped near the end of the
|
|
365
|
+
// crawl (or whose remaining anchors all happened to be
|
|
366
|
+
// discovered before it capped) never enters `rejections` at
|
|
367
|
+
// all, and would otherwise stay `rejected_count: NULL` forever
|
|
368
|
+
// despite the crawl completing normally, corrupting the "NULL
|
|
369
|
+
// means the crawl never reached crawlEnd" contract
|
|
370
|
+
// `list-dedupe-cap-events.ts` documents.
|
|
371
|
+
const shapeKeysToFinalize = new Set([
|
|
372
|
+
...this.#dedupeCapEventIds.keys(),
|
|
373
|
+
...rejections.keys(),
|
|
374
|
+
]);
|
|
375
|
+
await Promise.all([...shapeKeysToFinalize].map((shapeKey) => {
|
|
376
|
+
const rejectedCount = rejections.get(shapeKey) ?? 0;
|
|
377
|
+
const id = this.#dedupeCapEventIds.get(shapeKey);
|
|
378
|
+
// A shape capped THIS session has an id here (the
|
|
379
|
+
// `dedupeCap` event always enqueues an INSERT before any
|
|
380
|
+
// rejection for that shape can be counted) and is
|
|
381
|
+
// finalized once via its row id. A shape with no id was
|
|
382
|
+
// never observed this session at all — it was preloaded
|
|
383
|
+
// into `DedupeCapTracker`'s sticky set from an EARLIER
|
|
384
|
+
// session's `dedupe_cap_events` row (see
|
|
385
|
+
// `CrawlConfig.preloadedStickyShapeKeys`'s JSDoc), so gate
|
|
386
|
+
// rejections still accumulate for it but no new row (and
|
|
387
|
+
// thus no id) is ever created. That earlier row's count is
|
|
388
|
+
// accumulated onto by shape_key instead of overwritten.
|
|
389
|
+
return id === undefined
|
|
390
|
+
? this.#archive.accumulateDedupeCapRejectedCount(shapeKey, rejectedCount)
|
|
391
|
+
: this.#archive.finalizeDedupeCapEvent(id, rejectedCount);
|
|
392
|
+
}));
|
|
393
|
+
})
|
|
394
|
+
.catch((error) => reject(error));
|
|
317
395
|
writeQueue
|
|
318
396
|
.drain()
|
|
319
397
|
.then(() => resolve())
|
|
@@ -521,9 +599,14 @@ export class CrawlerOrchestrator extends EventEmitter {
|
|
|
521
599
|
scopeMap.set(parsed.hostname, [...existing, parsed]);
|
|
522
600
|
}
|
|
523
601
|
await archive.repromoteExternalPages(scopeMap, archived);
|
|
602
|
+
// Seed the sticky set from prior sessions' confirmed traps so
|
|
603
|
+
// `--append` does not pay the cost of re-discovering them (see
|
|
604
|
+
// `DedupeCapTracker`'s constructor JSDoc).
|
|
605
|
+
const preloadedStickyShapeKeys = await archive.listDedupeCapShapeKeys();
|
|
524
606
|
const orchestrator = new CrawlerOrchestrator(archive, {
|
|
525
607
|
...mergedConfig,
|
|
526
608
|
roots: mergedRoots,
|
|
609
|
+
preloadedStickyShapeKeys,
|
|
527
610
|
});
|
|
528
611
|
const { scraped, pending } = await archive.getCrawlingState();
|
|
529
612
|
const resources = await archive.getResourceUrlList();
|
|
@@ -856,6 +939,16 @@ export class CrawlerOrchestrator extends EventEmitter {
|
|
|
856
939
|
inventoryMode: { seedUrls: seedSet },
|
|
857
940
|
};
|
|
858
941
|
if (htmlSeeds.length > 0) {
|
|
942
|
+
// Seed the sticky set from prior sessions' confirmed traps
|
|
943
|
+
// so `--inventory` does not pay the cost of
|
|
944
|
+
// re-discovering them (see `DedupeCapTracker`'s
|
|
945
|
+
// constructor JSDoc). Scoped to this branch only,
|
|
946
|
+
// matching `#preloadDnsBurnedHostCache`'s scoping below —
|
|
947
|
+
// the fallback (non-HTML-only) branch never calls
|
|
948
|
+
// `orchestrator.crawling(...)`, so the tracker is never
|
|
949
|
+
// consulted there.
|
|
950
|
+
orchestratorOptions.preloadedStickyShapeKeys =
|
|
951
|
+
await archive.listDedupeCapShapeKeys();
|
|
859
952
|
const orchestrator = new CrawlerOrchestrator(archive, orchestratorOptions);
|
|
860
953
|
// Re-read pending *after* the pre-insert so the strict-
|
|
861
954
|
// pending set includes the freshly inserted
|
|
@@ -1007,7 +1100,14 @@ export class CrawlerOrchestrator extends EventEmitter {
|
|
|
1007
1100
|
log('Start retrying failed pages');
|
|
1008
1101
|
log('Archive %s', absFilePath);
|
|
1009
1102
|
log('Reset %d failed page(s)', resetUrls.length);
|
|
1010
|
-
|
|
1103
|
+
// Seed the sticky set from prior sessions' confirmed traps so
|
|
1104
|
+
// `--retry-failed` does not pay the cost of re-discovering
|
|
1105
|
+
// them (see `DedupeCapTracker`'s constructor JSDoc).
|
|
1106
|
+
const preloadedStickyShapeKeys = await archive.listDedupeCapShapeKeys();
|
|
1107
|
+
const orchestrator = new CrawlerOrchestrator(archive, {
|
|
1108
|
+
...config,
|
|
1109
|
+
preloadedStickyShapeKeys,
|
|
1110
|
+
});
|
|
1011
1111
|
const { scraped, pending } = await archive.getCrawlingState();
|
|
1012
1112
|
const resources = await archive.getResourceUrlList();
|
|
1013
1113
|
const pagesScrapedOffset = await archive.getScrapedHtmlPageCount();
|
|
@@ -1056,9 +1156,14 @@ export class CrawlerOrchestrator extends EventEmitter {
|
|
|
1056
1156
|
static async resume(stubPath, options, initializedCallback) {
|
|
1057
1157
|
const archive = await Archive.resume(stubPath);
|
|
1058
1158
|
const archivedConfig = await archive.getConfig();
|
|
1159
|
+
// Seed the sticky set from prior sessions' confirmed traps so
|
|
1160
|
+
// `--resume` does not pay the cost of re-discovering them (see
|
|
1161
|
+
// `DedupeCapTracker`'s constructor JSDoc).
|
|
1162
|
+
const preloadedStickyShapeKeys = await archive.listDedupeCapShapeKeys();
|
|
1059
1163
|
const config = {
|
|
1060
1164
|
...archivedConfig,
|
|
1061
1165
|
...cleanObject(options),
|
|
1166
|
+
preloadedStickyShapeKeys,
|
|
1062
1167
|
};
|
|
1063
1168
|
const orchestrator = new CrawlerOrchestrator(archive, config);
|
|
1064
1169
|
const _url = await archive.getUrl();
|
package/lib/crawler.d.ts
CHANGED
|
@@ -46,6 +46,7 @@ export type { NetworkProbe } from './crawler/probe-network.js';
|
|
|
46
46
|
export { probeNetwork } from './crawler/probe-network.js';
|
|
47
47
|
export { computeOutageClampTimestamp } from './archive/db-ops/outages/compute-outage-clamp-timestamp.js';
|
|
48
48
|
export { chooseProbeHost } from './crawler/choose-probe-host.js';
|
|
49
|
+
export { assertChromeIsInstalled } from './crawler/assert-chrome-installed.js';
|
|
49
50
|
export { computeFileSha256 } from './utils/compute-file-sha256.js';
|
|
50
51
|
export { populateEntityTables } from './archive/populate-entity-tables/populate-entities.js';
|
|
51
52
|
export type { PageDomPathResolver } from './archive/populate-entity-tables/populate-image-items.js';
|
package/lib/crawler.js
CHANGED
|
@@ -43,6 +43,7 @@ export { default as NetworkGate } from './crawler/network-gate.js';
|
|
|
43
43
|
export { probeNetwork } from './crawler/probe-network.js';
|
|
44
44
|
export { computeOutageClampTimestamp } from './archive/db-ops/outages/compute-outage-clamp-timestamp.js';
|
|
45
45
|
export { chooseProbeHost } from './crawler/choose-probe-host.js';
|
|
46
|
+
export { assertChromeIsInstalled } from './crawler/assert-chrome-installed.js';
|
|
46
47
|
export { computeFileSha256 } from './utils/compute-file-sha256.js';
|
|
47
48
|
// 0.13 ref-table population (issue #191, epic #103). Exposed as the
|
|
48
49
|
// public seam that the migration script (`scripts/migrate-to-0.13.mjs`)
|
|
@@ -7,11 +7,12 @@ import type { ErrorKind } from './types.js';
|
|
|
7
7
|
* Used by `resetFailedPages` to exclude pages whose latest recorded error
|
|
8
8
|
* falls in this set, so `--retry-failed` actually converges: without the
|
|
9
9
|
* exclusion, NXDOMAIN / TLS mismatch / `ERR_BLOCKED_BY_CLIENT` /
|
|
10
|
-
* `ECONNREFUSED` / HTTP parse-error pages would be reset to
|
|
11
|
-
* iteration, the crawler would re-attempt them, they would
|
|
12
|
-
* same way, and the retry-target count would stay constant
|
|
10
|
+
* `ECONNREFUSED` / HTTP parse-error / redirect-loop pages would be reset to
|
|
11
|
+
* pending on every iteration, the crawler would re-attempt them, they would
|
|
12
|
+
* fail again the same way, and the retry-target count would stay constant
|
|
13
|
+
* forever.
|
|
13
14
|
*
|
|
14
|
-
* Why these
|
|
15
|
+
* Why these six and not others:
|
|
15
16
|
* - **dns** — `ENOTFOUND` / `ERR_NAME_NOT_RESOLVED` are authoritative DNS
|
|
16
17
|
* answers; the host is gone (or never existed). EAI_AGAIN is split out as
|
|
17
18
|
* `dns-transient` precisely so it is NOT in this set.
|
|
@@ -28,6 +29,10 @@ import type { ErrorKind } from './types.js';
|
|
|
28
29
|
* the listener; either no process is listening on the port or its accept
|
|
29
30
|
* queue rejected the connection. Either way the answer is final until the
|
|
30
31
|
* server operator intervenes.
|
|
32
|
+
* - **redirect-loop** — `Maximum number of redirects exceeded` /
|
|
33
|
+
* `ERR_TOO_MANY_REDIRECTS` means the site's own redirect chain never
|
|
34
|
+
* terminates; the exact same chain is served on every future fetch until
|
|
35
|
+
* the site operator fixes it.
|
|
31
36
|
*
|
|
32
37
|
* Notably absent (intentionally retryable):
|
|
33
38
|
* - `connection-reset` / `connection-timeout` — could be middlebox or
|
|
@@ -6,11 +6,12 @@
|
|
|
6
6
|
* Used by `resetFailedPages` to exclude pages whose latest recorded error
|
|
7
7
|
* falls in this set, so `--retry-failed` actually converges: without the
|
|
8
8
|
* exclusion, NXDOMAIN / TLS mismatch / `ERR_BLOCKED_BY_CLIENT` /
|
|
9
|
-
* `ECONNREFUSED` / HTTP parse-error pages would be reset to
|
|
10
|
-
* iteration, the crawler would re-attempt them, they would
|
|
11
|
-
* same way, and the retry-target count would stay constant
|
|
9
|
+
* `ECONNREFUSED` / HTTP parse-error / redirect-loop pages would be reset to
|
|
10
|
+
* pending on every iteration, the crawler would re-attempt them, they would
|
|
11
|
+
* fail again the same way, and the retry-target count would stay constant
|
|
12
|
+
* forever.
|
|
12
13
|
*
|
|
13
|
-
* Why these
|
|
14
|
+
* Why these six and not others:
|
|
14
15
|
* - **dns** — `ENOTFOUND` / `ERR_NAME_NOT_RESOLVED` are authoritative DNS
|
|
15
16
|
* answers; the host is gone (or never existed). EAI_AGAIN is split out as
|
|
16
17
|
* `dns-transient` precisely so it is NOT in this set.
|
|
@@ -27,6 +28,10 @@
|
|
|
27
28
|
* the listener; either no process is listening on the port or its accept
|
|
28
29
|
* queue rejected the connection. Either way the answer is final until the
|
|
29
30
|
* server operator intervenes.
|
|
31
|
+
* - **redirect-loop** — `Maximum number of redirects exceeded` /
|
|
32
|
+
* `ERR_TOO_MANY_REDIRECTS` means the site's own redirect chain never
|
|
33
|
+
* terminates; the exact same chain is served on every future fetch until
|
|
34
|
+
* the site operator fixes it.
|
|
30
35
|
*
|
|
31
36
|
* Notably absent (intentionally retryable):
|
|
32
37
|
* - `connection-reset` / `connection-timeout` — could be middlebox or
|
|
@@ -45,4 +50,5 @@ export const PERMANENT_ERROR_KINDS = new Set([
|
|
|
45
50
|
'client-blocked',
|
|
46
51
|
'parse-error',
|
|
47
52
|
'connection-refused',
|
|
53
|
+
'redirect-loop',
|
|
48
54
|
]);
|
package/lib/types.d.ts
CHANGED
|
@@ -66,6 +66,7 @@ export interface InventoryRunAggregates {
|
|
|
66
66
|
* | `local-network` | **yes** | no | local machine's network is unreachable / changed (WiFi, sleep, ICMP-unreachable, …) |
|
|
67
67
|
* | `parse-error` | mostly persistent | no | HTTP response could not be parsed (proxy, garbage, MITM) |
|
|
68
68
|
* | `client-blocked` | persistent (per browser) | no | Chromium-side `ERR_BLOCKED_BY_*` family — the browser actively refused the request (ad/tracker heuristics, CSP, CORP, administrator block list, …) |
|
|
69
|
+
* | `redirect-loop` | no | no | the redirect chain exceeded `follow-redirects`' `maxRedirects` limit — the site's own redirect configuration never converges |
|
|
69
70
|
* | `protocol` | yes | no | puppeteer protocol layer (frame detached, target closed, …) |
|
|
70
71
|
* | `timeout` | yes | no | puppeteer navigation timeout or HEAD pre-flight race timeout (`Timeout: <url>`) |
|
|
71
72
|
* | `unknown` | unknown | no | catch-all for messages no matcher recognised |
|
|
@@ -90,7 +91,7 @@ export interface InventoryRunAggregates {
|
|
|
90
91
|
* entry), or a recoverable URL never reaching the puppeteer fallback (no
|
|
91
92
|
* PUPPETEER_FALLBACK entry).
|
|
92
93
|
*/
|
|
93
|
-
export type ErrorKind = 'dns' | 'dns-transient' | 'connection-refused' | 'connection-reset' | 'connection-timeout' | 'tls' | 'local-network' | 'parse-error' | 'client-blocked' | 'timeout' | 'protocol' | 'unknown';
|
|
94
|
+
export type ErrorKind = 'dns' | 'dns-transient' | 'connection-refused' | 'connection-reset' | 'connection-timeout' | 'tls' | 'local-network' | 'parse-error' | 'client-blocked' | 'redirect-loop' | 'timeout' | 'protocol' | 'unknown';
|
|
94
95
|
/**
|
|
95
96
|
* Event map for the `CrawlerOrchestrator` class.
|
|
96
97
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nitpicker/crawler",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"description": "Web crawler engine with headless browser rendering and archive storage",
|
|
5
5
|
"author": "D-ZERO",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -48,5 +48,5 @@
|
|
|
48
48
|
"@types/tar": "7.0.87",
|
|
49
49
|
"@types/unzipper": "0.10.11"
|
|
50
50
|
},
|
|
51
|
-
"gitHead": "
|
|
51
|
+
"gitHead": "bef8b6d48e3ca5167fee643d6aba8644a065df4a"
|
|
52
52
|
}
|