Skip to content

feat(query): Auto-detect UI base path from browser URL (ADR-009)#8568

Merged
yurishkuro merged 34 commits into
jaegertracing:mainfrom
yurishkuro:auto-ui-prefix
May 13, 2026
Merged

feat(query): Auto-detect UI base path from browser URL (ADR-009)#8568
yurishkuro merged 34 commits into
jaegertracing:mainfrom
yurishkuro:auto-ui-prefix

Conversation

@yurishkuro
Copy link
Copy Markdown
Member

@yurishkuro yurishkuro commented May 12, 2026

Which problem is this PR solving?

Jaeger UI today requires --query.base-path to be set correctly on the backend whenever the UI is served at a URL prefix. This has two fundamental limitations:

Resolves #5157

Description of the changes

ADR-009 (docs/adr/009-ui-base-path-auto-detection.md): documents the problem, the three use cases, the design decision, and alternatives considered.

UI change (companion PR jaeger-ui#3874): replaces the static <base href="/"> in index.html with an inline synchronous <script> that reads window.location.pathname — the actual external URL as seen by the browser — strips any known SPA sub-path suffix, and writes the correct <base href> before the browser's preload scanner processes any asset tags. Falls back to stripping the last path segment for unknown routes, and shows a plain-text error message in #jaeger-ui-root if React fails to mount.

Backend change (static_handler.go): removes the regexp-based <base href> injection from loadAndEnrichIndexHTML. The --query.base-path flag is kept — it still controls HTTP route registration (e.g. /jaeger/api/traces). RegisterRoutes validates and normalizes BasePath (panics if no leading /, trims trailing /).

E2E tests (examples/reverse-proxy/, scripts/e2e/ui-reverse-proxy.sh): a single Docker Compose file in examples/reverse-proxy/ brings up three isolated stacks (three Jaeger + three Apache httpd containers on separate networks), then the driver script verifies all three use cases:

  • UC-1 (port 18080): proxy forwards prefix unchanged — exercises the existing examples/reverse-proxy/ setup
  • UC-2 (port 18081): single pod, two external prefixes (/ and /alt/) — previously impossible
  • UC-3 (port 18082): proxy rewrites /external//internal/ — the previously broken case (issue [Feature]: Support external URL prefix #5157)

Each UC checks: 301 redirect for no-trailing-slash URL, index.html served, inline script present, all static assets reachable, API endpoints (/api/services, /api/operations) reachable, unknown-route fallback message present, and trace deep-link (when a trace is available).

Run locally with: make ui-reverse-proxy-integration-test (builds UI + binary + Docker image automatically). Use PAUSE=true to keep containers alive after the tests pass for manual inspection.

CI (.github/workflows/ci-e2e-ui-reverse-proxy.yml): new workflow builds the UI (temporarily from unmerged jaeger-ui PR #3874) + Jaeger binary + Docker image and runs the e2e script; wired into ci-e2e-all.yml. The JAEGER_UI_PR mechanism will be removed once the companion UI PR is merged and the submodule is pinned.

How was this change tested?

  • Backend unit tests in static_handler_test.go: all test cases assert data-inject-target="BASE_URL" marker presence (backend must not rewrite the base tag); TestRegisterRoutesInvalidBasePath covers invalid BasePath values.
  • 46 unit tests for the prefix-detection logic in the companion jaeger-ui PR.
  • All three E2E use cases pass locally (UC-1/2/3 PASSED).

AI Usage in this PR (choose one)

  • Heavy: AI generated most or all of the code changes

yurishkuro and others added 4 commits May 12, 2026 13:56
Propose replacing backend injection of <base href> with an inline
script in index.html that detects the URL prefix from window.location
at page-load time. This removes the need for --query.base-path to
control UI delivery, enables a single pod to serve under multiple
external prefixes, and fixes the proxy-rewrite use case (issue jaegertracing#5157)
where the external prefix differs from the internal one.

Signed-off-by: Yuri Shkuro <github@ysh.us>
The UI now auto-detects its URL prefix via an inline script in
index.html (see jaeger-ui commit). Remove the basePathPattern regexp
and the base-path rewrite logic from loadAndEnrichIndexHTML; the
--query.base-path flag is retained for HTTP route registration only.

Update static_handler_test.go to reflect that the backend no longer
rewrites the <base> tag, and remove the now-irrelevant invalid-base-
path validation test cases from TestNewStaticAssetsHandlerErrors.

Also expand ADR-009 implementation plan with per-step detail.

Signed-off-by: Yuri Shkuro <github@ysh.us>
Document unit tests (detect-base-path, static_handler) and manual
integration tests covering all three use cases: UC-1 direct access,
UC-2 single pod multiple prefixes, and UC-3 proxy prefix rewrite.

Signed-off-by: Yuri Shkuro <github@ysh.us>
Add three Docker Compose stacks under scripts/e2e/reverse-proxy/ (UC-1,
UC-2, UC-3) with permanent httpd.conf and docker-compose.yml files, a
driver script scripts/e2e/ui-reverse-proxy.sh, a GitHub Actions workflow
ci-e2e-ui-reverse-proxy.yml, and a make target
ui-reverse-proxy-integration-test.

Also add JAEGER_UI_DIR variable to BuildBinaries.mk and rebuild-ui.sh to
allow building from an external jaeger-ui checkout instead of the
submodule (used during local development of the ADR-009 UI changes).

All three use cases pass locally on arm64 (UC-1/2/3 PASSED).

ADR-009 status updated to Accepted and test plan updated with actual
setup details and results.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Yuri Shkuro <github@ysh.us>
@yurishkuro yurishkuro requested a review from a team as a code owner May 12, 2026 21:48
Copilot AI review requested due to automatic review settings May 12, 2026 21:48
@yurishkuro yurishkuro added the changelog:new-feature Change that should be called out as new feature in CHANGELOG label May 12, 2026
…y tests

Replace per-UC ad-hoc JS-only asset check with check_static_assets helper
that greps all static/ references from the live index.html and verifies
each one returns 200 via the external prefix. This covers favicon, CSS
bundle, and JS bundle in every UC, and auto-adapts to future asset names.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Yuri Shkuro <github@ysh.us>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces the ADR-009 approach for serving Jaeger UI behind reverse proxies by removing backend <base href> rewriting and adding CI-backed reverse-proxy E2E coverage. It also improves local UI build ergonomics by allowing the jaeger-ui submodule path to be overridden.

Changes:

  • Stop injecting <base href> in jaeger-query static handler and adjust related unit tests.
  • Add reverse-proxy E2E test harness (UC-1/2/3) plus a dedicated CI workflow and make target to run it.
  • Allow overriding the jaeger-ui checkout path via JAEGER_UI_DIR in build scripts.

Reviewed changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
scripts/makefiles/IntegrationTests.mk Adds a make target to run the new UI reverse-proxy E2E script.
scripts/makefiles/BuildBinaries.mk Introduces JAEGER_UI_DIR for building UI assets from a non-default location.
scripts/build/rebuild-ui.sh Threads JAEGER_UI_DIR through the UI rebuild script.
scripts/e2e/ui-reverse-proxy.sh New E2E script that validates UC-1/2/3 reverse-proxy scenarios.
scripts/e2e/reverse-proxy/uc1/docker-compose.yml UC-1 compose setup (prefix forwarded unchanged).
scripts/e2e/reverse-proxy/uc2/docker-compose.yml UC-2 compose setup (single Jaeger under multiple external prefixes).
scripts/e2e/reverse-proxy/uc2/httpd.conf UC-2 Apache config for dual-prefix forwarding/stripping.
scripts/e2e/reverse-proxy/uc3/docker-compose.yml UC-3 compose setup (external prefix rewritten to internal).
scripts/e2e/reverse-proxy/uc3/httpd.conf UC-3 Apache config for prefix rewrite.
docs/adr/README.md Adds ADR-009 to the ADR index.
docs/adr/009-ui-base-path-auto-detection.md New ADR documenting the auto-detection design and test plan.
cmd/jaeger/internal/extension/jaegerquery/internal/static_handler.go Removes backend base-path injection from index.html.
cmd/jaeger/internal/extension/jaegerquery/internal/static_handler_test.go Updates expectations and removes invalid base-path tests.
.github/workflows/ci-e2e-ui-reverse-proxy.yml New CI workflow to run the reverse-proxy E2E test.
.github/workflows/ci-e2e-all.yml Wires the new UI reverse-proxy workflow into the E2E fan-out.
Comments suppressed due to low confidence (1)

cmd/jaeger/internal/extension/jaegerquery/internal/static_handler_test.go:172

  • This test no longer covers invalid BasePath inputs ("x", "x/", "/x/") after removing the injection-time validation. Since BasePath still affects HTTP route registration, allowing invalid values to pass can lead to broken routing and/or double-slash patterns at runtime. Please add BasePath validation somewhere appropriate and restore/update these assertions to ensure invalid values are rejected early.
func TestNewStaticAssetsHandlerErrors(t *testing.T) {
	_, err := NewStaticAssetsHandler("fixture", StaticAssetsHandlerOptions{
		UIConfig: UIConfig{
			ConfigFile: "fixture/invalid-config",
		},
		Logger: zap.NewNop(),
	})
	require.Error(t, err)
}

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread .github/workflows/ci-e2e-ui-reverse-proxy.yml
Comment thread .github/workflows/ci-e2e-ui-reverse-proxy.yml Outdated
@codecov
Copy link
Copy Markdown

codecov Bot commented May 12, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 47.62%. Comparing base (9e0320f) to head (a414b84).

❌ Your project check has failed because the head coverage (47.62%) is below the target coverage (95.00%). You can increase the head coverage or adjust the target coverage.

Additional details and impacted files
@@             Coverage Diff             @@
##             main    #8568       +/-   ##
===========================================
- Coverage   96.52%   47.62%   -48.90%     
===========================================
  Files         330      174      -156     
  Lines       17354    10126     -7228     
===========================================
- Hits        16751     4823    -11928     
- Misses        454     4813     +4359     
- Partials      149      490      +341     
Flag Coverage Δ
badger_direct 8.95% <ø> (ø)
badger_e2e 1.04% <ø> (ø)
cassandra-4.x-direct-manual 14.56% <ø> (ø)
cassandra-4.x-e2e-auto 1.03% <ø> (ø)
cassandra-4.x-e2e-manual 1.03% <ø> (ø)
cassandra-5.x-direct-manual 14.56% <ø> (ø)
cassandra-5.x-e2e-auto 1.03% <ø> (ø)
cassandra-5.x-e2e-manual 1.03% <ø> (ø)
clickhouse-direct 8.98% <ø> (ø)
clickhouse-e2e 1.16% <ø> (ø)
elasticsearch-6.x-direct 16.89% <ø> (ø)
elasticsearch-7.x-direct 16.92% <ø> (ø)
elasticsearch-8.x-direct 17.07% <ø> (ø)
elasticsearch-8.x-e2e 1.04% <ø> (ø)
elasticsearch-9.x-e2e 1.04% <ø> (ø)
grpc_direct 7.89% <ø> (ø)
grpc_e2e 1.04% <ø> (ø)
kafka-3.x-v2 1.04% <ø> (ø)
memory_v2 1.04% <ø> (ø)
opensearch-1.x-direct 16.96% <ø> (ø)
opensearch-2.x-direct 16.96% <ø> (ø)
opensearch-2.x-e2e 1.04% <ø> (ø)
opensearch-3.x-e2e 1.04% <ø> (ø)
query 1.04% <ø> (ø)
tailsampling-processor 0.53% <ø> (ø)
unittests 30.10% <ø> (-64.71%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

yurishkuro and others added 3 commits May 12, 2026 17:58
…pose file

Replace three separate per-UC docker-compose files with one file that
brings up all six containers (three jaeger + three httpd) in parallel on
isolated networks. Each service uses a network alias so all httpd configs
can address the backend as "jaeger" without naming conflicts.

The shell script now does a single compose up/down and runs UC checks
sequentially against the already-running stack.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Yuri Shkuro <github@ysh.us>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Yuri Shkuro <github@ysh.us>
…roxy workflow

Also drop the --platform flag from docker build since the binary arch
already matches the runner.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Yuri Shkuro <github@ysh.us>
Copilot AI review requested due to automatic review settings May 12, 2026 22:02
@yurishkuro yurishkuro changed the title Auto UI prefix feat(query): Auto-detect UI base path from browser URL (ADR-009) May 12, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 3 comments.

Comment thread .github/workflows/ci-e2e-ui-reverse-proxy.yml
::group:: must appear at the start of the line; wrapping it in log()
prepended a timestamp and broke the fold.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Yuri Shkuro <github@ysh.us>
Comment thread scripts/e2e/reverse-proxy/docker-compose.yml Outdated
yurishkuro and others added 2 commits May 12, 2026 19:31
… fix CI

Address review comments on PR jaegertracing#8568:

- RegisterRoutes: add validation that BasePath starts with '/' and
  normalize trailing slash, matching the pattern used in jaegerai.
  Panics on invalid input (same as http.ServeMux on bad patterns).
- static_handler_test.go: change expectedBaseHTML for basePath="" and "/"
  cases from the old backend-injected '<base href="/"' to
  'data-inject-target="BASE_URL"' (backend no longer writes <base href>).
  Add TestRegisterRoutesInvalidBasePath covering paths without leading '/'.
- ci-e2e-ui-reverse-proxy.yml: fix comment ("single job", not "matrix
  job"); pin alpine:3.21 instead of alpine:latest; add JAEGER_UI_PR=3874
  to build from the unmerged UI PR until submodule is updated.
- rebuild-ui.sh: support JAEGER_UI_PR env var to gh pr checkout before
  building (temporary, to be removed after UI PR merges).

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Yuri Shkuro <github@ysh.us>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Yuri Shkuro <github@ysh.us>
Copilot AI review requested due to automatic review settings May 12, 2026 23:41
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 5 comments.

Comments suppressed due to low confidence (1)

cmd/jaeger/internal/extension/jaegerquery/internal/static_handler.go:209

  • RegisterRoutes panics on invalid BasePath (must start with '/'), which shifts misconfiguration detection from handler creation to route registration and makes the failure mode less consistent (NewStaticAssetsHandler returns errors, but BasePath errors now panic later). Consider moving BasePath validation/normalization into QueryOptions validation or NewStaticAssetsHandler so startup fails deterministically with an error message before partially initializing other components.
	if basePath == "" || basePath == "/" {
		basePath = "/"
	} else {
		if !strings.HasPrefix(basePath, "/") {
			panic(fmt.Sprintf("invalid base path %q: must start with '/'", basePath))
		}
		basePath = strings.TrimSuffix(basePath, "/")

Comment thread cmd/jaeger/internal/extension/jaegerquery/internal/static_handler.go Outdated
Comment thread scripts/build/rebuild-ui.sh Outdated
Comment thread scripts/e2e/ui-reverse-proxy.sh Outdated
Comment thread .github/workflows/ci-e2e-ui-reverse-proxy.yml Outdated
nvm is a shell function unavailable in GitHub Actions runners.
CI already has the correct Node.js version set by setup-node.js action.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Yuri Shkuro <github@ysh.us>
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 13, 2026

CI Summary Report

Metrics Comparison

✅ No significant metric changes

Code Coverage

❌ Coverage 51.4% (baseline 97.2%)

➡️ View CI run | View publish logs
2026-05-13 05:12:13 UTC

…e-proxy/

Per yurishkuro's review: move all httpd configs and the docker-compose
file into examples/reverse-proxy/ alongside the existing UC-1 example.
Also pin alpine:latest to alpine:3.21 for reproducible local builds.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Yuri Shkuro <github@ysh.us>
The Context section described prior behavior; now that the ADR is
implemented it consistently uses past tense.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Yuri Shkuro <github@ysh.us>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 16 out of 16 changed files in this pull request and generated 3 comments.

Comment thread cmd/jaeger/internal/extension/jaegerquery/internal/static_handler.go Outdated
Comment thread cmd/jaeger/internal/extension/jaegerquery/internal/static_handler_test.go Outdated
Comment thread cmd/jaeger/internal/extension/jaegerquery/internal/static_handler_test.go Outdated
yurishkuro and others added 2 commits May 12, 2026 22:50
Signed-off-by: Yuri Shkuro <github@ysh.us>
Documents X-Forwarded-Prefix header as a potential improvement for
unambiguous prefix detection in edge cases.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Yuri Shkuro <github@ysh.us>
Copilot AI review requested due to automatic review settings May 13, 2026 02:54
…xity tradeoffs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Yuri Shkuro <github@ysh.us>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 16 out of 16 changed files in this pull request and generated 3 comments.

Comment thread cmd/jaeger/internal/extension/jaegerquery/internal/static_handler_test.go Outdated
Comment thread examples/reverse-proxy/httpd-uc2.conf
Comment thread scripts/e2e/ui-reverse-proxy.sh
yurishkuro and others added 2 commits May 12, 2026 23:03
Without it GET /alt matched the root pass-through rule and forwarded as
/alt to Jaeger (404) instead of redirecting the browser to /alt/ for
correct prefix detection. Consistent with UC-1 and UC-3.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Yuri Shkuro <github@ysh.us>
Previously BasePath was validated in NewStaticAssetsHandler and trimmed
in RegisterRoutes, so other consumers (apiv3, jaegerai, server.go) saw
the raw value and could produce double-slash route patterns with inputs
like /jaeger//. Normalizing at Config.Validate() ensures all downstream
handlers see a clean value. Remove the now-redundant per-handler logic.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Yuri Shkuro <github@ysh.us>
Copilot AI review requested due to automatic review settings May 13, 2026 03:06
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 18 out of 18 changed files in this pull request and generated 3 comments.

Comment thread .github/workflows/ci-e2e-ui-reverse-proxy.yml
yurishkuro and others added 2 commits May 12, 2026 23:15
… (ADR-009)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Yuri Shkuro <github@ysh.us>
… 301

The root ProxyPass "/" was intercepting GET /alt before the Redirect
directive could redirect to /alt/. Adding ProxyPass "/alt" "!" excludes
that exact path from proxying so the Redirect fires correctly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Yuri Shkuro <github@ysh.us>
Copilot AI review requested due to automatic review settings May 13, 2026 03:41
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 18 out of 18 changed files in this pull request and generated 1 comment.

Comment thread cmd/jaeger/internal/extension/jaegerquery/config.go
yurishkuro and others added 2 commits May 13, 2026 00:04
… config

ProxyPass "/" intercepts all requests before mod_alias Redirect can fire.
Using RewriteRule with an exact-match pattern (^/alt$) works because
mod_rewrite runs in the URI translation phase alongside the proxy module,
and the [R=301,L] flags issue the redirect before any proxy rules apply.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Yuri Shkuro <github@ysh.us>
strings.TrimRight only removed trailing slashes, leaving paths like
//jaeger or /jaeger//foo which would break net/http mux routing since
the mux cleans request paths by collapsing // but patterns starting
with // or containing // may never match.

Use path.Clean to detect these cases and reject them explicitly rather
than silently normalizing to a different path than the user intended.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Yuri Shkuro <github@ysh.us>
Copilot AI review requested due to automatic review settings May 13, 2026 04:09
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 18 out of 18 changed files in this pull request and generated 4 comments.

Comment thread scripts/e2e/ui-reverse-proxy.sh
Comment thread cmd/jaeger/internal/extension/jaegerquery/config.go Outdated
Comment thread examples/reverse-proxy/httpd-uc2.conf
Comment thread cmd/jaeger/internal/extension/jaegerquery/internal/static_handler_test.go Outdated
- Include dot segments in the base_path error message since path.Clean
  also normalizes /./  segments, not just traversal and duplicate slashes.
- Reword the data-inject-target assertion message to accurately describe
  what is being checked (the <base> tag marker, not the inline script).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Yuri Shkuro <github@ysh.us>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 18 out of 18 changed files in this pull request and generated no new comments.

@yurishkuro yurishkuro merged commit 486ea50 into jaegertracing:main May 13, 2026
139 of 144 checks passed
@yurishkuro yurishkuro deleted the auto-ui-prefix branch May 13, 2026 05:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

changelog:new-feature Change that should be called out as new feature in CHANGELOG enhancement ui

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: Support external URL prefix

3 participants