Support serving Feldera from a URL subpath#6575
Conversation
Add an `--http-base-path` (`FELDERA_HTTP_BASE_PATH`) option so Feldera can be served behind a reverse proxy that mounts it on a subpath (e.g. `https://example.com/feldera/`) rather than at the origin root. The challenge is that SvelteKit bakes its base path in at build time — its client-side router strips the prefix off `location.pathname` using a value that is normally a compile-time constant — while the manager embeds a prebuilt console bundle into the binary, so a rebuild per deployment is not an option. We lean on SvelteKit 2's runtime base seam instead: the bundle is built with a distinctive sentinel base, and at startup the manager rewrites every occurrence of that sentinel in the embedded bundle to the configured prefix (or the empty string for a root deployment), then mounts the API under `<base>/v0` and the console under `<base>/`. A single prebuilt binary therefore serves any subpath with no rebuild; `/healthz` and the Swagger UI stay at the root for infrastructure probes and API exploration. On the web-console side, `kit.paths.base` now reads `WEBCONSOLE_BASE_PATH` (empty for local dev), and the remaining URL-handling leaks that bypassed the base path are routed through `resolve()`/`base`: the API endpoint now appends the base to the origin (fixing both REST and WebSocket calls), and the hardcoded root navigations and `pathname` comparisons in the auth flow, tenant switcher, pipelines redirect, pipeline table, and health banner are made base-aware. The manager logs the active prefix at startup and errors loudly if a base path is configured against a bundle that lacks the placeholder. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
mythical-fred
left a comment
There was a problem hiding this comment.
Draft review — high-level only, per my heartbeat policy for drafts.
The overall shape is nice: a single sentinel baked into the bundle at build time, rewritten to the operator-configured prefix at serve time. That's the right trade for "one binary, any subpath" and the write-up in the config docstring is clear about what the value must look like. A few things I'd want thought through before this is marked ready — not blockers, and I want to look at the finished version end-to-end when it is:
-
webconsole_resources(base_path)+OnceLockis a footgun waiting to happen. The lock caches on first call, keyed only implicitly by "whateverbase_paththe first caller passed." TodayHttpServercalls the factory once per worker with the same config, so it works. But nothing in the type or the docstring stops a future caller (test harness, an added CLI subcommand, a config reload path) from passing a differentbase_pathand silently getting the previously-rewritten bytes. Options:- Take the base path from a
pub staticcomputed inrun()once, and drop the argument; or - Assert (not
debug_assert!) that the cached path matches on subsequent calls; or - Compute the rewritten bundle eagerly in
run()beforeHttpServerstarts and pass a&'staticreference in.
Whichever you pick, the invariant "this function'sbase_pathargument is fixed for the process lifetime" needs to be enforced, not just documented.
- Take the base path from a
-
Missing placeholder should fail startup, not
error!and continue. A misconfigured deployment where the bundle was built withoutWEBCONSOLE_BASE_PATHset will produce a broken console that "works" enough to load and then 404s all its assets against<base>/.... A singleerror!in the log is exactly the kind of thing that gets missed. Since this is detected once at startup,bail!/return an error fromrun()— the operator can fix the config and restart, which is what you want anyway. -
Swagger UI and OpenAPI expose the un-prefixed paths.
SwaggerUi::new("/swagger-ui/...")andApiDocstill describe routes as/v0/..., but the actual API lives at<base>/v0/.... An external OpenAPI consumer generated against a subpath deployment will produce clients that hit the wrong URL. Two clean options:- Rewrite the
serversfield of the OpenAPI doc to include<base>(utoipa supports this); or - Explicitly document that
/swagger-uiand/api-doc/openapi.jsonstay at the root and describe the root-relative API surface — clients are expected to prepend<base>themselves.
Either is fine; the current state (silent divergence) is not.
- Rewrite the
-
Authentication callback / OIDC redirect paths. OIDC providers are configured with fixed redirect URIs. When a deployment moves from root to
/feldera(or vice versa), the login callback URL changes and needs to be re-registered on the IdP side. Worth a one-liner in the changelog / docs entry so operators aren't surprised. -
Cookies and localStorage. If auth cookies are set with
path=/(or unspecified — same effect), the "session" survives moving the app to a subpath, but two Feldera deployments on the same origin at different subpaths would clobber each other. If cookies are scoped to the base path, existing sessions from a root deployment become invisible after the switch. Whatever the current behavior is, it should be one of the two, deliberately, and mentioned in the config docstring. -
Base-path validation is very lenient.
/foo bar,/foo?x=1,//foo,/foo#frag, non-ASCII — none rejected. Probably fine to punt (operators own their config), but at minimum reject values containing?,#, whitespace, or a trailing/leading double slash, since those will silently break URL construction downstream. -
Test coverage.
base_path_mounts_scopes_under_prefixis the right integration test. Consider adding: (a) an end-to-end fetch of a rewritten HTML asset to confirm the sentinel actually appears, gets replaced, and doesn't survive; (b) a negative test where the bundle was built without the placeholder, asserting the "hard fail at startup" behavior once #2 lands. -
Nit for the ready-for-review pass: the constant is named
WEBCONSOLE_BASE_PATH_PLACEHOLDERbut the env var drivingsvelte.config.jsisWEBCONSOLE_BASE_PATHand it takes the placeholder value. It works, but the two names being one word apart makes the flow harder to follow. Either rename the env var toWEBCONSOLE_BASE_PATH_PLACEHOLDERend-to-end, or add a one-line comment insvelte.config.jsexplaining "this env var carries the placeholder value that the manager will rewrite at runtime."
Direction looks good. Ping me when it's ready.
Add an
--http-base-path(FELDERA_HTTP_BASE_PATH) option so Feldera can be served behind a reverse proxy that mounts it on a subpath (e.g.https://example.com/feldera/) rather than at the origin root.