different-ai/openwork · error · LocalManagedMcpPrivateUrlError

a request body cannot be redirected to another origin

Error message

a request body cannot be redirected to another origin

What it means

The redirect handler strips sensitive headers and forbids forwarding request bodies when a redirect crosses origins. A non-GET/HEAD method or any body on a cross-origin redirect throws LocalManagedMcpPrivateUrlError, because credentials or payloads would leak to the new origin.

Source

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

    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" };
}

async function guardedTransportFetch(input: string | URL, init?: RequestInit): Promise<Response> {
  // DOM, Bun, and Undici publish structurally equivalent Fetch API types from
  // different declarations. Keep the conversion isolated at this transport boundary.
  const requestInit = { ...init, dispatcher: guardedDispatcher } as unknown as UndiciRequestInit;

View on GitHub (pinned to 2b7df46e8a)

Solutions

  1. Update the configured MCP URL to the final destination so no cross-origin redirect happens
  2. Change the server redirect to same-origin (or fix the proxy rewrite) if the move is unintentional
  3. Split the call: make the initial request, then issue the follow-up request to the new origin explicitly with fresh auth
  4. Use 307/308 with a same-origin target if the method/body must be preserved

Example fix

// before
await createLocalManagedMcpConnection({ url: "https://old.example.com/mcp" }); // 302 -> https://new.example.net/mcp with POST body
// after
await createLocalManagedMcpConnection({ url: "https://new.example.net/mcp" });
Defensive patterns

Strategy: try-catch

Validate before calling

const res = await fetch(url, { method: "POST", body, redirect: "manual" });
if ([301,302,303,307,308].includes(res.status)) {
  const next = new URL(res.headers.get("location"), url);
  if (next.origin !== new URL(url).origin) throw new Error(`Cross-origin redirect to ${next.origin}`);
}

Try / catch

try {
  await guardedFetch(url, { method: "POST", body });
} catch (error) {
  if (error instanceof LocalManagedMcpPrivateUrlError && error.message.includes("cannot be redirected to another origin")) {
    // update config to the final origin, or re-issue the request explicitly there
  }
  throw error;
}

Prevention

When it happens

Trigger: A guarded fetch (via createLocalManagedMcpConnection) gets a 301/302/303/307/308 whose Location is on a different origin while the request has a body or uses POST/PUT/etc.; redirectedRequestInit throws for the new URL.

Common situations: MCP server moved to a new domain and issued a redirect on a POST; trailing-slash or path rewrites crossing subdomains (app.example.com -> api.example.com); auth-gated endpoints redirecting POSTs to a login host.

Related errors


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