Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
5 changes: 3 additions & 2 deletions docs/health_checks.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 5 additions & 5 deletions handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -266,7 +266,7 @@ func NewHandler(opts *HandlerOpts) (*Handler, error) {

middlewareStack := apimiddleware.NewMiddlewareStack()

if prefix != "/" {
if prefix != "" {
middlewareStack.Use(&stripPrefixMiddleware{prefix})
}

Expand Down
24 changes: 24 additions & 0 deletions handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
})
}
}
11 changes: 7 additions & 4 deletions internal/riveruicmd/riveruicmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -202,7 +205,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{
Expand Down
9 changes: 9 additions & 0 deletions internal/riveruicmd/riveruicmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading