prefix

package
v0.0.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 14, 2026 License: Apache-2.0 Imports: 5 Imported by: 0

README

prefix

prefix lets the same application run at / locally and below a reverse-proxy path in production.

prefixValue := prefix.NormalizeRoutePrefix("/wiki/") // "/wiki"
handler := prefix.MountUnderPrefixWithOptions(app, prefixValue,
    prefix.WithRedirectRewriting(),
)

pageURL := prefix.RouteURL(prefixValue, "/pages/start") // "/wiki/pages/start"

Application routes should stay prefix-free. Use RouteURL for generated links and URLForRequest or Redirect for request-relative links and redirects.

Documentation

Overview

Package prefix provides small helpers for mounting net/http handlers under a configurable URL prefix.

Typical use-case: your service can run at root in local/dev, but under a sub-path in production (for example behind a reverse proxy).

The package exposes:

  • ValidateRoutePrefix: optionally validates strict path-only configuration
  • RouteURL: generates outbound references from prefix-free application paths
  • NormalizeRoutePrefix: converts user-configurable values into a canonical prefix ("" or "/prefix")
  • MountUnderPrefix: mounts handlers under that prefix and applies consistent redirect behavior for the bare prefix
  • MountUnderPrefixWithOptions: same mount behavior with redirect status code overrides via Option values

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func MountUnderPrefix

func MountUnderPrefix(h http.Handler, prefix string) http.Handler

MountUnderPrefix mounts h under route prefix and returns a handler that serves:

  • prefix + "/" subtree via http.StripPrefix(prefix, h)
  • bare prefix redirect to prefix + "/"

If prefix normalizes to "", MountUnderPrefix returns h unchanged. This includes empty, whitespace-only, root-like values, and full URLs without a path.

Redirect status is:

  • 308 for GET and HEAD
  • 307 for all other methods

Prefix normalization inside this function matches NormalizeRoutePrefix:

  • full URLs use only their path
  • leading slash is added when missing
  • trailing slashes are removed

This function uses default redirect status codes (GET/HEAD: 308, others: 307). Mounted requests carry context for URLForRequest and Redirect. Handler Location headers are unchanged unless WithRedirectRewriting is enabled via MountUnderPrefixWithOptions, which also supports overriding redirect codes.

Example
package main

import (
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"

	httpprefix "github.com/containeroo/httpkit/prefix"
)

func main() {
	inner := http.NewServeMux()
	inner.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) {
		_, _ = io.WriteString(w, "root")
	})
	inner.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
		_, _ = io.WriteString(w, "ok")
	})

	h := httpprefix.MountUnderPrefix(inner, "/app")

	rec1 := httptest.NewRecorder()
	h.ServeHTTP(rec1, httptest.NewRequest(http.MethodGet, "/app", nil))
	fmt.Println(rec1.Code, rec1.Header().Get("Location"))

	rec2 := httptest.NewRecorder()
	h.ServeHTTP(rec2, httptest.NewRequest(http.MethodGet, "/app/health", nil))
	fmt.Println(rec2.Code, rec2.Body.String())

}
Output:
308 /app/
200 ok

func MountUnderPrefixWithOptions

func MountUnderPrefixWithOptions(h http.Handler, prefix string, opts ...Option) http.Handler

MountUnderPrefixWithOptions behaves like MountUnderPrefix and accepts optional redirect status code overrides and opt-in handler redirect rewriting.

If prefix normalizes to "", h is returned unchanged.

Allowed redirect codes are 301, 302, 303, 307, and 308. Invalid codes are replaced with defaults (GET/HEAD: 308, others: 307).

Example
package main

import (
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"

	httpprefix "github.com/containeroo/httpkit/prefix"
)

func main() {
	inner := http.NewServeMux()
	inner.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) {
		_, _ = io.WriteString(w, "root")
	})

	h := httpprefix.MountUnderPrefixWithOptions(
		inner,
		"/app",
		httpprefix.WithGetHeadRedirectCode(http.StatusMovedPermanently), // 301
		httpprefix.WithOtherRedirectCode(http.StatusFound),              // 302
	)

	rec1 := httptest.NewRecorder()
	h.ServeHTTP(rec1, httptest.NewRequest(http.MethodGet, "/app", nil))
	fmt.Println(rec1.Code, rec1.Header().Get("Location"))

	rec2 := httptest.NewRecorder()
	h.ServeHTTP(rec2, httptest.NewRequest(http.MethodPost, "/app", nil))
	fmt.Println(rec2.Code, rec2.Header().Get("Location"))

}
Output:
301 /app/
302 /app/

func NormalizeRoutePrefix

func NormalizeRoutePrefix(input string) string

NormalizeRoutePrefix converts user input into a canonical route prefix.

It accepts either a raw path (for example, "api", "/api", "/api///") or a full URL (for example, "https://example.com/api/").

Rules:

  • Empty, whitespace-only, root-like values ("/", "///") return "".
  • Trailing slashes are removed.
  • A leading slash is added when missing.
  • For full URLs, only the URL path is used.

The returned value is always either "" or a string beginning with "/".

Example
package main

import (
	"fmt"

	httpprefix "github.com/containeroo/httpkit/prefix"
)

func main() {
	fmt.Println(httpprefix.NormalizeRoutePrefix(""))
	fmt.Println(httpprefix.NormalizeRoutePrefix("/"))
	fmt.Println(httpprefix.NormalizeRoutePrefix("app"))
	fmt.Println(httpprefix.NormalizeRoutePrefix("https://example.com/app/"))
}
Output:

/app
/app

func PrefixForRequest added in v0.0.2

func PrefixForRequest(r *http.Request) string

PrefixForRequest returns the normalized prefix under which r is mounted.

func Redirect

func Redirect(w http.ResponseWriter, r *http.Request, target string, status int)

Redirect redirects to a prefix-free application path using the request's mount. It avoids a second prefix when redirect rewriting is enabled. Do not pass an already-generated URL; external URLs follow RouteURL's rules.

func RouteURL

func RouteURL(prefix, target string) string

RouteURL generates an external URL reference for a prefix-free application path. The prefix is normalized with NormalizeRoutePrefix; callers needing strict configuration should first use ValidateRoutePrefix.

Local absolute paths gain the prefix, preserving their escaping, query, and fragment. For example, RouteURL("/app", "/pages/foo?q=bar#top") returns "/app/pages/foo?q=bar#top". RouteURL("/app", "/") returns "/app/".

Pass application-local paths, not URLs already generated by this function. No attempt is made to recognize an already-prefixed path: RouteURL("/pages", "/pages/foo") returns "/pages/pages/foo".

Malformed local paths, network-path references ("//host"), backslashes, line breaks, and dot segments fall back to the deployment root. Escaped path characters are checked after decoding as well. External URLs and relative references are returned unchanged. This function is not a general URL sanitizer or an authorization check for redirect destinations.

Example
package main

import (
	"fmt"

	httpprefix "github.com/containeroo/httpkit/prefix"
)

func main() {
	fmt.Println(httpprefix.RouteURL("/app", "/"))
	fmt.Println(httpprefix.RouteURL("/app", "/pages/foo?q=bar#top"))
	fmt.Println(httpprefix.RouteURL("", "/pages/foo"))
	fmt.Println(httpprefix.RouteURL("/pages", "/pages/foo"))
}
Output:
/app/
/app/pages/foo?q=bar#top
/pages/foo
/pages/pages/foo

func URLForRequest

func URLForRequest(r *http.Request, target string) string

URLForRequest generates an external URL for a prefix-free application path. It uses the mount installed by MountUnderPrefix or MountUnderPrefixWithOptions. Unmounted requests use an empty prefix. Target handling follows RouteURL.

func ValidateRoutePrefix

func ValidateRoutePrefix(prefix string) error

ValidateRoutePrefix validates strict path-only deployment configuration. It accepts an empty string or "/" for root deployment, and absolute paths containing non-empty segments of ASCII letters, digits, '-', '.', '_', or '~'. One trailing slash is allowed. Dot segments ("." and "..") are rejected. URLs, escapes, whitespace, queries, fragments, and repeated slashes are rejected.

Validate before calling NormalizeRoutePrefix when malformed configuration should be rejected rather than normalized permissively. Validation does not change the input, and the existing normalizer and mounting helpers remain permissive for backward compatibility.

Example
package main

import (
	"fmt"

	httpprefix "github.com/containeroo/httpkit/prefix"
)

func main() {
	input := "/app/"
	if err := httpprefix.ValidateRoutePrefix(input); err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println(httpprefix.NormalizeRoutePrefix(input))
	fmt.Println(httpprefix.ValidateRoutePrefix("/app/../other") != nil)
}
Output:
/app
true

Types

type Option

type Option func(*Options)

Option mutates Options used by MountUnderPrefixWithOptions.

func WithGetHeadRedirectCode

func WithGetHeadRedirectCode(code int) Option

WithGetHeadRedirectCode sets the redirect status code for GET and HEAD requests.

func WithOptions

func WithOptions(opts Options) Option

WithOptions overwrites all options used by MountUnderPrefixWithOptions.

func WithOtherRedirectCode

func WithOtherRedirectCode(code int) Option

WithOtherRedirectCode sets the redirect status code for non-GET/HEAD requests.

func WithRedirectRewriting

func WithRedirectRewriting() Option

WithRedirectRewriting prefixes application-local redirect destinations. Raw Location paths must be prefix-free; use Redirect for explicit redirects. External URLs and relative references are preserved using RouteURL's rules.

type Options

type Options struct {
	// GetHeadRedirectCode is used for redirects on GET and HEAD requests.
	GetHeadRedirectCode int
	// OtherRedirectCode is used for redirects on non-GET/HEAD requests.
	OtherRedirectCode int
	// RewriteRedirects prefixes application-local Location headers on redirects.
	RewriteRedirects bool
}

Options configures mounting and redirects used by MountUnderPrefixWithOptions.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL