From 347ab8119f2254f3944eadacb45569f852a1ca1d Mon Sep 17 00:00:00 2001 From: Swarnava Sengupta Date: Fri, 25 Sep 2026 17:55:50 +0530 Subject: [PATCH] feat(local-proxy): forward browser-facing origin Send the resolved local proxy origin in the proxy-owned request header for local HTTP targets, HTTPS fallback deployments, and WebSocket upgrades. This gives fallback applications a stable origin that is not overwritten with x-forwarded-host at the Vercel edge. Replace any inbound copy of the header before forwarding fallback requests to prevent spoofed values from winning. Document the contract, add release metadata, and cover configured and default origins in tests. --- .changeset/fallback-local-proxy-origin.md | 5 +++++ README.md | 2 ++ packages/microfrontends/README.md | 2 ++ .../microfrontends/src/bin/local-proxy.test.ts | 16 +++++++--------- packages/microfrontends/src/bin/local-proxy.ts | 12 +++++++----- 5 files changed, 23 insertions(+), 14 deletions(-) create mode 100644 .changeset/fallback-local-proxy-origin.md diff --git a/.changeset/fallback-local-proxy-origin.md b/.changeset/fallback-local-proxy-origin.md new file mode 100644 index 0000000..ee24870 --- /dev/null +++ b/.changeset/fallback-local-proxy-origin.md @@ -0,0 +1,5 @@ +--- +"@vercel/microfrontends": patch +--- + +Send the browser-facing local proxy origin to local and fallback applications in the `x-vercel-mfe-local-proxy-origin` request header. diff --git a/README.md b/README.md index 0014779..562f52f 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,8 @@ The local development proxy can preserve the origin used to reach it. Set it exp The origin must be an absolute `http` or `https` origin, such as `https://myapp.localhost`, with no path, query string, hash, username, or password. The proxy uses this origin anywhere it needs to preserve the URL clients used to reach the proxy. +Proxied requests include this value in the `x-vercel-mfe-local-proxy-origin` header, including requests that fall back to a deployed application. When no origin is configured, the header contains the proxy's default `http://localhost:` origin. + ## Need help? Reach out for help in the [Vercel Community](https://community.vercel.com). diff --git a/packages/microfrontends/README.md b/packages/microfrontends/README.md index 0014779..562f52f 100644 --- a/packages/microfrontends/README.md +++ b/packages/microfrontends/README.md @@ -27,6 +27,8 @@ The local development proxy can preserve the origin used to reach it. Set it exp The origin must be an absolute `http` or `https` origin, such as `https://myapp.localhost`, with no path, query string, hash, username, or password. The proxy uses this origin anywhere it needs to preserve the URL clients used to reach the proxy. +Proxied requests include this value in the `x-vercel-mfe-local-proxy-origin` header, including requests that fall back to a deployed application. When no origin is configured, the header contains the proxy's default `http://localhost:` origin. + ## Need help? Reach out for help in the [Vercel Community](https://community.vercel.com). diff --git a/packages/microfrontends/src/bin/local-proxy.test.ts b/packages/microfrontends/src/bin/local-proxy.test.ts index 219375a..5fb9b22 100644 --- a/packages/microfrontends/src/bin/local-proxy.test.ts +++ b/packages/microfrontends/src/bin/local-proxy.test.ts @@ -744,7 +744,7 @@ describe('class LocalProxy', () => { 'x-forwarded-proto': 'https', 'x-forwarded-host': 'vercel.localhost', 'x-forwarded-port': '443', - 'x-vercel-mfe-local-proxy-origin': '1', + 'x-vercel-mfe-local-proxy-origin': 'https://vercel.localhost', }), }), ); @@ -832,7 +832,9 @@ describe('class LocalProxy', () => { }, ); - const req = mockRequest('/a-path-that-surely-does-not-match-any-routes'); + const req = mockRequest('/a-path-that-surely-does-not-match-any-routes', { + 'x-vercel-mfe-local-proxy-origin': 'https://spoofed.example.com', + }); (req as unknown as { on: jest.Mock }).on = jest.fn(); const res = mockResponse(); (res as unknown as { on: jest.Mock }).on = jest.fn(); @@ -847,15 +849,11 @@ describe('class LocalProxy', () => { 'x-forwarded-proto': 'https', 'x-forwarded-host': 'vercel.localhost', 'x-forwarded-port': '443', + 'x-vercel-mfe-local-proxy-origin': 'https://vercel.localhost', }), }), expect.any(Function), ); - const [requestOptions] = (https.request as unknown as jest.Mock).mock - .calls[0] as [{ headers: Record }]; - expect(requestOptions.headers).not.toHaveProperty( - 'x-vercel-mfe-local-proxy-origin', - ); expect(res.writeHead).toHaveBeenCalledWith( 307, expect.objectContaining({ @@ -1101,7 +1099,7 @@ describe('class LocalProxy', () => { 'x-forwarded-proto': 'https', 'x-forwarded-host': 'vercel.localhost', 'x-forwarded-port': '443', - 'x-vercel-mfe-local-proxy-origin': '1', + 'x-vercel-mfe-local-proxy-origin': 'https://vercel.localhost', }), }), ); @@ -1137,7 +1135,7 @@ describe('class LocalProxy', () => { expect(options.headers).not.toHaveProperty('x-forwarded-host'); expect(options.headers).not.toHaveProperty('x-forwarded-port'); expect(options.headers).toMatchObject({ - 'x-vercel-mfe-local-proxy-origin': '1', + 'x-vercel-mfe-local-proxy-origin': 'http://localhost:6720', }); }); }); diff --git a/packages/microfrontends/src/bin/local-proxy.ts b/packages/microfrontends/src/bin/local-proxy.ts index 73f11c1..b48f1cd 100644 --- a/packages/microfrontends/src/bin/local-proxy.ts +++ b/packages/microfrontends/src/bin/local-proxy.ts @@ -21,9 +21,10 @@ import { localAuthHtml } from './local-auth'; import { logger } from './logger'; import type { LocalProxyApplicationResponse, LocalProxyOptions } from './types'; -// This is a header set to `1` by the local proxy on all outgoing requests to locally running applications. -// Applications may optionally route all traffic that they receive without the header to the local proxy. -// This allows the application to appear as if it is serving routes from another microfrontend. +// This header is set by the local proxy on all outgoing requests and contains the +// browser-facing origin. Applications may optionally route all traffic that they +// receive without the header to the local proxy. This allows the application to +// appear as if it is serving routes from another microfrontend. const MFE_LOCAL_PROXY_HEADER = 'x-vercel-mfe-local-proxy-origin'; // This is a query parameter that can be passed to the local proxy to be used as the value of any flags @@ -735,7 +736,7 @@ export class LocalProxy { const headers: Record = this.configuredOrigin ? forwardedHeadersForOrigin(this.getOrigin()) : {}; - headers[MFE_LOCAL_PROXY_HEADER] = '1'; + headers[MFE_LOCAL_PROXY_HEADER] = this.getOrigin(); this.proxy.ws(req, socket, head, { target: target.url, headers, @@ -818,6 +819,7 @@ export class LocalProxy { ...fallbackHeaders, host: hostname, ...forwardedHeaders, + [MFE_LOCAL_PROXY_HEADER]: origin, cookie: Object.entries(cookies) .reduce((acc, [name, value]) => { if ( @@ -920,7 +922,7 @@ export class LocalProxy { }); } else { const headers: Record = { ...forwardedHeaders }; - headers[MFE_LOCAL_PROXY_HEADER] = '1'; + headers[MFE_LOCAL_PROXY_HEADER] = origin; if (mfeFlagValue !== undefined) { headers[MFE_FLAG_VALUE_HEADER] = mfeFlagValue.toString(); }