rulefiles

package module
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Aug 24, 2026 License: MIT Imports: 0 Imported by: 0

README

rulefiles

Go parsers for the _headers and _redirects files of a static site build, the formats popularised by Netlify and Cloudflare Pages.

Package File Purpose Documentation
header _headers Response header fields for a path pkg.go.dev/thde.io/rulefiles/header
redirect _redirects URL redirects, rewrites, and errors pkg.go.dev/thde.io/rulefiles/redirect
go get thde.io/rulefiles@latest

Quick Start

NewHandler reads a rules file and wraps an http.Handler. Handlers compose with header innermost (around the static file server) and redirect outermost, so that header rules apply to the rewritten destination of a redirect:

site, err := header.NewHandler(headersFile, http.FileServerFS(build))
if err != nil {
	return err
}

site, err = redirect.NewHandler(redirectsFile, site)
if err != nil {
	return err
}

For callers that need to inspect or act on rules directly without middleware, both packages also export Parse and Resolve functions. See the package documentation for details.

File Formats

_redirects

_redirects holds one <source> <target> [<status>] rule per line, as documented by Netlify and Cloudflare Pages. The status defaults to 301. A status of 200 rewrites the request internally instead of redirecting. Query parameters of the request are preserved unless the target defines them.

/old/path        /new/path
/docs/:id/*      /articles/:id/:splat  302
/gone            /                     410
_headers

_headers holds an unindented path pattern followed by indented <name>: <value> or ! <name> lines, as documented by Netlify and Cloudflare Pages. All matching rules apply in the order declared; a field set by more than one rule is joined with a comma. Field names are case insensitive.

/*
  X-Robots-Tag: noindex
/static/*
  Cache-Control: public, max-age=31536000, immutable
  ! X-Robots-Tag
Path Patterns & Placeholders

Both formats share pattern syntax:

  • Patterns match against the decoded request path and are case sensitive.
  • Named placeholders (:name) capture individual path segments.
  • A trailing * captures the remainder of the path as :splat.
  • Placeholders expand into the target path, query, and fragment separately with proper escaping. Placeholders cannot introduce directory traversals (..).
  • Comments start with # at the start of a line or after whitespace.
Differences from Netlify and Cloudflare Pages
  • Matching on a scheme, host, query string, country, language, role, or cookie is not supported. Proxying to another host is supported via redirect.WithProxying and redirect.WithProxy.
  • A trailing ! on a redirect status (e.g. 301!) forces a rule to apply even if a file of the same name exists, reported as Rule.Force (enabled with redirect.WithExists).
  • A rule that omits the status redirects with 301 (Netlify default). Cloudflare defaults to 302; use redirect.WithDefaultStatus to configure.
  • By default a trailing slash is ignored on both sides (Netlify default). Use WithExactTrailingSlash() to match trailing slashes exactly (Cloudflare default).
  • * is only supported as the last segment of a pattern.
  • Special characters in source paths may be URL-encoded or written as literals.
  • Header fields that frame the response (Content-Length, Transfer-Encoding, Connection, Upgrade, etc.) are rejected with ErrUnsupported.
  • A field declared more than once is joined with ", " (Cloudflare behavior). Set-Cookie values are retained as separate header lines.
  • Lines longer than 1 MiB are rejected to bound allocation.

Documentation

Full API documentation, options, and examples are available via go doc or pkg.go.dev:

Tests

go test ./...

Both parsers include fuzz tests verifying round-trip encoding and safe resolution:

go test ./redirect -run '^$' -fuzz FuzzResolve
go test ./header -run '^$' -fuzz FuzzResolve

Documentation

Overview

Package rulefiles is the root of a module that parses the _headers and _redirects files of a static site build, the formats popularised by Netlify and Cloudflare Pages.

The root package holds no code of its own; the two formats are implemented by dedicated packages:

Each package can resolve rules individually for callers that act on the result directly, or wrap a net/http.Handler for middleware use.

Middleware Pipeline

When serving a static site with both header and redirect rules, the handlers compose with header rules wrapped innermost, directly around the file server, and redirect rules outermost:

site, err := header.NewHandler(headersFile, http.FileServerFS(build))
if err != nil {
	return err
}
site, err = redirect.NewHandler(redirectsFile, site)
if err != nil {
	return err
}

This ensures that when a redirect rule rewrites a request (HTTP status 200), the header rules apply to the rewritten destination path rather than the originally requested URL.

Pattern Syntax

Both rule formats share the same path pattern syntax:

  • Patterns match against the decoded request path and are case-sensitive.
  • Named placeholders (":name") match a single path segment and capture its value.
  • A trailing splat ("*") matches the remainder of the path and is captured as ":splat".
  • Comments begin with "#" at the start of a line or following whitespace.

Error Handling

Parser functions report all errors across the file together using errors.Join. Every error wraps one of two sentinel errors:

Example

Example combines redirect and header rules.

package main

import (
	"context"
	"fmt"
	"log/slog"
	"net/http"
	"net/http/httptest"
	"strings"

	"thde.io/rulefiles/header"
	"thde.io/rulefiles/redirect"
)

func main() {
	const (
		redirectsFile = "/old/*  /new/:splat\n/app/*  /index.html  200\n/gone   /            410\n"
		headersFile   = "/*\n  X-Robots-Tag: noindex\n/new/*\n  Cache-Control: no-store\n"
	)

	// files simulates a static file server.
	files := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		//nolint:gosec // Echo path for example output.
		_, _ = fmt.Fprint(w, "the file at "+r.URL.Path)
	})

	// The header rules apply to the path a request is rewritten to, so they wrap
	// the file server rather than the site.
	served, err := header.NewHandler(strings.NewReader(headersFile), files)
	if err != nil {
		slog.Error("reading the header rules", "error", err)

		return
	}
	site, err := redirect.NewHandler(strings.NewReader(redirectsFile), served)
	if err != nil {
		slog.Error("reading the redirect rules", "error", err)

		return
	}

	for _, request := range []string{"/index.html", "/old/page.html", "/new/page.html", "/app/deep/link", "/gone"} {
		rec := httptest.NewRecorder()
		site.ServeHTTP(rec, httptest.NewRequestWithContext(context.Background(), http.MethodGet, request, http.NoBody))

		fmt.Printf("%s: %d location=%q cache-control=%q robots=%q\n",
			request, rec.Code, rec.Header().Get("Location"),
			rec.Header().Get("Cache-Control"), rec.Header().Get("X-Robots-Tag"))
	}

}
Output:
/index.html: 200 location="" cache-control="" robots="noindex"
/old/page.html: 301 location="/new/page.html" cache-control="" robots=""
/new/page.html: 200 location="" cache-control="no-store" robots="noindex"
/app/deep/link: 200 location="" cache-control="" robots="noindex"
/gone: 410 location="" cache-control="" robots=""

Directories

Path Synopsis
Package header parses and applies _headers files.
Package header parses and applies _headers files.
internal
rulefile
Package rulefile implements the syntax shared by the _redirects and _headers files, the formats popularised by Netlify and Cloudflare Pages: comments, path patterns and the ":name" placeholders a pattern captures.
Package rulefile implements the syntax shared by the _redirects and _headers files, the formats popularised by Netlify and Cloudflare Pages: comments, path patterns and the ":name" placeholders a pattern captures.
Package redirect parses and resolves _redirects files.
Package redirect parses and resolves _redirects files.

Jump to

Keyboard shortcuts

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