santifer/career-ops · error · Error

plugin egress must use HTTPS: ${u.href}

Error message

plugin egress must use HTTPS: ${u.href}

What it means

Thrown by the guardedFetch host check in plugins/_engine.mjs when a plugin egress URL's protocol is not https:, unless it is an explicitly opted-in loopback HTTP host (allowsLocalhost + http: + a loopback hostname, to support local-AI providers like Ollama/LM Studio on http://localhost:11434). This enforces TLS for all plugin network egress by default.

Source

Thrown at plugins/_engine.mjs:374

 * Posture note: core providers use redirect:'error' (reject ANY redirect). This
 * is the deliberately looser plugin posture — allowlist-pinned FOLLOW — because
 * keyed APIs (Notion/Google/Apify) legitimately 30x within their own host set;
 * the allowlist + per-hop re-validation + cross-host credential strip bound it.
 *
 * ADVISORY only: this binds a plugin that routes through ctx.fetch*, not one
 * that calls global fetch directly (see the trust note in README.md).
 *
 * @param {string[]} allowedHosts
 */
function makeGuardedFetch(allowedHosts, { allowsLocalhost = false } = {}) {
  const allow = new Set(allowedHosts);
  const isLoopbackHost = (h) => /^(localhost|127\.\d+\.\d+\.\d+|\[?::1\]?)$/i.test(h);
  const hostOk = (u) => {
    if (u.protocol !== 'https:') {
      // Plain HTTP is allowed ONLY for an opted-in loopback host (local-AI
      // providers like Ollama/LM Studio serve http://localhost:11434).
      if (!(allowsLocalhost && u.protocol === 'http:' && isLoopbackHost(u.hostname))) {
        throw new Error(`plugin egress must use HTTPS: ${u.href}`);
      }
    }
    if (allow.size > 0 && !allow.has(u.hostname)) throw new Error(`plugin egress to "${u.hostname}" is not in allowedHosts [${[...allow].join(', ')}]`);
  };
  return async function guardedFetch(url, opts = {}) {
    const { timeoutMs = 10_000, headers = {}, method = 'GET', body = null } = opts;
    let current = new URL(url);
    hostOk(current);
    // SSRF: reject a host that resolves to a private/loopback/metadata address
    // (re-checked on every redirect hop). Loopback allowed only when opted in.
    await resolveAndValidate(current.hostname, { allowsLocalhost });
    let reqHeaders = { ...headers };
    for (let hop = 0; hop <= MAX_REDIRECTS; hop++) {
      const controller = new AbortController();
      const timer = setTimeout(() => controller.abort(), timeoutMs);
      let res;
      try {
        res = await fetch(current.href, {

View on GitHub (pinned to 9b17a8ac97)

Solutions

  1. Change the plugin's request URL to its https:// equivalent.
  2. For a legitimate local-AI provider, declare allowsLocalhost in the plugin's network permission so http:// to loopback is permitted.
  3. Ensure no redirect downgrades https to http (the guard re-checks each hop).
  4. Review the plugin manifest's allowedHosts/network config.

Example fix

// before
await ctx.fetch('http://api.example.com/data');
// after
await ctx.fetch('https://api.example.com/data');
Defensive patterns

Strategy: validation

Validate before calling

function isHttpsOrAllowedLoopback(s, { allowsLocalhost } = {}) {
  const u = new URL(s);
  if (u.protocol === 'https:') return true;
  const loopback = /^(localhost|127\.\d+\.\d+\.\d+|\[?::1\]?)$/i.test(u.hostname);
  return allowsLocalhost && u.protocol === 'http:' && loopback;
}

Type guard

/** Narrows a URL string to one the plugin egress guard will accept. */
function isAllowedEgressUrl(s, { allowsLocalhost } = {}) {
  if (typeof s !== 'string') return false;
  let u;
  try { u = new URL(s); } catch { return false; }
  if (u.protocol === 'https:') return true;
  const loopback = /^(localhost|127\.\d+\.\d+\.\d+|\[?::1\]?)$/i.test(u.hostname);
  return !!(allowsLocalhost && u.protocol === 'http:' && loopback);
}

Prevention

When it happens

Trigger: A plugin calls ctx.fetch('http://api.example.com/...') without TLS; a redirect lands on an http:// target; a plugin targets a local-AI server but allowsLocalhost was not enabled for it.

Common situations: Plugin author used http:// during dev and forgot to switch to https://; a third-party endpoint redirects from https to http; local Ollama/LM Studio integration without opting in allowsLocalhost in the manifest/config.

Related errors


AI-assisted analysis of santifer/career-ops@9b17a8ac97 (2026-08-13). Data as JSON: /api/errors/0b0b64534a6941a4. Report an issue: GitHub.