Skip to content

Support serving Feldera from a URL subpath#6575

Draft
Karakatiza666 wants to merge 1 commit into
mainfrom
base-path
Draft

Support serving Feldera from a URL subpath#6575
Karakatiza666 wants to merge 1 commit into
mainfrom
base-path

Conversation

@Karakatiza666

Copy link
Copy Markdown
Contributor

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.

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 mythical-fred left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. webconsole_resources(base_path) + OnceLock is a footgun waiting to happen. The lock caches on first call, keyed only implicitly by "whatever base_path the first caller passed." Today HttpServer calls 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 different base_path and silently getting the previously-rewritten bytes. Options:

    • Take the base path from a pub static computed in run() 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() before HttpServer starts and pass a &'static reference in.
      Whichever you pick, the invariant "this function's base_path argument is fixed for the process lifetime" needs to be enforced, not just documented.
  2. Missing placeholder should fail startup, not error! and continue. A misconfigured deployment where the bundle was built without WEBCONSOLE_BASE_PATH set will produce a broken console that "works" enough to load and then 404s all its assets against <base>/.... A single error! in the log is exactly the kind of thing that gets missed. Since this is detected once at startup, bail!/return an error from run() — the operator can fix the config and restart, which is what you want anyway.

  3. Swagger UI and OpenAPI expose the un-prefixed paths. SwaggerUi::new("/swagger-ui/...") and ApiDoc still 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 servers field of the OpenAPI doc to include <base> (utoipa supports this); or
    • Explicitly document that /swagger-ui and /api-doc/openapi.json stay 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.
  4. 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.

  5. 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.

  6. 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.

  7. Test coverage. base_path_mounts_scopes_under_prefix is 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.

  8. Nit for the ready-for-review pass: the constant is named WEBCONSOLE_BASE_PATH_PLACEHOLDER but the env var driving svelte.config.js is WEBCONSOLE_BASE_PATH and 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 to WEBCONSOLE_BASE_PATH_PLACEHOLDER end-to-end, or add a one-line comment in svelte.config.js explaining "this env var carries the placeholder value that the manager will rewrite at runtime."

Direction looks good. Ping me when it's ready.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants