{"record":{"id":"cb95752f896de41c","repo":"jackwener/OpenCLI","slug":"malformed-toutiao-recommend-row-invalid-source-ur","errorCode":null,"errorMessage":"Malformed toutiao recommend row: invalid source_url ${JSON.stringify(raw)}.","messagePattern":"Malformed toutiao recommend row: invalid source_url (.+?)\\.","errorType":"exception","errorClass":"CommandExecutionError","httpStatus":null,"severity":"error","filePath":"clis/toutiao/utils.js","lineNumber":214,"sourceCode":"    // The feed serves protocol-relative image URLs (`//p3.pstatp.com/...`);\n    // hot-board already returns absolute ones, so normalise for parity.\n    return raw.startsWith('//') ? `https:${raw}` : raw;\n}\n\nfunction firstPartyToutiaoUrl(sourceUrl) {\n    const raw = trimOrNull(sourceUrl);\n    if (!raw) return null;\n    try {\n        const url = raw.startsWith('/')\n            ? new URL(raw, 'https://www.toutiao.com')\n            : new URL(raw);\n        if (!/^https?:$/i.test(url.protocol) || !/(^|\\.)toutiao\\.com$/i.test(url.hostname)) {\n            throw new CommandExecutionError(`Malformed toutiao recommend row: off-domain source_url ${JSON.stringify(raw)}.`);\n        }\n        return url;\n    } catch (error) {\n        if (error instanceof CommandExecutionError) throw error;\n        throw new CommandExecutionError(`Malformed toutiao recommend row: invalid source_url ${JSON.stringify(raw)}.`);\n    }\n}\n\nfunction articleIdFromToutiaoPath(pathname) {\n    return String(pathname || '').match(/^\\/(?:group|article)\\/([A-Za-z0-9_-]+)\\/?$/)?.[1] || '';\n}\n\nfunction buildGroupIdentity(sourceUrl, rawGroupId) {\n    const source = firstPartyToutiaoUrl(sourceUrl);\n    const sourceId = source ? articleIdFromToutiaoPath(source.pathname) : '';\n    const explicitId = trimOrNull(rawGroupId);\n    const groupId = explicitId || trimOrNull(sourceId);\n    if (!groupId) {\n        throw new CommandExecutionError('Malformed toutiao recommend row: missing group_id and article source_url.');\n    }\n    if (!/^[A-Za-z0-9_-]+$/.test(groupId)) {\n        throw new CommandExecutionError(`Malformed toutiao recommend row: invalid group_id ${JSON.stringify(rawGroupId)}.`);\n    }","sourceCodeStart":196,"sourceCodeEnd":232,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/toutiao/utils.js#L196-L232","documentation":"firstPartyToutiaoUrl catches failures from the URL constructor itself (invalid URL syntax) and rethrows them as this CommandExecutionError. It means the row's source_url could not be parsed as a URL at all — distinct from 3824, where the URL parses but is off-domain.","triggerScenarios":"A source_url like 'not a url', 'http://', or containing raw spaces/characters that make new URL() throw, when the value does not start with '/' (relative '/group/...' values get a base URL and usually parse).","commonSituations":"Upstream feed returning empty/truncated HTML in the source_url field; escaped or double-encoded URLs; fields shifted during CSV/JSON extraction so an unrelated string lands in source_url.","solutions":["Inspect the raw row and fix or drop it; the URL is unparseable so it cannot be salvaged as-is.","Trim whitespace and decode HTML entities (&amp;) before passing the value.","Validate with a try { new URL(v) } catch wrapper before calling the command.","If the value starts with a path, pass it with the leading '/' so the toutiao.com base is applied."],"exampleFix":"// before\nfirstPartyToutiaoUrl('  https://www.toutiao.com/article/123/  ');\n// after\nfirstPartyToutiaoUrl(raw.trim().replace(/&amp;/g, '&'));","handlingStrategy":"type-guard","validationCode":"function isParsableUrl(u) { try { new URL(u, 'https://www.toutiao.com'); return true; } catch { return false; } }\nif (!isParsableUrl(raw)) skipRow(raw);","typeGuard":"const isParseableUrl = (v) => { try { new URL(String(v), 'https://www.toutiao.com'); return true; } catch { return false; } };","tryCatchPattern":"try {\n  const url = firstPartyToutiaoUrl(row.source_url);\n} catch (e) {\n  if (e instanceof CommandExecutionError && e.message.includes('invalid source_url')) return dropRow(row);\n  throw e;\n}","preventionTips":["Trim and HTML-unescape (&amp; -> &) source_url values before use.","Pre-validate with new URL() in a try/catch before calling the command.","Reject empty/whitespace-only values early."],"tags":["url-validation","data-integrity","toutiao"],"backgroundTag":"malformed-url","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}