From c3127cfada39101fb11f5c13df7559a456d28fc7 Mon Sep 17 00:00:00 2001 From: Jan Kott <51777660+boostvolt@users.noreply.github.com> Date: Tue, 13 Jan 2026 19:57:07 +0100 Subject: [PATCH 1/3] fix(handler): prevent double slash in URLs for root path prefix --- handler.go | 10 +++++----- internal/riveruicmd/riveruicmd.go | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/handler.go b/handler.go index 12bf994..835a38f 100644 --- a/handler.go +++ b/handler.go @@ -145,12 +145,12 @@ func (opts *HandlerOpts) validate() error { } func NormalizePathPrefix(prefix string) string { - if prefix == "" { - return "/" + if prefix == "" || prefix == "/" { + return "" } prefix = strings.TrimSuffix(prefix, "/") if !strings.HasPrefix(prefix, "/") { - return "/" + prefix + prefix = "/" + prefix } return prefix } @@ -189,7 +189,7 @@ func NewHandler(opts *HandlerOpts) (*Handler, error) { JobListHideArgsByDefault: opts.JobListHideArgsByDefault, }) - prefix := cmp.Or(strings.TrimSuffix(opts.Prefix, "/"), "") + prefix := opts.Prefix frontendIndex, err := fs.Sub(FrontendIndex, "dist") if err != nil { @@ -266,7 +266,7 @@ func NewHandler(opts *HandlerOpts) (*Handler, error) { middlewareStack := apimiddleware.NewMiddlewareStack() - if prefix != "/" { + if prefix != "" { middlewareStack.Use(&stripPrefixMiddleware{prefix}) } diff --git a/internal/riveruicmd/riveruicmd.go b/internal/riveruicmd/riveruicmd.go index 6be5bc5..1f4da79 100644 --- a/internal/riveruicmd/riveruicmd.go +++ b/internal/riveruicmd/riveruicmd.go @@ -202,7 +202,7 @@ func initServer[TClient any](ctx context.Context, opts *initServerOpts, createCl }) filters := []sloghttp.Filter{} if opts.silentHealthChecks { - apiHealthPrefix := strings.TrimSuffix(opts.pathPrefix, "/") + "/api/health-checks" + apiHealthPrefix := opts.pathPrefix + "/api/health-checks" filters = append(filters, sloghttp.IgnorePathPrefix(apiHealthPrefix)) } logHandler := sloghttp.NewWithConfig(opts.logger, sloghttp.Config{ From 21c97ee0dbd8aed534f44a279ee321057ae543dd Mon Sep 17 00:00:00 2001 From: Blake Gentry Date: Mon, 19 Jan 2026 12:09:52 -0600 Subject: [PATCH 2/3] improve test coverage for path prefix --- handler_test.go | 24 ++++++++++++++++++++++++ internal/riveruicmd/riveruicmd_test.go | 9 +++++++++ package.json | 2 +- 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/handler_test.go b/handler_test.go index b80c1d2..1ebdd15 100644 --- a/handler_test.go +++ b/handler_test.go @@ -139,3 +139,27 @@ func TestMountStaticFiles(t *testing.T) { require.Equal(t, "text/plain; charset=utf-8", recorder.Header().Get("Content-Type")) require.Contains(t, recorder.Body.String(), "User-Agent") } + +func TestNormalizePathPrefix(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + input string + want string + }{ + {name: "Empty", input: "", want: ""}, + {name: "Root", input: "/", want: ""}, + {name: "NoLeadingSlash", input: "prefix", want: "/prefix"}, + {name: "LeadingSlash", input: "/prefix", want: "/prefix"}, + {name: "TrailingSlash", input: "/prefix/", want: "/prefix"}, + {name: "NoLeadingWithTrailing", input: "prefix/", want: "/prefix"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + require.Equal(t, tt.want, NormalizePathPrefix(tt.input)) + }) + } +} diff --git a/internal/riveruicmd/riveruicmd_test.go b/internal/riveruicmd/riveruicmd_test.go index c6b8f70..40e881a 100644 --- a/internal/riveruicmd/riveruicmd_test.go +++ b/internal/riveruicmd/riveruicmd_test.go @@ -216,6 +216,15 @@ func TestSilentHealthchecks_SuppressesLogs(t *testing.T) { require.Equal(t, http.StatusOK, recorder.Code) require.Empty(t, memoryHandler.records) + // reset and test with trailing slash prefix + memoryHandler.records = nil + initRes = makeServer(t, "/pfx/", true) + + recorder = httptest.NewRecorder() + initRes.httpServer.Handler.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/pfx/api/health-checks/minimal", nil)) + require.Equal(t, http.StatusOK, recorder.Code) + require.Empty(t, memoryHandler.records) + // now silent=false should log health memoryHandler.records = nil initRes = makeServer(t, "/", false) diff --git a/package.json b/package.json index fe43a4d..ea38306 100644 --- a/package.json +++ b/package.json @@ -60,8 +60,8 @@ "prettier": "^3.7.4", "prettier-plugin-tailwindcss": "^0.7.2", "storybook": "^10.1.10", - "tailwindcss": "^4.1.8", "tailwind-csstree": "^0.1.0", + "tailwindcss": "^4.1.8", "typescript": "^5.9.3", "typescript-eslint": "^8.50.0", "vite": "^7.3.0", From b11663b7043794da1acff3194c30638a638dcb43 Mon Sep 17 00:00:00 2001 From: Blake Gentry Date: Mon, 19 Jan 2026 12:21:35 -0600 Subject: [PATCH 3/3] doc improvements --- docs/README.md | 6 +++++- docs/health_checks.md | 5 +++-- internal/riveruicmd/riveruicmd.go | 9 ++++++--- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/docs/README.md b/docs/README.md index 67c8114..1e83904 100644 --- a/docs/README.md +++ b/docs/README.md @@ -49,7 +49,11 @@ See [health checks](health_checks.md). ### Custom path prefix -The `riverui` command accepts a `-prefix` arg to set a path prefix on both the API and static assets. When executing the Docker image, this is accepted as a `PATH_PREFIX` env. +Serve River UI under a URL prefix like `/ui` by setting `-prefix` (binary) or `PATH_PREFIX` (Docker). Rules: **must start with `/`**, use `/` for no prefix, and a trailing `/` is ignored. + +Example: `./riverui -prefix=/ui` serves the UI at `/ui/` and the API at `/ui/api/...` (and `/ui` will redirect to `/ui/`). + +Reverse proxies: either preserve the prefix and set `-prefix=/ui`, or strip the prefix and leave `-prefix=/` (don’t do both). ### Hiding job list arguments by default diff --git a/docs/health_checks.md b/docs/health_checks.md index 1c6044d..b565dfe 100644 --- a/docs/health_checks.md +++ b/docs/health_checks.md @@ -6,12 +6,13 @@ River UI exposes two types of health checks: For production deployments, it is recommended to use the `complete` health check. ## How to use + ### HTTP Endpoint Useful when running on Kubernetes or behind load balancer that can hit the HTTP endpoint. -The URL would be `{prefix}/api/health-checks/{name}` +The URL would be `{prefix}/api/health-checks/{name}`. -- `{prefix}` is the path prefix set in the environment variable `PATH_PREFIX` or `-prefix` flag +- `{prefix}` is the path prefix set in the environment variable `PATH_PREFIX` or `-prefix` flag (must start with `/`; use `/` for none; trailing slash is ignored) - `{name}` is the health check name. Can be `minimal` or `complete`. **Example:** When setting `PATH_PREFIX=/my-prefix` and wanting to include the database connection in the health check the path would be diff --git a/internal/riveruicmd/riveruicmd.go b/internal/riveruicmd/riveruicmd.go index 1f4da79..1fb4722 100644 --- a/internal/riveruicmd/riveruicmd.go +++ b/internal/riveruicmd/riveruicmd.go @@ -36,7 +36,7 @@ func Run[TClient any](createClient func(*pgxpool.Pool) (TClient, error), createB })) var pathPrefix string - flag.StringVar(&pathPrefix, "prefix", "/", "path prefix to use for the API and UI HTTP requests") + flag.StringVar(&pathPrefix, "prefix", "/", "path prefix for API and UI routes (must start with '/', use '/' for no prefix)") var healthCheckName string flag.StringVar(&healthCheckName, "healthcheck", "", "the name of the health checks: minimal or complete") @@ -146,8 +146,11 @@ func initServer[TClient any](ctx context.Context, opts *initServerOpts, createCl if opts == nil { return nil, errors.New("opts is required") } - if !strings.HasPrefix(opts.pathPrefix, "/") || opts.pathPrefix == "" { - return nil, fmt.Errorf("invalid path prefix: %s", opts.pathPrefix) + if opts.pathPrefix == "" { + return nil, errors.New("invalid path prefix: cannot be empty (use \"/\" for no prefix)") + } + if !strings.HasPrefix(opts.pathPrefix, "/") { + return nil, fmt.Errorf("invalid path prefix %q: must start with '/' (use \"/\" for no prefix)", opts.pathPrefix) } opts.pathPrefix = riverui.NormalizePathPrefix(opts.pathPrefix)