{"record":{"id":"5512341f0bebe5b7","repo":"jackwener/OpenCLI","slug":"taxonomy-relay-unavailable-site-powerchina-comma","errorCode":null,"errorMessage":"[taxonomy=relay_unavailable] site=powerchina command=search detached browser context: ${message}","messagePattern":"\\[taxonomy=relay_unavailable\\] site=powerchina command=search detached browser context: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"clis/powerchina/search.js","lineNumber":202,"sourceCode":"      apiFailure = cleanText(error instanceof Error ? error.message : String(error || ''));\n    }\n\n    if (apiSucceeded && extractedRows.length === 0) {\n      return [];\n    }\n\n    if (!apiSucceeded) {\n      try {\n        extractedRows = await searchRowsFromEntries(page, {\n          query,\n          candidateUrls: buildSearchCandidates(query),\n          allowedHostFragments: ['bid.powerchina.cn', 'powerchina.cn'],\n          limit,\n        });\n      } catch (error) {\n        const message = cleanText(error instanceof Error ? error.message : String(error || ''));\n        if (RETRYABLE_SEARCH_ERROR_HINT.test(message)) {\n          throw new Error(`[taxonomy=relay_unavailable] site=powerchina command=search detached browser context: ${message}`);\n        }\n        throw error;\n      }\n    }\n\n    const rows = filterNavigationRows(\n      dedupeCandidates(extractedRows).map((item) => ({\n        title: cleanText(item.title),\n        url: cleanText(item.url),\n        date: normalizeDate(cleanText(item.date)),\n        contextText: cleanText(item.contextText),\n      })),\n    );\n\n    if (rows.length === 0 && extractedRows.length > 0) {\n      throw new EmptyResultError('powerchina search', 'extracted only navigation/portal rows, no bid entries matched');\n    }\n","sourceCodeStart":184,"sourceCodeEnd":220,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/powerchina/search.js#L184-L220","documentation":"This is a rewritten relay/browser error. When the underlying headless-browser command fails with a 'detached browser context'-style message (page navigation destroying the execution context, browser target closed, or a relay-detached error), searchRowsFromEntries catches it and re-throws a normalized [taxonomy=relay_unavailable] Error tagged with site=powerchina and command=search. The library does this so upstream callers/retry logic can recognize a transient relay unavailability and retry, instead of seeing an opaque Puppeteer/Playwright internal message.","triggerScenarios":"Calling the powerchina search command when, during searchRowsFromEntries' page.goto/evaluate calls against bid.powerchina.cn, the page navigates or the browser target closes, and error.message matches RETRYABLE_SEARCH_ERROR_HINT (/(detached while handling command|execution context was destroyed|target closed|cannot find context with specified id)/i).","commonSituations":"SPA re-render/navigation racing the DOM evaluation on the search page; headless browser process crashing or being killed mid-scrape (OOM, supervisor restart); relay/remote-browser session expiring between navigation and evaluation; slow network causing timeouts that close the target.","solutions":["Retry the search command with backoff — the taxonomy tag marks this error as retryable/transient.","Check that the browser/relay process is stable (memory, restarts) and not being torn down mid-command.","Re-run with a slower/fewer-candidate configuration to reduce navigation races on bid.powerchina.cn.","If persistent, verify network reachability of bid.powerchina.cn and that the relay service hosting the browser is healthy."],"exampleFix":"// before\nconst rows = await searchRowsFromEntries({ ... });\n// after\nlet rows;\nfor (let attempt = 0; attempt < 3; attempt++) {\n  try { rows = await searchRowsFromEntries({ ... }); break; }\n  catch (error) {\n    if (String(error.message).includes('relay_unavailable') && attempt < 2) {\n      await new Promise(r => setTimeout(r, 1000 * 2 ** attempt));\n      continue;\n    }\n    throw error;\n  }\n}","handlingStrategy":"retry","validationCode":"// preflight: ensure browser/relay is reachable before issuing the command\nconst healthy = await fetch(relayHealthUrl, { signal: AbortSignal.timeout(5000) }).then(r => r.ok).catch(() => false);\nif (!healthy) throw new Error('relay unavailable, aborting search');","typeGuard":"function isRelayUnavailableError(err) {\n  return err instanceof Error && /relay_unavailable/.test(err.message);\n}","tryCatchPattern":"try {\n  rows = await searchPowerchina(query);\n} catch (err) {\n  if (isRelayUnavailableError(err)) {\n    rows = await withBackoff(() => searchPowerchina(query), { retries: 3 });\n  } else throw err;\n}","preventionTips":["Always wrap browser-scrape commands in a retry-with-exponential-backoff helper keyed on the [taxonomy=relay_unavailable] tag.","Monitor relay/browser process health (memory, restarts) before long scraping batches.","Keep a stable, warmed-up browser session rather than launching a fresh context per request.","Avoid issuing search and navigation concurrently in the same page to prevent execution-context destruction."],"tags":["browser-context","relay","retryable","scraping","headless-browser"],"backgroundTag":"detached-browser-context","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}