feat(relay): serve the relay under an optional BUZZ_BASE_PATH prefix - #3372
feat(relay): serve the relay under an optional BUZZ_BASE_PATH prefix#3372kcao-gss wants to merge 1 commit into
Conversation
The relay mounted every route at the root, so it could not be deployed
behind a gateway that routes to apps by path rather than by hostname.
Serving a WebSocket at `/` is not an option on such a platform, which
left self-hosters with a path-routing ingress no choice but a fork.
Add `BUZZ_BASE_PATH`, empty by default. When set, the whole surface nests
under the prefix: the WebSocket and NIP-11 handshake answer on
`wss://host/<prefix>` and the HTTP routes live beneath it. Health probes
stay mounted at the root as well, since Kubernetes reaches the pod
directly rather than through the gateway.
The prefix is normalized at load time (one leading slash, no trailing
slash) and values that would make the server's URL reconstruction differ
from the client's connect URL — query, fragment, whitespace, dot or empty
segments — fail startup rather than surfacing later as an unexplainable
401.
Threaded through every place a URL is rebuilt from the request host, since
each one is signed and compared:
- `nip98_expected_url` — the single choke point for bridge, invite,
and moderation `u` tags
- `nip42_expected_relay_url` — the WebSocket AUTH `relay` tag
- the operator API's expected URL
- the NIP-11 `push.origin` descriptor, which push clients dial verbatim
Client-side, three places assumed the relay owned the origin root:
`buzz-cli` rejected prefixed media URLs as non-media, the desktop
Download affordance keyed on a root-anchored `/media/` path, and mobile
used `Uri.resolve('/query')`, which discards a base path. Mobile now
joins endpoints through a shared `relayEndpoint` helper.
Known limitation, documented on the config field: the browser web bundle
bakes absolute `/assets/...` URLs at build time, so serving that SPA under
a prefix also needs a matching Vite `base`. The protocol surface is
unaffected.
Closes block#3354
Signed-off-by: Kyler Cao <kcao@gssmail.com>
|
CI here needs a maintainer to approve the workflow run (first-time fork contributor). To save you that round trip, I ran the same workflows on my fork: kcao-gss#1 — 22 of 23 CI jobs green. Green there, including everything I couldn't run locally:
Two jobs fail on the fork for permission reasons that won't reproduce here:
Neither touches code paths in this PR. Locally I also ran the relay lib suite against a live Postgres: 778 passed, 0 failed. Happy to rebase or split this if the shape isn't what you want before you spend CI minutes on it. |
|
Per CONTRIBUTING, the closest existing work I could find — I'd missed this line when I opened the PR. Closest open PR: #2527 ("relay: support host aliases so in-cluster clients can bind to a community"). Same family of problem — making the relay reachable in a deployment where the public hostname doesn't apply — but the orthogonal axis. #2527 widens which hosts map to a community; this PR changes which path the relay answers on. No functional overlap, and no file overlap except Worth noting the two compose: a deployment could use aliases to reach a community by internal DNS and a base path to sit behind a path-routing gateway. Also adjacent but not overlapping: #2736 (bundles the pair-relay sidecar for deploy) and #2617 (documents No duplicate of this change found in open PRs or issues. |
…res (#3396) ## Problem The prerequisites table lists language toolchains (Rust, Node, pnpm, Flutter, Docker, `just`) but no system libraries. Hermit pins the former and not the latter, so following the setup section exactly on Linux still leaves `just ci` unable to run: it fails partway through its first dependency, `just check`, at `desktop-tauri-clippy`. ``` The system library `gdk-pixbuf-2.0` required by crate `gdk-pixbuf-sys` was not found. The file `gdk-pixbuf-2.0.pc` needs to be installed and the PKG_CONFIG_PATH environment variable must contain its parent directory. ``` The desktop crates link against GTK and WebKitGTK. CI installs those packages explicitly, so it never sees this — which is exactly why the gap is invisible from the maintainer side. Since `check` runs first in the `ci` chain, the failure also masks everything after it (`test-unit`, `desktop-test`, `web-build`, `mobile-test` never run), which makes it read as a broken repo rather than a missing dependency. ## Change Adds a `#### Linux: Tauri system libraries` subsection under Prerequisites with: - The apt list copied from `.github/workflows/ci.yml`, so a local run matches CI rather than drifting from it - A pointer to [Tauri's prerequisites](https://tauri.app/start/prerequisites/) for non-Debian distributions - A note that server-side contributors can skip it — `just fmt-check`, `just clippy`, `just test-unit`, and `just test` need no GTK Docs only. No TOC entry needed, since the TOC lists `##` headings and this is a `####` subsection. ## How I hit it Running `just ci` before pushing #3372, on Ubuntu under WSL2 with the Hermit toolchain active and all Docker services healthy. Everything the guide asks for was in place. The four `check` steps before `desktop-tauri-clippy` (`fmt-check`, `clippy`, `desktop-check`, `desktop-tauri-fmt-check`) passed, which is what makes the failure point specific rather than a general build problem. ## Closest existing work None found. I searched open and closed issues and PRs for `gdk-pixbuf`, `libgtk`, `webkit2gtk`, `system dependencies`, `prerequisites`, `just ci`, and `linux setup`. The Linux/GTK issues that exist (#2604, #2643, #2982, #2811, #2562) are all runtime bugs in shipped builds, not setup-path failures. ## Verification The package list is transcribed from `.github/workflows/ci.yml:152-163`; the same list appears in `release.yml` and `linux-canary.yml`. I have not installed the packages on my machine, so I can confirm the failure and the source of the fix but not that the list is exhaustive on a clean box — worth a second pair of eyes from anyone who has done a fresh Linux setup recently. Signed-off-by: Kyler Cao <kcao@gssmail.com>
Closes #3354.
Why
buzz-relaymounted every route at the root, so it could not be deployed behind a gateway that routes to apps by path rather than by hostname. That shape is common in shared-ingress Kubernetes setups: each app is assigned a path prefix, auth policy is applied per prefix, and the root belongs to the gateway rather than to any one app. A relay that can only serve at/has nowhere to go.Compose behind a dedicated hostname works, so this is not a blocker for every self-hoster — but a path-routed ingress previously left no option except a fork.
What
BUZZ_BASE_PATH, empty by default. Unset, the router is byte-identical to before. When set, the whole surface nests under the prefix — the WebSocket and NIP-11 handshake answer onwss://host/<prefix>, the HTTP routes live beneath it.neststrips the prefix before the inner router sees the request, so route matches and the SPA fallback's own path checks keep working unchanged.Health probes stay mounted at the root as well: Kubernetes reaches the pod directly rather than through the gateway that needs the prefix, and
deploy/composecurls/_livenesson the published port.The prefix is normalized at load time (one leading slash, no trailing slash —
relay,/relay,/relay/all become/relay). Values that would make the server's URL reconstruction differ from the client's connect URL — query, fragment, whitespace, dot segments, empty interior segments — fail startup rather than surfacing later as an unexplainable 401.The part that needed care
The prefix has to reach every place a URL is rebuilt from the request host, because each one is signed and compared byte-for-byte:
nip98_expected_urlutag for bridge, invite, and moderation calls — one choke point for all of themnip42_expected_relay_urlrelaytagauthorize_operator_requestpush.originMiss one and it reads as a signature bug, not a routing bug.
Client-side sweep
Three places assumed the relay owned the origin root:
buzz-clirejected a prefixed media URL as "not a /media/ path"; it now strips the relay URL's own path before the check. The sha-shorthand form was already correct./media/pathname. Now matches/media/as a full path segment anywhere, still origin-pinned, so/media-evil/<hash>is still rejected.Uri.resolve('/query')in three places, which discards a base path outright. These now go through a sharedrelayEndpointhelper.One existing mobile test had codified the prefix-dropping behavior against a base URL of
https://relay.example/base. Updated — and its NIP-98utag assertion now readshttps://relay.example/base/query, which is the cross-check that the client and the relay agree on the prefixed URL.Verified clean:
relayHttpFromWs(desktop) is a pure scheme swap andnormalize_relay_url(CLI) is a scheme swap plus trailing-slash strip, so both carry a prefix through already.Known limitation
The browser web bundle (
BUZZ_WEB_DIR) references its assets at absolute/assets/...URLs baked in at build time, so serving that SPA under a prefix additionally requires rebuilding it with a matching Vitebase. Documented on the config field. The protocol surface — WebSocket, bridge, media, git — is unaffected.Testing
17 new tests: prefix normalization and rejection, the default-empty path, router resolution under single- and multi-segment prefixes, root passthrough for probes, 404 for unprefixed application paths, both signed-URL builders, the push descriptor, the CLI media URL, and the mobile endpoint helper.
Suites run:
buzz-relay811 lib tests,buzz-cli253, desktop 3733, mobile 874.cargo fmt --check, clippy on both crates,flutter analyze, biome lint, andcargo check --workspace --all-targetsall clean.Nine relay tests and one mobile test fail in my environment; I confirmed each one fails identically on a clean tree. The relay ones are
Sqlx(PoolTimedOut)— they need Postgres, which I did not have running.Compatibility
BUZZ_BASE_PATHunset is the default and changes nothing: same routes, same signed-URL strings, same NIP-11 document. Operators opting in must set the same prefix onRELAY_URLandBUZZ_MEDIA_BASE_URL.