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
- Update the configured MCP URL to the final destination so no cross-origin redirect happens
- Change the server redirect to same-origin (or fix the proxy rewrite) if the move is unintentional
- Split the call: make the initial request, then issue the follow-up request to the new origin explicitly with fresh auth
- 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
- Configure the final, canonical MCP URL so no cross-origin redirects occur
- Keep redirects same-origin (trailing-slash rules included)
- Never rely on redirects to carry authenticated POST requests to new domains
- Strip/re-establish auth headers manually when an endpoint genuinely moves origins
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
- an HTTPS request cannot redirect to a less secure protocol
- Managed MCP outbound request exceeded the guarded redirect l
- The MCP discovery response exceeded the 1 MiB limit.
- request_failed
- External URL protocol "${parsed.protocol}" is not allowed.
AI-assisted analysis of different-ai/openwork@2b7df46e8a (2026-09-01).
Data as JSON: /api/errors/5772d59836e5bc4b.
Report an issue: GitHub.