Add MFE dev-mode proxy for local development

What does this MR do and why?

Local-development flow for delivered MFEs: a dev-/test-only route /-/mfe/:name/dev/*path proxies the dev magic version to per-MFE local dev servers, so applications hot-reload in GDK without ever touching the production delivery path:

  • Mfe::DevProxyController — mounted only under Gitlab.dev_or_test_env?, so it can never reach a production route table; kept separate from the artifact delivery endpoint so the production serve path never contains proxy logic.
  • Gitlab::Mfe dev API: GITLAB_MFE_DEV_APPS (which apps are served from dev servers), GITLAB_MFE_DEV_SERVERS (name=url,… map), GITLAB_MFE_DEV_SERVER (single-server fallback) — several MFEs can be developed simultaneously. An app in dev mode without any configured server is a configuration error and raises (Gitlab::Mfe::MissingDevServerError): there is deliberately no default server to silently guess at.
  • Dev-server websocket origins are appended to the development CSP connect-src, so HMR connections are allowed.

How to verify

bin/rspec spec/requests/mfe/dev_proxy_spec.rb spec/lib/gitlab/mfe_spec.rb \
  spec/lib/gitlab/content_security_policy/config_loader_spec.rb

End-to-end in GDK with a real application (Duo Chat)

Browser-verified walkthrough; no consumer code is required — a console snippet proves serving, execution, and HMR.

1. Run the Duo Chat dev server (https://gitlab.com/gitlab-org/frontend/gitlab-mfe):

git clone https://gitlab.com/gitlab-org/frontend/gitlab-mfe && cd gitlab-mfe
pnpm install
pnpm dev:rspack   # serves the remote on http://localhost:4001

The dev server needs three small config fixes to be consumable through the monolith proxy (HMR socket target, Host/Origin check, rspack's lazy compilation posting to the page origin where Rails answers 422). They ship as a separate merge request to that project; until it merges, apply this diff before pnpm dev:rspack:

diff --git a/gitlab-shared/rspack-config/src/index.ts b/gitlab-shared/rspack-config/src/index.ts
index fb9318b..afd38fa 100644
--- a/gitlab-shared/rspack-config/src/index.ts
+++ b/gitlab-shared/rspack-config/src/index.ts
@@ -256,7 +256,22 @@ export function defineGitLabRspackConfig(options: GitLabRspackConfigOptions): Co
         'Access-Control-Allow-Origin': '*',
       },
       hot: true,
+      // The websocket carries the monolith page's Origin (gdk.test), which
+      // the dev server's Host/Origin check would reject. The server only
+      // binds localhost, so relaxing the check is safe.
+      allowedHosts: 'all',
+      // The monolith consumes this server through its same-origin dev proxy
+      // (/-/mfe/<name>/dev/...), so the page origin is the monolith, not this
+      // server. The HMR client must therefore be pointed at this server
+      // explicitly; otherwise it derives the socket URL from the page origin.
+      client: {
+        webSocketURL: `ws://localhost:${port}/ws`,
+      },
     },
+    // rspack serve enables lazy compilation; its client posts module
+    // activity to the page origin, which is the monolith when proxied -
+    // Rails answers 422 and the remote fails to mount.
+    lazyCompilation: false,
   };
 
   return config;

2. Point GDK at it — in <gdk>/env.runit:

export GITLAB_MFE_DEV_APPS=duo_chat
export GITLAB_MFE_DEV_SERVERS=duo_chat=http://localhost:4001

then gdk restart rails-web (delivery gates also apply: mfe.enabled: true in gitlab.yml + Feature.enable(:mfe_enabled)).

3. Proxy check:

curl -s http://gdk.test:3000/-/mfe/duo_chat/dev/mf-manifest.json | head -c 120   # the dev server's live manifest

4. Execution check — on any GDK page, in the browser console:

s = document.createElement('script'); s.src = '/-/mfe/duo_chat/dev/remoteEntry.js'; document.head.append(s);
// a moment later:
window.mfe_duo_chat   // => the federation container, served and executed through the proxy

The rspack dev client inside the bundle also connects its HMR websocket to ws://localhost:4001/ws — allowed by this MR's development-CSP change (visible in the Network tab, status 101).

5. HMR check: touch mfes/duo-chat/src/components/duo_chat/duo_chat_view.vue — the console logs [HMR] Updated modules: ... App is up to date. without a page reload.

6. Fail-loud misconfiguration: remove GITLAB_MFE_DEV_SERVERS (keeping GITLAB_MFE_DEV_APPS) and restart — any page raises Gitlab::Mfe::MissingDevServerError with instructions, by design: there is no default server to silently guess at.

In production builds none of this exists — the route is mounted only under Gitlab.dev_or_test_env?.

Stack

This is part of a stacked series extracted from a single 1700+ line MR for reviewability. Each part is independently verifiable and leaves master inert: nothing is user-reachable until an application is pinned.

# Scope MR
1 Config plumbing + instance kill switch !245043 (closed)
2 Sidecar manifest validation + pin-file catalog !245044 (closed)
3 Verified bake task + baked manifest !245045 (closed)
4 Same-origin delivery endpoint !245046 (closed)
5 Dev-mode proxy for local development !245301 (closed)

Merging bottom-up; when a part merges, GitLab retargets the next one to master automatically.

A separate demonstration MR shows the whole stack working end to end with the real Duo Chat MFE (production bake + dev-mode HMR): !245014.

Edited by Illya Klymov

Merge request reports

Loading