{"record":{"id":"90aa38445421ab17","repo":"santifer/career-ops","slug":"breezy-cannot-derive-api-url-for-entry-name","errorCode":null,"errorMessage":"breezy: cannot derive API URL for ${entry.name}","messagePattern":"breezy: cannot derive API URL for (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"providers/breezy.mjs","lineNumber":66,"sourceCode":"    return null;\n  }\n  if (parsed.protocol !== 'https:') return null;\n  if (!BREEZY_HOST_RE.test(parsed.hostname)) return null;\n  return `https://${parsed.hostname}`;\n}\n\n/** @type {Provider} */\nexport default {\n  id: 'breezy',\n\n  detect(entry) {\n    const origin = resolveOrigin(entry);\n    return origin ? { url: `${origin}/json` } : null;\n  },\n\n  async fetch(entry, ctx) {\n    const origin = resolveOrigin(entry);\n    if (!origin) throw new Error(`breezy: cannot derive API URL for ${entry.name}`);\n    const apiUrl = `${origin}/json`;\n    assertBreezyUrl(apiUrl);\n    // redirect:'error' + the host check above keep the final hostname pinned to\n    // the tenant — a server-side redirect can't bounce us off-domain (SSRF).\n    const json = /** @type {any} */ (await ctx.fetchJson(apiUrl, { redirect: 'error' }));\n    return parseBreezyResponse(json, entry.name);\n  },\n};\n\n/**\n * Parse a Breezy `<tenant>.breezy.hr/json` response. Exported for unit tests.\n *\n * Breezy returns a top-level array of positions:\n *   [{ name, url, published_date?,\n *      location: { name?, city?, state?, country?: { name }, is_remote? } }]\n *\n * - url: Breezy supplies an absolute posting URL on the tenant domain\n *   (`https://<tenant>.breezy.hr/p/<id>-<slug>`); it is the Job contract's dedup","sourceCodeStart":48,"sourceCodeEnd":84,"githubUrl":"https://github.com/santifer/career-ops/blob/aac998c7ed7248ea853b720ceeb1fdbeb322fc5d/providers/breezy.mjs#L48-L84","documentation":"The breezy provider's fetch() throws this when resolveOrigin(entry) returns null, i.e. neither entry.api nor entry.careers_url yields a usable https://<tenant>.breezy.hr origin. The provider cannot construct its board feed URL (<origin>/json) without one, so it fails fast instead of fetching a guess.","triggerScenarios":"Calling provider.fetch with a portals entry that has no api or careers_url field, both fields empty/whitespace-only, non-string values, or values that fail to parse as https URLs matching the breezy hostname pattern (e.g. 'http://acme.breezy.hr' or 'https://acme.example.com').","commonSituations":"A portals.yml entry for a Breezy company where the careers_url was deleted during an edit; a YAML indentation mistake leaving the field nested under the wrong key; a company that moved off Breezy so its URL no longer matches the pattern; entries created before the api field convention existed.","solutions":["Add a valid careers_url to the portals.yml entry: 'https://<tenant>.breezy.hr'.","Or set the explicit api field to 'https://<tenant>.breezy.hr/json'.","Check YAML indentation — the field must sit directly under the entry, not nested one level deeper.","Confirm the company still hosts its board on breezy.hr; if it migrated ATS, remove or repoint the entry."],"exampleFix":"# before (entry missing careers_url)\n- name: Acme\n# after\n- name: Acme\n  careers_url: https://acme.breezy.hr","handlingStrategy":"validation","validationCode":"function hasBreezyOrigin(entry) {\n  const raw = (typeof entry.api === 'string' && entry.api) || (typeof entry.careers_url === 'string' && entry.careers_url) || '';\n  if (!raw.trim()) return false;\n  try {\n    const u = new URL(raw.trim());\n    return u.protocol === 'https:' && /^[a-z0-9][a-z0-9-]*\\.breezy\\.hr$/.test(u.hostname);\n  } catch { return false; }\n}\nif (!hasBreezyOrigin(entry)) throw new Error(`entry ${entry.name} has no usable breezy origin`);","typeGuard":"function hasOriginField(entry) { return (typeof entry.api === 'string' && entry.api.trim() !== '') || (typeof entry.careers_url === 'string' && entry.careers_url.trim() !== ''); }","tryCatchPattern":"try {\n  await provider.fetch(entry, ctx);\n} catch (err) {\n  if (String(err.message).startsWith('breezy: cannot derive API URL')) {\n    console.warn(`Skipping ${entry.name}: no valid careers_url/api for breezy provider`);\n    return null;\n  }\n  throw err;\n}","preventionTips":["Require careers_url on every portals entry via a config schema check at load time.","Check YAML indentation so careers_url sits directly under the entry object.","Run provider.detect() over all entries at startup; null detections surface missing fields early.","Re-verify entries after a company changes ATS vendor."],"tags":["config","missing-field","url","provider"],"backgroundTag":"missing-required-config-field","analyzedSha":"aac998c7ed7248ea853b720ceeb1fdbeb322fc5d","analyzedAt":"2026-09-16T06:35:29.214Z","contentChangedAt":"2026-09-16T06:35:29.214Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}