santifer/career-ops · error · Error

rippling: cannot derive API URL for

Error message

rippling: cannot derive API URL for ${entry.name}

What it means

The rippling provider derives its API URL from a slug in the provider entry via resolveSlug. If no slug can be resolved for the entry, fetch throws this error naming the entry, rather than calling the API with an undefined target.

Solutions

  1. Add/fix the slug field on the provider entry (the identifier Rippling uses in its board URL).
  2. Derive the slug from the company's public Rippling jobs URL (the path segment) and store it in the entry.
  3. Remove or re-point the entry if the company no longer uses Rippling ATS.

Example fix

// before
{ name: 'acme' }
// after
{ name: 'acme', slug: 'acme' } // https://api.rippling.com/...?slug=acme
Defensive patterns

Strategy: validation

Validate before calling

if (typeof entry.slug !== 'string' || !entry.slug.trim()) {
  throw new Error(`Entry ${entry.name} is missing a Rippling slug`);
}

Type guard

const hasSlug = (e) => typeof e?.slug === 'string' && e.slug.trim().length > 0;

Try / catch

try { jobs = await provider.fetch(entry, ctx); } catch (e) { if (e.message.includes('cannot derive API URL')) { console.error(`Add slug to entry ${entry.name}`); return null; } throw e; }

Prevention

When it happens

Trigger: Calling fetch with an entry whose slug is missing, an empty string, or otherwise falsy after resolveSlug's checks — e.g. { name: 'acme' } with no slug/ats identifier, or a slug key typo'd in config.

Common situations: A newly added portals.yml company entry missing the Rippling slug, a renamed config key after an edit, or a company that moved off Rippling so the stored slug no longer applies.

Understand the failure class

Background: "Invalid URL" / "URL cannot be empty": fix the malformed or missing URL behind request-construction failures — this error's family across 50 libraries.

Related errors


AI-assisted analysis of santifer/career-ops@aac998c7ed (2026-09-16). Data as JSON: /api/errors/554936554262f3e8. Report an issue: GitHub.

Appendix: source

Thrown at providers/rippling.mjs:74

  if (parsed.protocol !== 'https:') throw new Error(`rippling: URL must use HTTPS: ${url}`);
  if (parsed.hostname !== API_HOST) {
    throw new Error(`rippling: untrusted hostname "${parsed.hostname}" — must be ${API_HOST}`);
  }
  return url;
}

/** @type {Provider} */
export default {
  id: 'rippling',

  detect(entry) {
    const slug = resolveSlug(entry);
    return slug ? { url: apiUrlForSlug(slug) } : null;
  },

  async fetch(entry, ctx) {
    const slug = resolveSlug(entry);
    if (!slug) throw new Error(`rippling: cannot derive API URL for ${entry.name}`);
    const apiUrl = apiUrlForSlug(slug);
    assertRipplingApiUrl(apiUrl);
    // redirect:'error' prevents SSRF via server-side redirects
    const json = await ctx.fetchJson(apiUrl, { redirect: 'error' });
    return parseRipplingResponse(json, entry.name);
  },
};

/**
 * Parse a Rippling board API response. Exported for unit tests.
 *
 * The response is a top-level JSON ARRAY of postings. Field mapping → the
 * normalized Job shape:
 *   - title:    `name`, trimmed (postings without one are dropped).
 *   - url:      `url` — an absolute `https:` posting URL host-locked to
 *               `ats.rippling.com` (Rippling always serves postings there, so an
 *               off-host or non-https URL is untrusted and the posting is dropped).
 *               It is the dedup key and is display-only (written to the

View on GitHub (pinned to aac998c7ed)