santifer/career-ops · error

garena: is not a usable URL segment

Error message

garena: ${name} is not a usable URL segment: ${JSON.stringify(value)}

What it means

urlSegment in providers/garena.mjs builds URL path segments by encodeURIComponent-ing configured values (e.g. office names). Before encoding it rejects the values '.' and '..' outright, because percent-encoded dot segments can still be interpreted as relative path traversal by some servers. The error names the field (name) and shows the offending value as JSON.

Solutions

  1. Set the office field in the portals.yml garena entry to a real office slug (a non-empty name without dot segments).
  2. Check resolveOffice's fallback: if the office key is missing, provide it explicitly instead of letting a degenerate value flow in.
  3. Strip or validate config values before they reach the provider (reject '', '.', '..' at config-load time).
  4. Re-run the scan after fixing the entry to confirm a usable URL is built.

Example fix

// before
garena:
  office: "."   # rejected by urlSegment
// after
garena:
  office: "singapore"
Defensive patterns

Strategy: validation

Validate before calling

function isSafeSegment(v) {
  return typeof v === 'string' && v.length > 0 && v !== '.' && v !== '..';
}
if (!isSafeSegment(entry?.garena?.office)) {
  throw new Error(`config: garena.office must be a real slug for "${entry.name}"`);
}

Type guard

const isSafeUrlSegment = (v) =>
  typeof v === 'string' && v.length > 0 && v !== '.' && v !== '..';

Try / catch

try {
  const jobs = parseGarenaResponse(json, entry);
} catch (e) {
  if (e.message.includes('is not a usable URL segment')) {
    console.error(`Fix garena.office for "${entry.name}" — got a dot segment`);
    return [];
  }
  throw e;
}

Prevention

When it happens

Trigger: A portals.yml garena.office entry set to '.', '..', or a value that resolves to one of these via resolveOffice (e.g. empty/missing config falling through to a dot, or a stray '.' left in the config by hand).

Common situations: A config cleanup that replaced an office name with '.' meaning 'current directory' by habit; an empty office value coerced into a dot segment upstream; copy-pasting a filesystem-style relative path into the office field.

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


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

Appendix: source

Thrown at providers/garena.mjs:95

/** @param {{ garena?: { office?: string } }} entry */
function resolveOffice(entry) {
  const office = entry && entry.garena && entry.garena.office;
  return typeof office === 'string' && office.trim() ? office.trim() : DEFAULT_OFFICE;
}

/**
 * Escapes a config-derived value before it is interpolated into a Garena URL.
 * `office` is a `portals.yml` segment, so a malformed one is a config bug and
 * should fail loudly: separators (`/`, `?`, `#`, ...) are percent-escaped, and
 * `.`/`..` are rejected outright because escaping leaves them intact as
 * traversal segments.
 * @param {string} name - Field name, for the error message.
 * @param {string} value
 * @returns {string}
 */
function urlSegment(name, value) {
  if (value === '.' || value === '..') {
    throw new Error(`garena: ${name} is not a usable URL segment: ${JSON.stringify(value)}`);
  }
  return encodeURIComponent(value);
}

/**
 * Escapes the per-posting `id` from the API response. Unlike `urlSegment`, a
 * bad value here returns `null` rather than throwing: `id` is host-controlled
 * and sits inside the parse loop, so a lone surrogate (URIError) or a `.`/`..`
 * traversal segment must drop only that posting, not unwind the whole page
 * (#3513).
 * @param {string} id
 * @returns {string | null}
 */
function idUrlSegment(id) {
  if (id === '.' || id === '..') return null;
  return safeEncodeURIComponent(id);
}

View on GitHub (pinned to aac998c7ed)