santifer/career-ops · error · Error
justjoin: invalid URL
Error message
justjoin: invalid URL: ${url} What it means
assertJustJoinUrl in the JustJoin.it provider throws this when new URL(url) itself fails — i.e. the string is not a parseable absolute URL (missing scheme, whitespace, garbage). It is a fail-fast input validation guarding fetches so only well-formed https justjoin.it URLs ever reach the network layer. Related but distinct throws handle HTTPS violations, untrusted hostnames, and bad paths; this one is purely about URL parseability.
Solutions
- Prefix the scheme in the portal entry: set api/careers_url to the full 'https://justjoin.it/api/candidate-api/offers' (or an https://justjoin.it/job-offers/... URL)
- Trim whitespace and strip wrapping quotes from the configured URL before it reaches the provider
- Validate the entry config at load time (e.g. new URL(entry.api ?? entry.careers_url)) so a malformed URL fails at startup with a clear config message rather than mid-scan
- If the value comes from an env var or template, confirm it was substituted — a literal '${...}' placeholder will never parse
- Check portals.yml for typos (colon-slash count, protocol spelling) in the justjoin entry
Example fix
// before (portals.yml entry)
companies:
- name: example
api: justjoin.it/api/candidate-api/offers
// after
companies:
- name: example
api: https://justjoin.it/api/candidate-api/offers Defensive patterns
Strategy: validation
Validate before calling
function isValidJustJoinUrl(url) {
if (typeof url !== 'string') return false;
let parsed;
try { parsed = new URL(url.trim()); } catch { return false; }
return parsed.protocol === 'https:'
&& parsed.hostname === 'justjoin.it'
&& (parsed.pathname.startsWith('/job-offers') || parsed.pathname === '/api/candidate-api/offers');
}
// in portals.yml loading: if (!isValidJustJoinUrl(entry.api)) fail fast with the entry name; Type guard
function isAbsoluteHttpUrl(value) {
if (typeof value !== 'string' || !value.trim()) return false;
try { const u = new URL(value.trim()); return u.protocol === 'https:' || u.protocol === 'http:'; }
catch { return false; }
} Try / catch
try {
const offers = await justjoinProvider.fetch(entry, ctx);
} catch (err) {
if (String(err.message).includes('invalid URL')) {
console.error(`justjoin entry ${entry.name} has a malformed api/careers_url: ${err.message} — fix portals.yml`);
return [];
}
throw err;
} Prevention
- Always configure api/careers_url as a fully-qualified https://justjoin.it/... URL — never relative or scheme-less
- Trim and de-quote configured values when loading portals.yml
- Validate all portal entries with new URL() at config load time so bad URLs fail at startup, not mid-scan
- Check for unsubstituted template placeholders (${...}) when URLs come from env or templating
- Remember the provider only trusts host justjoin.it — redirects to other domains will be rejected by sibling guards
When it happens
Trigger: Raised wherever assertJustJoinUrl runs on entry input: buildApiUrl(entry, from) with entry.api set to a relative string ('/api/offers'), a URL missing the scheme ('justjoin.it/api/candidate-api/offers'), containing spaces or control characters, or entry.careers_url being a non-URL string passed straight through; also detect(), though detect catches the throw internally and returns null.
Common situations: Portal entry in portals.yml has api: 'justjoin.it/api/candidate-api/offers' (no https://); a copy-pasted URL carries trailing whitespace/newline or surrounding quotes; someone configured a relative path assuming the base is implied; an environment variable or template placeholder left unfilled so the value is a literal like '${API_URL}'.
Understand the failure class
Background: "Invalid URL" errors: why new URL(), URI.parse, and reqwest::Url reject your string — missing scheme, whitespace, and bad path format — this error's family across 39 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/4528aff245591b42.
Report an issue: GitHub.
Appendix: source
Thrown at providers/justjoin.mjs:19
// @ts-check
/** @typedef {import('./_types.js').Provider} Provider */
// JustJoin.it provider — hits the current candidate offers API.
// Browser URLs under https://justjoin.it/job-offers/... are accepted for
// detection, but fetches use https://justjoin.it/api/candidate-api/offers.
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 {View on GitHub (pinned to aac998c7ed)