santifer/career-ops · error · Error
plugin egress must use HTTPS
Error message
plugin egress must use HTTPS: ${u.href} What it means
The plugin sandbox's guarded fetch wrapper (hostOk) enforces HTTPS for all plugin network egress. Plain HTTP is permitted only when the plugin's allowsLocalhost opt-in is set AND the URL host is loopback (localhost, 127.x.x.x, ::1) — the carve-out for local AI providers like Ollama. Any other http:// URL throws this error before the request is made.
Solutions
- Change the plugin's URL to https://
- Run a local TLS proxy or expose the service over HTTPS
- If the service is genuinely local (Ollama/LM Studio), use http://localhost:<port> and ensure the plugin's allowsLocalhost opt-in is enabled in its manifest/config
Example fix
// before
const res = await ctx.fetch('http://api.example.com/data');
// Error: plugin egress must use HTTPS: http://api.example.com/data
// after
const res = await ctx.fetch('https://api.example.com/data'); Defensive patterns
Strategy: validation
Validate before calling
const u = new URL(target);
const isLoopback = (h) => /^(localhost|127\.\d+\.\d+\.\d+|\[?::1\]?)$/i.test(h);
if (u.protocol !== 'https:' && !(allowsLocalhost && u.protocol === 'http:' && isLoopback(u.hostname))) {
throw new Error(`upgrade to HTTPS before calling ctx.fetch: ${u.href}`);
} Type guard
const isHttpsOrAllowedLoopback = (u, allowsLocalhost) => u.protocol === 'https:' || (allowsLocalhost && u.protocol === 'http:' && /^(localhost|127\.\d+\.\d+\.\d+|\[?::1\]?)$/i.test(u.hostname));
Try / catch
try { return await ctx.fetch(url); } catch (e) { if (String(e.message).startsWith('plugin egress must use HTTPS')) { throw new Error(`${e.message} — switch the endpoint to https:// or run a local TLS proxy`); } throw e; } Prevention
- Default all plugin endpoints to https:// from the start
- Only use http:// for genuinely local services (Ollama/LM Studio) on localhost
- Check the egress policy in the plugin manifest before adding new endpoints
When it happens
Trigger: A plugin calling ctx.fetch('http://example.com/...') (non-loopback host); or an http:// non-loopback URL even with allowsLocalhost enabled.
Common situations: A plugin pointing at an internal dev API over HTTP; hardcoded http:// URLs in plugin code after migrating to the sandbox; testing against a staging server without TLS.
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
- plugin egress to " " is not in allowedHosts [ ]
- a16z-speedrun-talent: URL must use HTTPS
- Access denied: Egress guard blocked private target IP
- Access denied: Egress guard blocked private target IP
- agentic-jobs: URL must use HTTPS
AI-assisted analysis of santifer/career-ops@aac998c7ed (2026-09-16).
Data as JSON: /api/errors/0b0b64534a6941a4.
Report an issue: GitHub.
Appendix: source
Thrown at plugins/_engine.mjs:391
* 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 aac998c7ed)