{"record":{"id":"e7382320564c7a5f","repo":"jackwener/OpenCLI","slug":"invalid-linkedin-learning-host-parsed-hostname","errorCode":null,"errorMessage":"Invalid LinkedIn Learning host: \"${parsed.hostname}\"","messagePattern":"Invalid LinkedIn Learning host: \"(.+?)\"","errorType":"validation","errorClass":"ArgumentError","httpStatus":null,"severity":"error","filePath":"clis/linkedin-learning/course.js","lineNumber":21,"sourceCode":" */\nimport { cli, Strategy } from '@jackwener/opencli/registry';\nimport { ArgumentError, CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';\nimport { DOMAIN, fetchLinkedInLearningApi, normalizeWhitespace } from './shared.js';\n\nfunction parseSlug(value) {\n    const s = normalizeWhitespace(value);\n    if (!s) throw new ArgumentError('<slug> is required');\n    let slug = s;\n    if (/^https?:\\/\\//i.test(s)) {\n        let parsed;\n        try {\n            parsed = new URL(s);\n        } catch {\n            throw new ArgumentError(`Invalid LinkedIn Learning URL: \"${s}\"`);\n        }\n        const host = parsed.hostname.toLowerCase();\n        if (host !== 'linkedin.com' && host !== 'www.linkedin.com') {\n            throw new ArgumentError(`Invalid LinkedIn Learning host: \"${parsed.hostname}\"`);\n        }\n        const m = parsed.pathname.match(/^\\/learning\\/([^/?#]+)/);\n        if (!m) throw new ArgumentError(`Invalid LinkedIn Learning course URL: \"${s}\"`);\n        slug = m[1];\n    } else {\n        const m = s.match(/^\\/?learning\\/([^/?#]+)/);\n        slug = m ? m[1] : s;\n    }\n    if (!/^[a-zA-Z0-9-_]+$/.test(slug)) {\n        throw new ArgumentError(`Invalid LinkedIn Learning slug: \"${slug}\"`);\n    }\n    return slug;\n}\n\nfunction parseCourse(el, slug) {\n    const title = normalizeWhitespace(el?.title);\n    if (!title) return null;\n    const description = typeof el?.description === 'string'","sourceCodeStart":3,"sourceCodeEnd":39,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/linkedin-learning/course.js#L3-L39","documentation":"Thrown by parseSlug when the user passes a full http(s) URL whose hostname is not linkedin.com or www.linkedin.com. The library only accepts course URLs on official LinkedIn hosts, so any other domain (typos, shortener redirects, mirror sites) is rejected before any network call. It is an ArgumentError, i.e. a caller-input validation failure.","triggerScenarios":"Calling the linkedin-learning course command with args.slug set to an http(s) URL whose parsed hostname is not exactly 'linkedin.com' or 'www.linkedin.com' (case-insensitive), e.g. 'https://linkedin.elearning.example/learning/foo' or 'https://lnkd.in/learning/foo'.","commonSituations":"Passing a link copied from a LinkedIn learning-path partner site, a URL shortener (lnkd.in), a staging/corporate proxy host, or a regional mirror; typo like 'linkedi n.com'; pasting a LinkedIn post URL instead of a course URL after editing the domain.","solutions":["Pass the plain course slug (e.g. 'agentic-ai-build-your-first-agentic-ai-system') instead of a URL.","Use a URL on www.linkedin.com, e.g. https://www.linkedin.com/learning/<slug>/, or the path form '/learning/<slug>'.","Resolve any URL shortener to its final linkedin.com destination before passing it."],"exampleFix":"// before\nopencli linkedin-learning course 'https://lnkd.in/learning/agentic-ai-basics'\n// after\nopencli linkedin-learning course 'https://www.linkedin.com/learning/agentic-ai-basics/'","handlingStrategy":"validation","validationCode":"function isLinkedInHost(u) {\n  try {\n    const { hostname } = new URL(u);\n    const h = hostname.toLowerCase();\n    return h === 'linkedin.com' || h === 'www.linkedin.com';\n  } catch { return false; }\n}\n// call: if (arg.startsWith('http') && !isLinkedInHost(arg)) fixBeforeCalling(arg);","typeGuard":"function isLinkedInLearningUrl(v) {\n  try {\n    const u = new URL(v);\n    const h = u.hostname.toLowerCase();\n    return (h === 'linkedin.com' || h === 'www.linkedin.com') && /^\\/learning\\/[^/?#]+/.test(u.pathname);\n  } catch { return false; }\n}","tryCatchPattern":"try {\n  run(['linkedin-learning', 'course', arg]);\n} catch (e) {\n  if (String(e.message).includes('Invalid LinkedIn Learning host')) {\n    // strip to slug or resolve shortener, then retry\n  } else throw e;\n}","preventionTips":["Prefer passing the bare slug instead of URLs.","Resolve lnkd.in and other shorteners to their final linkedin.com destination first.","Keep a helper that normalizes any LinkedIn course URL to https://www.linkedin.com/learning/<slug>/ before invoking."],"tags":["argument-validation","url-parsing","linkedin-learning"],"backgroundTag":"invalid-url-host","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}