{"record":{"id":"f5754c3d1263a485","repo":"jackwener/OpenCLI","slug":"linkedin-people-search-returned-malformed-extracti-f5754c","errorCode":null,"errorMessage":"LinkedIn people search returned malformed extraction payload: invalid ${label}","messagePattern":"LinkedIn people search returned malformed extraction payload: invalid (.+?)","errorType":"error_code","errorClass":"CommandExecutionError","httpStatus":null,"severity":"error","filePath":"clis/linkedin/people-search.js","lineNumber":74,"sourceCode":"        }\n        const name = normalizeWhitespace(row.name);\n        const profileUrl = normalizeProfileUrl(row.profile_url);\n        if (!name || !profileUrl) {\n            throw new CommandExecutionError(`LinkedIn people search returned row without stable profile identity at index ${index}`);\n        }\n        return {\n            name,\n            headline: normalizeWhitespace(row.headline),\n            location: normalizeWhitespace(row.location),\n            profile_url: profileUrl,\n        };\n    });\n}\n\nfunction parseNonNegativeCount(value, label) {\n    const count = Number(value);\n    if (!Number.isInteger(count) || count < 0) {\n        throw new CommandExecutionError(`LinkedIn people search returned malformed extraction payload: invalid ${label}`);\n    }\n    return count;\n}\n\nfunction extractionScript() {\n    // Class-based selectors are dead (LinkedIn rotates hashed class\n    // names on every deploy) and display:contents flattens the DOM\n    // tree so per-card containers don't exist. Read main.innerText\n    // and slice between consecutive person-name lines instead.\n    return String.raw`(() => {\n    if (!/search\\/results\\/people/.test(window.location.href)) {\n      return { error: 'not on people search page', url: window.location.href };\n    }\n    const main = document.querySelector('main') || document.body;\n    const normalize = (s) => String(s || '').replace(/[\\s\\u00a0\\u202f]+/g, ' ').trim();\n    const collapseRepeatedName = (name) => {\n      const parts = normalize(name).split(' ').filter(Boolean);\n      if (parts.length === 0 || parts.length % 2 !== 0) return normalize(name);","sourceCodeStart":56,"sourceCodeEnd":92,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/linkedin/people-search.js#L56-L92","documentation":"parseNonNegativeCount validates numeric extraction-payload fields (candidate_count, person_entries_count, resolved_count) returned by the injected LinkedIn DOM extraction script. If a value is not an integer >= 0, the library assumes the page-side extractor ran in a changed/broken DOM and throws CommandExecutionError naming the bad field via ${label}. This guards downstream row-normalization logic from garbage counts.","triggerScenarios":"Calling the linkedin people-search command when page.evaluate(extractionScript()) returns a result object whose candidate_count, person_entries_count, or resolved_count is undefined, a non-integer number, negative, or a non-numeric string — e.g. LinkedIn changed the page markup so the counter selectors resolve to nothing.","commonSituations":"LinkedIn DOM/markup updates breaking the in-page extractor; scraping an empty/interstitial results page; LinkedIn serving a variant layout (different locale or mobile layout); a partial extraction script that sets counts conditionally.","solutions":["Update the package to the latest version that matches current LinkedIn markup","Re-run the search in a signed-in browser session so the real results page (with counters) renders","Log result.candidate_count/result.person_entries_count/result.resolved_count in the evaluate callback to see which field is malformed and why","Check whether LinkedIn redirected to an auth/CUL page and clear that first (see errors 2345/2348)","File/inspect the extractionScript() selectors and patch them for the new LinkedIn layout"],"exampleFix":"// before: payload field missing, throws\nconst candidateCount = parseNonNegativeCount(result.candidate_count, 'candidate_count');\n// after: default missing fields to 0 before validation\nconst candidateCount = parseNonNegativeCount(result.candidate_count ?? 0, 'candidate_count');","handlingStrategy":"validation","validationCode":"function isValidCount(v) { return Number.isInteger(Number(v)) && Number(v) >= 0; }\nconst payload = result ?? {};\nif (![payload.candidate_count, payload.person_entries_count, payload.resolved_count].every(isValidCount)) {\n  throw new Error('extraction payload has invalid count fields');\n}","typeGuard":"const isExtractionCounts = (o) => o != null && typeof o === 'object' &&\n  ['candidate_count', 'person_entries_count', 'resolved_count'].every((k) => Number.isInteger(Number(o[k])) && Number(o[k]) >= 0);","tryCatchPattern":"try { const count = parseNonNegativeCount(result.candidate_count, 'candidate_count'); } catch (e) { logger.warn('malformed counts', result); /* fallback: proceed with rows only */ }","preventionTips":["Keep the CLI updated against LinkedIn markup changes","Log raw evaluate payloads before validating counts","Treat count fields as optional and default to 0 in your own wrappers","Detect auth/CUL redirects before count validation so the real cause surfaces"],"tags":["scraping","linkedin","dom-parsing","payload-validation"],"backgroundTag":"malformed-extraction-payload","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}