{"record":{"id":"f28717a3bbcc230f","repo":"santifer/career-ops","slug":"oraclecloud-cannot-derive-api-url-for-entry-nam","errorCode":null,"errorMessage":"oraclecloud: cannot derive API URL for ${entry.name}","messagePattern":"oraclecloud: cannot derive API URL for (.+?)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/oraclecloud.mjs","lineNumber":233,"sourceCode":"  return out;\n}\n\n/** @type {Provider} */\nexport default {\n  id: 'oraclecloud',\n\n  detect(entry) {\n    try {\n      const site = resolveSite(entry);\n      return site ? { url: buildApiUrl(site, 0, PAGE_SIZE) } : null;\n    } catch {\n      return null;\n    }\n  },\n\n  async fetch(entry, ctx) {\n    const site = resolveSite(entry);\n    if (!site) throw new Error(`oraclecloud: cannot derive API URL for ${entry.name}`);\n\n    const maxPages = Number.isInteger(entry.max_pages) && entry.max_pages > 0\n      ? Math.min(entry.max_pages, MAX_PAGES)\n      : MAX_PAGES;\n\n    const all = [];\n    let total = null;\n    for (let page = 0; page < maxPages; page++) {\n      const offset = page * PAGE_SIZE;\n      const apiUrl = buildApiUrl(site, offset, PAGE_SIZE);\n      assertOracleUrl(apiUrl); // SSRF guard before every fetch\n      if (page > 0) await sleep(INTER_PAGE_DELAY_MS, ctx);\n\n      const json = await fetchJsonWithRetry(ctx, apiUrl, {\n        redirect: 'error',\n        headers: { 'User-Agent': BROWSER_LIKE_USER_AGENT, Accept: 'application/json' },\n      }, RETRY_POLICY);\n","sourceCodeStart":215,"sourceCodeEnd":251,"githubUrl":"https://github.com/santifer/career-ops/blob/9b17a8ac97b398a496b38e423ae24e433b43254f/providers/oraclecloud.mjs#L215-L251","documentation":"Thrown by oraclecloud's fetch() when resolveSite(entry) returns null — the entry has no api or careers_url that resolves to a valid Oracle FA cloud site. resolveSite() iterates [entry.api, entry.careers_url], parses each as a URL, checks protocol is https and hostname matches ORACLE_HOST_RE, then extracts siteNumber and lang from the path. If neither field yields a valid Oracle site, fetch() cannot proceed.","triggerScenarios":"resolveSite returns null when: (1) both entry.api and entry.careers_url are missing/empty/non-string; (2) they fail URL parsing; (3) protocol isn't https; (4) hostname fails ORACLE_HOST_RE. detect() catches the same resolveSite null and returns null, so this throw in fetch() means detect() was bypassed or the entry changed between detect() and fetch().","commonSituations":"The portals.yml entry for this provider has a missing or wrong-format careers_url. The entry was constructed by a batch script without the URL field. A config merge overwrote the correct URL with an empty or different value. The entry's careers_url points to a non-Oracle FA host (e.g. a legacy Taleo URL).","solutions":["Check the portals.yml entry has a valid careers_url: https://<tenant>.fa.<region>.oraclecloud.com/hcmUI/CandidateExperience/<lang>/sites/<siteNumber>","Verify entry.api (if set) also matches the Oracle FA cloud pattern — resolveSite checks api first.","Ensure detect() is called and its result respected before calling fetch() — skip entries where detect() returns null.","If the entry was programmatically generated, add the careers_url field with the correct Oracle HCM URL."],"exampleFix":"// before — entry missing or with wrong URL\njob_boards:\n  oracle:\n    provider: oraclecloud\n    name: Acme\n    # careers_url missing\n\n// after\njob_boards:\n  oracle:\n    provider: oraclecloud\n    name: Acme\n    careers_url: 'https://acme.fa.eu.oraclecloud.com/hcmUI/CandidateExperience/en/sites/CX_1'\n    # optional overrides:\n    # siteNumber: 'CX_1'\n    # max_pages: 5","handlingStrategy":"validation","validationCode":"const ORACLE_HOST_RE = /^[a-z0-9-]+\\.fa\\.(?:[a-z0-9-]+\\.)?(?:ocs\\.)?oraclecloud(?:[1-9][0-9]?)?\\.com$/i;\n\n/** Replicate resolveSite's logic to validate an entry before fetch. */\nfunction canResolveOracleSite(entry) {\n  for (const raw of [entry.api, entry.careers_url]) {\n    if (typeof raw !== 'string' || !raw) continue;\n    try {\n      const p = new URL(raw);\n      if (p.protocol === 'https:' && ORACLE_HOST_RE.test(p.hostname)) return true;\n    } catch { /* skip */ }\n  }\n  return false;\n}\n\nif (!canResolveOracleSite(entry)) {\n  console.warn(`oraclecloud entry ${entry.name} cannot resolve site — skipping`);\n  continue;\n}","typeGuard":"/** @param {import('./_types.js').PortalEntry} entry @returns {boolean} */\nfunction hasValidOracleUrl(entry) {\n  const url = entry.api || entry.careers_url;\n  return typeof url === 'string'\n    && /\\.fa\\..*\\.oraclecloud\\.com/i.test(url)\n    && url.startsWith('https://');\n}","tryCatchPattern":"try {\n  await oracleProvider.fetch(entry, ctx);\n} catch (err) {\n  if (String(err.message).startsWith('oraclecloud: cannot derive API URL')) {\n    console.warn(`skipping ${entry.name}: no valid Oracle FA cloud URL`);\n    continue;\n  }\n  throw err;\n}","preventionTips":["Call detect(entry) before fetch() — it wraps resolveSite in a try-catch returning null for invalid entries.","Validate portals.yml entries at load time for required Oracle FA cloud URLs.","Ensure config merges don't overwrite Oracle entry URLs with empty or wrong values."],"tags":["config-validation","ssrf-guard","oraclecloud","config"],"backgroundTag":null,"analyzedSha":"9b17a8ac97b398a496b38e423ae24e433b43254f","analyzedAt":"2026-08-13T00:48:39.135Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}