jackwener/OpenCLI · error · ArgumentError

job-url must be a https://www.linkedin.com/jobs/view/

Error message

job-url must be a https://www.linkedin.com/jobs/view/<id> URL

What it means

normalizeJobUrl first runs assertSafeLinkedinUrl (rejecting non-LinkedIn/unsafe URLs) and then requires the path to match /jobs/view/<id> or a currentJobId=<id> query param. If neither pattern matches, an ArgumentError is thrown because no job id can be extracted to build the detail query URL.

Solutions

  1. Pass the full job view URL containing a numeric id: https://www.linkedin.com/jobs/view/<numericId>/.
  2. If you have a search URL, extract currentJobId from it or open the job and copy the canonical /jobs/view/<id> URL.
  3. Resolve shortened (lnkd.in) or redirect URLs to their final linkedin.com/jobs/view/<id> form before calling.
  4. Validate the id client-side: /linkedin\.com\/(jobs\/view\/(\d+)|.*currentJobId=(\d+))/ before invoking.

Example fix

// before
jobDetail('https://www.linkedin.com/jobs/search/?keywords=engineer'); // ArgumentError

// after
jobDetail('https://www.linkedin.com/jobs/view/3987654321/');
Defensive patterns

Strategy: validation

Validate before calling

const JOB_URL_RE = /^https:\/\/www\.linkedin\.com\/jobs\/view\/(\d+)/;
const jobIdFromUrl = (u) => {
  const m = String(u).match(JOB_URL_RE) || String(u).match(/[?&]currentJobId=(\d+)/);
  if (!m) throw new TypeError('Expected a linkedin.com/jobs/view/<id> URL');
  return m[1];
};

Type guard

const isLinkedinJobUrl = (u) =>
  typeof u === 'string' &&
  (/^https:\/\/www\.linkedin\.com\/jobs\/view\/\d+/.test(u) || /[?&]currentJobId=\d+/.test(u));

Try / catch

try {
  await linkedinJobDetail(jobUrl);
} catch (e) {
  if (e instanceof ArgumentError && /job-url/.test(e.message)) {
    const id = await resolveJobIdFromSearchOrRedirect(jobUrl);
    return linkedinJobDetail(`https://www.linkedin.com/jobs/view/${id}/`);
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling the job-detail command with a job URL like https://www.linkedin.com/jobs/view/some-slug without a numeric id, a jobs collection URL (/jobs/search/...), a truncated URL, or a non-LinkedIn URL that slipped past the safety check but lacks the job-id pattern.

Common situations: Copying a job-search results URL instead of a specific job view URL, URLs where the id lives only in a tracking redirect, sharing mobile app links (linkd.in shorteners) that were never resolved, or malformed saved links from a database.

Understand the failure class

Background: "Invalid URL" errors: why new URL(), URI.parse, and reqwest::Url reject your string — missing scheme, whitespace, and bad path format — this error's family across 39 libraries.

Related errors


AI-assisted analysis of jackwener/OpenCLI@49907e53dc (2026-08-29). Data as JSON: /api/errors/1b4b9935e5c20d52. Report an issue: GitHub.

Appendix: source

Thrown at clis/linkedin/job-detail.js:15

import { cli, Strategy } from '@jackwener/opencli/registry';
import { ArgumentError, CommandExecutionError } from '@jackwener/opencli/errors';
import {
  assertLinkedInAuthenticated,
  assertSafeLinkedinUrl,
  normalizeHttpUrl,
  normalizeWhitespace,
  unwrapEvaluateResult,
} from './shared.js';

function normalizeJobUrl(value) {
  const url = assertSafeLinkedinUrl(value, 'job-url');
  const parsed = new URL(url);
  const match = parsed.pathname.match(/^\/jobs\/view\/(\d+)/) || parsed.search.match(/[?&]currentJobId=(\d+)/);
  if (!match) throw new ArgumentError('job-url must be a https://www.linkedin.com/jobs/view/<id> URL');
  return `https://www.linkedin.com/jobs/search/?currentJobId=${match[1]}`;
}

function decodeLinkedinRedirect(url) {
  if (!url) return '';
  try {
    const parsed = new URL(url);
    if (parsed.pathname === '/redir/redirect/') return normalizeHttpUrl(parsed.searchParams.get('url') || '');
  } catch {}
  return normalizeHttpUrl(url);
}

function buildExtractionScript() {
  return String.raw`(() => {
    const clean = (s) => String(s || '').replace(/[\u00a0\u202f]+/g, ' ').replace(/\s+/g, ' ').trim();
    const readRenderedDescription = () => {
      const expanders = Array.from(document.querySelectorAll('button, a'))
        .filter((el) => /\b(show more|see more|more)\b/i.test(clean(el.innerText || el.textContent || el.getAttribute('aria-label') || '')));

View on GitHub (pinned to 49907e53dc)