different-ai/openwork · error · LocalManagedMcpPrivateUrlError

an HTTPS request cannot redirect to a less secure protocol

Error message

an HTTPS request cannot redirect to a less secure protocol

What it means

When following redirects, the guarded fetch refuses to let an https: request be redirected to a non-https protocol (http:, ftp:, etc.). This blocks protocol-downgrade attacks during the redirect chain and throws LocalManagedMcpPrivateUrlError for the target URL.

Source

Thrown at apps/server/src/local-managed-mcp-url-guard.ts:208

type FetchLike = (url: string | URL, init?: RequestInit) => Promise<Response>;
const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
const guardedDispatcher = new Agent({
  connect: {
    lookup: createLocalManagedMcpPublicLookup(),
    // Node's 250 ms family-attempt default is too aggressive for otherwise
    // healthy dual-stack MCP providers on some macOS networks. Keep fallback
    // enabled, but give the first family enough time to establish TLS before
    // trying the validated alternative address.
    autoSelectFamily: true,
    autoSelectFamilyAttemptTimeout: 1_000,
  },
});

function redirectedRequestInit(init: RequestInit | undefined, status: number, from: URL, to: URL): RequestInit {
  const headers = new Headers(init?.headers);
  if (from.protocol === "https:" && to.protocol !== "https:") {
    throw new LocalManagedMcpPrivateUrlError(to.toString(), "an HTTPS request cannot redirect to a less secure protocol");
  }
  const method = (init?.method ?? "GET").toUpperCase();
  if (from.origin !== to.origin) {
    if ((method !== "GET" && method !== "HEAD") || init?.body != null) {
      throw new LocalManagedMcpPrivateUrlError(to.toString(), "a request body cannot be redirected to another origin");
    }
    for (const name of ["authorization", "cookie", "proxy-authorization", "mcp-session-id", "last-event-id", "x-api-key", "x-auth-token"]) {
      headers.delete(name);
    }
  }
  const switchToGet = (status === 303 && method !== "HEAD") || ((status === 301 || status === 302) && method === "POST");
  if (switchToGet) {
    headers.delete("content-length");
    headers.delete("content-type");
    return { ...init, method: "GET", body: undefined, headers, redirect: "manual" };
  }
  return { ...init, headers, redirect: "manual" };
}

View on GitHub (pinned to 2b7df46e8a)

Solutions

  1. Fix the server/proxy redirect so it targets an https: URL
  2. If the destination only supports HTTP, use it directly as an intentionally insecure connection via the allowPrivateUrls() path (dev only)
  3. Trace the redirect chain (curl -I) to find which hop downgrades the scheme
  4. Update hardcoded redirect/base-URL config to include the https scheme

Example fix

// before (server)
return Response.redirect("http://mcp.example.com/v2", 302);
// after
return Response.redirect("https://mcp.example.com/v2", 302);
Defensive patterns

Strategy: try-catch

Validate before calling

const res = await fetch(url, { redirect: "manual" });
if ([301,302,303,307,308].includes(res.status)) {
  const next = new URL(res.headers.get("location"), url);
  if (next.protocol !== "https:") throw new Error(`Redirect downgrades to ${next.protocol}`);
}

Try / catch

try {
  await createLocalManagedMcpConnection({ url });
} catch (error) {
  if (error instanceof LocalManagedMcpPrivateUrlError && error.message.includes("less secure protocol")) {
    // inspect server redirect chain; fix scheme on the redirecting hop
  }
  throw error;
}

Prevention

When it happens

Trigger: An HTTPS MCP request receives a 301/302/303/307/308 response whose Location header points at an http: (or otherwise non-https) URL, while private URLs are not allowed.

Common situations: Misconfigured reverse proxy or load balancer redirecting https to http; redirect target built with a wrong scheme in a server config; intercepted redirect chain during corporate proxy rewriting.

Related errors


AI-assisted analysis of different-ai/openwork@2b7df46e8a (2026-09-01). Data as JSON: /api/errors/58c65012a8bb2d09. Report an issue: GitHub.