← back to Codex Review 2026 04 30

am-recrawl/poke.md

54 lines

**Failure mode:** Chrome/CDP disconnects once, then the scraper keeps processing the queue against a dead browser. `scrapeProductPage()` creates `page` before its `try` at `scrape-andrewmartin.js:173-175`, so `browser.newPage()` / `setUserAgent()` failures escape to `scrapeAllProducts()` and are swallowed at `537-541`. That explains the repeated `Connection closed` work. The final `TargetCloseError` is a pending Puppeteer `evaluate()` after the target is already gone, commonly at `184`.

**Root causes:** `scrape-andrewmartin.js:596-599` calls `closePool()` after `main()` already ran its `finally` and called it at `590-592`. `scraper-utils.js:184-185` wraps raw `pool.end()`, which is not idempotent, hence “Called end on pool more than once”.

**Service model:** one-shot. Header usage says `node scrape-andrewmartin.js`, no server/listener loop, and `main()` exits after final stats. PM2 should use `autorestart:false` / `--no-autorestart`; use cron for scheduling.

```diff
diff --git a/scraper-utils.js b/scraper-utils.js
@@
-async function closePool() {
-  await pool.end();
-}
+let poolClosePromise = null;
+async function closePool() {
+  if (!poolClosePromise) poolClosePromise = pool.end();
+  return poolClosePromise;
+}

diff --git a/scrape-andrewmartin.js b/scrape-andrewmartin.js
@@
 async function scrapeProductPage(browser, url) {
-  const page = await browser.newPage();
-  await page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36');
+  let page;

   try {
+    if (!browser.isConnected()) throw new Error('Puppeteer browser disconnected');
+    page = await browser.newPage();
+    await page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36');
@@
-    await page.close();
+    if (!page.isClosed()) await page.close();
     return result;
   } catch (err) {
     console.error('  Error scraping ' + url + ': ' + err.message);
-    await page.close().catch(() => {});
+    if (page && !page.isClosed()) await page.close().catch(() => {});
+    if (/Target closed|Connection closed|browser disconnected/i.test(err.message)) throw err;
     return { product: null, variantUrls: [] };
   }
 }
@@
   while (queue.length > 0) {
+    if (!browser.isConnected()) throw new Error('Puppeteer browser disconnected; aborting scrape');
@@
-main().catch(function(err) {
+main().catch(function(err) {
   console.error('FATAL:', err);
-  closePool().then(function() { process.exit(1); });
+  process.exitCode = 1;
 });
```

PM2 patch/config: run as `pm2 start scrape-andrewmartin.js --name scrape-andrewmartin --no-autorestart`.