santifer/career-ops · error · Error
justjoin: URL path must be /job-offers or…
Error message
justjoin: URL path must be /job-offers or /api/candidate-api/offers: ${url} What it means
Thrown by assertJustJoinUrl when the hostname is trusted but the path is neither under /job-offers nor exactly /api/candidate-api/offers. The provider only knows how to build pagination requests against the candidate offers API, so arbitrary justjoin.it pages (homepage, blog, a single job-offer detail page) are rejected. Browser URLs under /job-offers/... are accepted for detection but fetches are rewritten to the API path (see buildApiUrl).
Solutions
- Set the URL path to /job-offers (browser listing) or exactly /api/candidate-api/offers (API) in portals.yml
- Prefer omitting api entirely and letting the provider default to the correct API_BASE
- Fix typos — the API path must match exactly, including the /api/candidate-api/ prefix
Example fix
// before (portals.yml) careers_url: https://justjoin.it/job-offer/frontend-developer // after careers_url: https://justjoin.it/job-offers
Defensive patterns
Strategy: validation
Validate before calling
function hasAcceptedPath(url) {
try {
const p = new URL(url);
return p.hostname === 'justjoin.it' &&
(p.pathname.startsWith('/job-offers') || p.pathname === '/api/candidate-api/offers');
} catch { return false; }
} Type guard
const hasAcceptedPath = (u) => { try { const p = new URL(u); return p.pathname.startsWith('/job-offers') || p.pathname === '/api/candidate-api/offers'; } catch { return false; } }; Try / catch
try {
await provider.fetch(entry, ctx);
} catch (e) {
if (e.message.startsWith('justjoin: URL path must be')) {
console.error(`Point the entry at /job-offers or /api/candidate-api/offers: ${e.message}`);
} else throw e;
} Prevention
- Copy URLs from the job listing page (justjoin.it/job-offers), not a single ad page
- Omit api and let the provider default to the correct API endpoint
- Note the singular/plural trap: /job-offer/<slug> is rejected, /job-offers is accepted
When it happens
Trigger: entry.careers_url or entry.api set to e.g. 'https://justjoin.it/' , 'https://justjoin.it/job-offer/some-slug' (singular), or 'https://justjoin.it/some-other-page' — anything whose pathname fails startsWith('/job-offers') and !== '/api/candidate-api/offers'. Raised from buildApiUrl during fetch if entry.api has a bad path that wasn't caught earlier (detect returns null for bad paths, so usually this fires when entry.api is valid-host but wrong-path while careers_url is absent).
Common situations: Configuring the root domain as careers_url; copying an individual job-ad URL (job-offer/ slug) instead of the listing path; renaming after JustJoin moved from /job-offers to other routes; hand-building an API URL with a typo in the path.
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
- arbeitnow: invalid URL
- ashby: invalid URL
- bamboohr: invalid URL
- breezy: invalid URL
- builtin: invalid URL
AI-assisted analysis of santifer/career-ops@aac998c7ed (2026-09-16).
Data as JSON: /api/errors/4874b61f3b9be7ff.
Report an issue: GitHub.
Appendix: source
Thrown at providers/justjoin.mjs:26
const ALLOWED_HOSTS = new Set(['justjoin.it']);
const API_BASE = 'https://justjoin.it/api/candidate-api/offers';
const JOB_BASE = 'https://justjoin.it/job-offer/';
const PAGE_SIZE = 100;
const MAX_PAGES = 50;
function assertJustJoinUrl(url) {
let parsed;
try {
parsed = new URL(url);
} catch {
throw new Error(`justjoin: invalid URL: ${url}`);
}
if (parsed.protocol !== 'https:') throw new Error(`justjoin: URL must use HTTPS: ${url}`);
if (!ALLOWED_HOSTS.has(parsed.hostname)) {
throw new Error(`justjoin: untrusted hostname "${parsed.hostname}" — must be justjoin.it`);
}
if (!parsed.pathname.startsWith('/job-offers') && parsed.pathname !== '/api/candidate-api/offers') {
throw new Error(`justjoin: URL path must be /job-offers or /api/candidate-api/offers: ${url}`);
}
return parsed;
}
function detectUrl(entry) {
const url = entry.api || entry.careers_url || '';
if (typeof url !== 'string' || !url.trim()) return null;
try {
const parsed = assertJustJoinUrl(url);
return { url: parsed.href };
} catch {
return null;
}
}
function normalizeLocation(offer) {
const parts = [];
const workplace = String(offer?.workplaceType || '').trim();View on GitHub (pinned to aac998c7ed)