santifer/career-ops · error · Error
justjoin: careers_url or api must be a trusted justjoin.it…
Error message
justjoin: careers_url or api must be a trusted justjoin.it URL
What it means
Thrown by the provider's fetch() when detectUrl(entry) returns null, meaning entry has no api or careers_url value that passes assertJustJoinUrl (trusted justjoin.it HTTPS URL with an accepted path). detect() also returns null in that case, so this error means the justjoin provider was selected for a portals.yml entry it cannot claim. It is the top-level guard replacing the more specific URL errors (315-317), which detect() swallows.
Solutions
- Add a valid careers_url (e.g. https://justjoin.it/job-offers) or api (https://justjoin.it/api/candidate-api/offers) to the portals.yml entry
- Confirm the entry should use the justjoin provider at all — if the company uses another ATS, fix the provider routing/detection
- Call provider.detect(entry) first in your own code and skip the provider when it returns null instead of calling fetch
Example fix
// before — calling fetch on an entry that may not be justjoin
const jobs = await justjoinProvider.fetch(entry, ctx);
// after — gate on detect
if (justjoinProvider.detect(entry)) {
const jobs = await justjoinProvider.fetch(entry, ctx);
} Defensive patterns
Strategy: type-guard
Validate before calling
const url = entry.api || entry.careers_url || '';
const usable = typeof url === 'string' && url.trim() && providerDetectsJustJoin(url);
if (!usable) throw new Error('entry is not a justjoin.it source'); Type guard
const isJustJoinEntry = (entry) => {
const u = entry?.api || entry?.careers_url || '';
if (typeof u !== 'string' || !u.trim()) return false;
try {
const p = new URL(u);
return p.protocol === 'https:' && p.hostname === 'justjoin.it' &&
(p.pathname.startsWith('/job-offers') || p.pathname === '/api/candidate-api/offers');
} catch { return false; }
}; Try / catch
try {
const jobs = await justjoinProvider.fetch(entry, ctx);
} catch (e) {
if (e.message.includes('must be a trusted justjoin.it URL')) {
console.error(`Skipping entry ${entry.company ?? '?'}: no valid justjoin.it api/careers_url configured`);
} else throw e;
} Prevention
- Always check provider.detect(entry) !== null before calling fetch
- Ensure every portals.yml entry has a non-empty, correct careers_url for its provider
- Keep provider dispatch driven by detect() rather than a hardcoded provider choice per entry
When it happens
Trigger: Calling provider.fetch(entry, ctx) where entry.api and entry.careers_url are: missing/empty, non-string, whitespace-only, or fail any assertJustJoinUrl check (non-HTTPS, wrong host, wrong path). Also hit when a routing layer dispatches an entry to the justjoin provider that belongs to another provider.
Common situations: A portals.yml company entry with no careers_url; a careers_url pointing at a non-justjoin ATS (Greenhouse/Lever) mis-routed to this provider; an empty string after YAML parsing; someone deleted the URL field during config cleanup.
Understand the failure class
Background: "Invalid value" and "allowed values are" config errors: what your library rejected and how to fix it — this error's family across 41 libraries.
Related errors
- apify: entry has invalid field_map. Each of title, url…
- apify: entry missing 'actor' (e.g. misceres/indeed-scraper)
- arbeitnow: invalid URL
- ashby: invalid URL
- bamboohr: invalid URL
AI-assisted analysis of santifer/career-ops@aac998c7ed (2026-09-16).
Data as JSON: /api/errors/26ae7be53d88c7eb.
Report an issue: GitHub.
Appendix: source
Thrown at providers/justjoin.mjs:110
url: `${JOB_BASE}${slug}`,
company: String(offer.companyName || '').trim(),
location: normalizeLocation(offer),
postedAt: postedAtMillis(offer.publishedAt),
};
})
.filter(Boolean);
}
/** @type {Provider} */
export default {
id: 'justjoin',
detect(entry) {
return detectUrl(entry);
},
async fetch(entry, ctx) {
if (!detectUrl(entry)) throw new Error('justjoin: careers_url or api must be a trusted justjoin.it URL');
const jobs = [];
let from = 0;
const maxPages = Number(entry.max_pages || MAX_PAGES);
for (let page = 0; page < maxPages; page++) {
const url = buildApiUrl(entry, from);
const json = await ctx.fetchJson(url, { redirect: 'error' });
const parsed = parseJustJoinResponse(json);
jobs.push(...parsed);
const next = json?.meta?.next?.cursor;
if (next == null || parsed.length === 0) break;
from = Number(next);
if (!Number.isFinite(from)) break;
}
return jobs;
},
};View on GitHub (pinned to aac998c7ed)