{"record":{"id":"a7e1680dbccedb11","repo":"jackwener/OpenCLI","slug":"linkedin-services-read-could-not-find-stable-servi","errorCode":null,"errorMessage":"LinkedIn services-read could not find stable Services page content","messagePattern":"LinkedIn services-read could not find stable Services page content","errorType":"exception","errorClass":"CommandExecutionError","httpStatus":null,"severity":"error","filePath":"clis/linkedin/services-read.js","lineNumber":122,"sourceCode":"    const description = lines[i + 1] || '';\n    if (title) pairs.push(description ? `${title} — ${description}` : title);\n  }\n  return pairs;\n}\n\nfunction normalizeServices(row) {\n  if (!row || typeof row !== 'object') {\n    throw new CommandExecutionError('LinkedIn services-read returned malformed extraction payload');\n  }\n  const services = Array.isArray(row.services_provided) ? row.services_provided.map(normalizeWhitespace).filter(Boolean) : [];\n  const mediaItems = pairsToMedia(row.media_lines);\n  const publicMedia = [];\n  const serviceUrl = normalizeWhitespace(row.service_url);\n  const pageTitle = normalizeWhitespace(row.page_title);\n  const overview = normalizeWhitespace(row.overview);\n  const availability = normalizeWhitespace(row.availability);\n  if (!serviceUrl || (!pageTitle && !overview && services.length === 0)) {\n    throw new CommandExecutionError('LinkedIn services-read could not find stable Services page content');\n  }\n  return {\n    service_url: serviceUrl,\n    page_title: pageTitle,\n    overview,\n    availability,\n    work_locations: Array.isArray(row.work_locations) ? row.work_locations.map((item) => {\n      const text = normalizeWhitespace(item);\n      const words = text.split(' ');\n      if (words.length % 2 === 0) {\n        const half = words.length / 2;\n        const left = words.slice(0, half).join(' ');\n        if (left === words.slice(half).join(' ')) return left;\n      }\n      return text;\n    }).filter(Boolean).join('; ') : '',\n    pricing: normalizeWhitespace(row.pricing).replace(/^Pricing,\\s*Select one option,\\s*/i, '').replace(/,\\s*required$/i, ''),\n    services_provided: services.join('; '),","sourceCodeStart":104,"sourceCodeEnd":140,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/linkedin/services-read.js#L104-L140","documentation":"normalizeServices throws this CommandExecutionError when the extracted row exists but lacks the minimal signals of a real Services page: no service_url, or service_url present with none of page_title, overview, or any services_provided entries. It guards against returning fabricated/empty records when the page content is not actually a Services page.","triggerScenarios":"The extraction ran but matched no meaningful content: service_url blank, or a row with a URL yet empty title/overview/services — typically when the script executed on a wrong or partially loaded page.","commonSituations":"Navigating to a /services/page/<id>/ URL that was deleted or renamed; a not-fully-rendered page (extracted before content hydration); LinkedIn markup changes causing selectors to match nothing.","solutions":["Increase the wait after page.goto(servicesUrl) (currently 5s) so content hydrates before extraction.","Open the servicesUrl in the browser to confirm the Services page still exists and renders.","Update extraction selectors to current LinkedIn markup, then re-run."],"exampleFix":"// before\nawait page.goto(servicesUrl);\nawait page.wait(5);\n// after\nawait page.goto(servicesUrl, { waitUntil: 'networkidle' });\nawait page.waitForSelector('[data-services-content], .services-page', { timeout: 15000 }).catch(() => {});\nawait page.wait(5);","handlingStrategy":"retry","validationCode":"// wait for content before extracting\nawait page.goto(servicesUrl, { waitUntil: 'networkidle' });\nawait page.waitForSelector('.services-page, [data-services]', { timeout: 15000 }).catch(() => {});","typeGuard":"function looksLikeServicesRow(row) {\n  return Boolean(row && row.service_url && (row.page_title || row.overview || (row.services_provided || []).length));\n}","tryCatchPattern":"try {\n  services = await readServices(page, servicesUrl);\n} catch (e) {\n  if (/could not find stable Services page content/.test(e.message)) {\n    await page.wait(5); services = await readServices(page, servicesUrl); // one retry\n  } else throw e;\n}","preventionTips":["Wait for network idle/content hydration before extraction, not a fixed 5s.","Verify the /services/page/<id>/ URL still exists before scraping.","Re-validate selectors after LinkedIn markup releases."],"tags":["linkedin","scraping","empty-content","dom-change"],"backgroundTag":"dom-structure-changed","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}