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
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ permissions:

jobs:
ci:
uses: faustbrian/go-library-tools/.github/workflows/library-ci.yml@1f9629e5f27418600460b55a50a5b2fc81697fab # v1.2.0
uses: faustbrian/go-library-tools/.github/workflows/library-ci.yml@6c76f5c670d193ce369a7242d4c634f1117286e9 # v1.3.0 cohesion enforcement
with:
tooling_sha: 1f9629e5f27418600460b55a50a5b2fc81697fab
tooling_sha: 6c76f5c670d193ce369a7242d4c634f1117286e9
release_dry_run: ${{ inputs.release_dry_run || false }}

required:
Expand Down
4 changes: 2 additions & 2 deletions .golib.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
schema_version: 1
tool_version: v1.2.0
tool_checksums_sha256: 7de6de3a14184eced8c0114bf802cb24a808f3e1592d358f6f6f3d02552d0e27
tool_version: v1.3.0
tool_checksums_sha256: 72d38d7d0cab33bfeb9345df6b141eacf9841c5d6817f597ff25b957dc8dc48f

manifest:
modules: modules.json
Expand Down
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ Versioning.
specification decision, conformance, history, and monitoring contract.
- Expose the owned middleware, JSON-RPC, and service integrations through the
typed interoperability gate required by the repository contract.
- Adopt the checksum-verified `go-library-tools` v1.3.0 CLI, schema-v2
cohesion metadata, and repository-local cohesion gate while retaining the
router's public API and package-owned verification evidence.
- Pin reusable CI to the immutable v1.3.0 workflow so hosted checks enforce
the same cohesion contract as local validation.
- Preserve the pinned Go 1.26.6 escaped-path redirect serialization across
later Go toolchains instead of silently adopting Go 1.27 wire changes.

### Documentation

Expand All @@ -25,11 +32,14 @@ Versioning.
decision history.

- ROUTER-DEC-001 sha256:56f4ea57efd1fb24b8650c883bea47f05872c63d6fb4f4fb43f1f2bdafe9ff8c
- ROUTER-DEC-001 sha256:f446182ad093b6e3dc1f40e180fe8ad03f88733233cf8a3f2722740c3b804585
- ROUTER-DEC-002 sha256:f8dbfe4fe493cc3b71c400f985206aef6654f266f705ff0a6f8fdbb9fd979811
- ROUTER-DEC-003 sha256:e57444cf3c6e408a366c339ed4f622897c655ccf85dfa6545628d07e04cd5964
- ROUTER-DEC-003 sha256:131ba5fada36d81eb47b1d595b624ee2cf0b95e6cfdfde47409b8492e156d2f3
- ROUTER-DEC-004 sha256:d06af40fb0a74ae46f2f22d843c09effd2c397777bf07d2a9f931fdec3e9efb2
- ROUTER-DEC-005 sha256:383d99f58bc5bf65b234941feb386c096b673e5276e45cba86aa31e5d80971e9
- ROUTER-DEC-005 sha256:35abe45420556f55ed3d65b43fb6f2d172710ada3949468b4a0526aac74db0d4
- ROUTER-DEC-005 sha256:adf00e74ff1fee7ab18248fef929a805e30d4dfd14ddaa69d67859d70af701f0
- ROUTER-DEC-006 sha256:9fa23204c8ff2ae12d5631c7947091b32436be3bf075a29f2700037a7bce27c1
- ROUTER-DEC-007 sha256:1eb13de30f37d6bc1e8e7924ace29a967364d88f22570ca7cc84f2c5659e9139
- ROUTER-DEC-008 sha256:bcf6f5c4c6b7b3960e028c33645cedbb22dcfab2d7460d2acee73077edee2c2b
Expand All @@ -42,6 +52,12 @@ Versioning.

- Replace archived monorepo links and completed execution artifacts with a
standalone, human-oriented documentation structure.
- Link the router to the immutable v1.3.0 Golib ecosystem index and
service-edge package-selection guidance.
- Bind monitored Go source files to immutable Go 1.26.6 URLs and record the
reviewed Go 1.27 redirect change without changing the support baseline.
- Record RFC 9110 Erratum 9162 as behavior-neutral because generated `Allow`
field values already use comma-space separation.

## [1.0.0] - 2026-08-25

Expand Down
11 changes: 9 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
GOLIB ?= golib

.PHONY: check ci inventory repository-check
.PHONY: check ci cohesion inventory repository-check

check:
$(GOLIB) check --all

ci:
$(GOLIB) repository check
$(GOLIB) cohesion check
$(GOLIB) check --all

inventory repository-check:
cohesion:
$(GOLIB) cohesion check

inventory:
$(GOLIB) inventory

repository-check:
$(GOLIB) repository check
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ The minimum supported toolchain is Go 1.26.6. The package has no runtime
dependencies and no global router, reflection discovery, controller resolver,
container, session, template, or application lifecycle.

See the versioned [Golib ecosystem index](https://github.com/faustbrian/go-library-tools/blob/v1.3.0/docs/ecosystem/README.md)
and its [service-edge package guidance](https://github.com/faustbrian/go-library-tools/blob/v1.3.0/docs/ecosystem/design-language.md#package-families-and-selection)
for the shared design language and related packages.

## Five-minute start

```go
Expand Down Expand Up @@ -71,6 +75,9 @@ Run `make check` for the blocking local checks. NilAway remains visible as an
advisory result in the shared contract. Each target is independently
reproducible.

Run `make cohesion` to validate this module's catalog classification,
ownership contract, documentation entry points, and ecosystem navigation.

## License

MIT. See [LICENSE](LICENSE) and [NOTICE](NOTICE).
Expand Down
72 changes: 72 additions & 0 deletions compatibility_security_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,78 @@ func TestCanonicalRedirectsPrecedeRouteAndMethodSelection(t *testing.T) {
}
}

func TestPinnedServeMuxRedirectsPreserveGo126EscapedPathBehavior(t *testing.T) {
t.Parallel()

canonical := mustCompile(t, router.New())
subtreeBuilder := router.New()
mustRegister(t, subtreeBuilder, router.Route{
Methods: []string{http.MethodGet}, Path: "/encoded%2f/", Handler: http.NotFoundHandler(),
})
subtree := mustCompile(t, subtreeBuilder)

for _, testCase := range []struct {
name string
router http.Handler
target string
location string
}{
{
name: "canonical path",
router: canonical,
target: "/files/a%2Fb//tail?source=test",
location: "/files/a%252Fb/tail?source=test",
},
{
name: "subtree root",
router: subtree,
target: "/encoded%2F",
location: "/encoded/",
},
} {
t.Run(testCase.name, func(t *testing.T) {
response := httptest.NewRecorder()
testCase.router.ServeHTTP(response, httptest.NewRequest(http.MethodGet, testCase.target, nil))
if response.Code != http.StatusTemporaryRedirect || response.Header().Get("Location") != testCase.location {
t.Fatalf("%s: status=%d location=%q", testCase.target, response.Code, response.Header().Get("Location"))
}
})
}
}

func TestPinnedSubtreeRedirectDoesNotOverrideExplicitRoot(t *testing.T) {
t.Parallel()

for _, testCase := range []struct {
name string
rootPattern string
treePattern string
requestTarget string
}{
{name: "literal", rootPattern: "/tree", treePattern: "/tree/", requestTarget: "/tree"},
{name: "wildcard", rootPattern: "/{value}", treePattern: "/{value}/", requestTarget: "/tree"},
} {
t.Run(testCase.name, func(t *testing.T) {
builder := router.New()
mustRegister(t, builder, router.Route{
Methods: []string{http.MethodGet}, Path: testCase.rootPattern,
Handler: http.HandlerFunc(func(writer http.ResponseWriter, _ *http.Request) {
writer.Header().Set("X-Route", "root")
writer.WriteHeader(http.StatusNoContent)
}),
})
mustRegister(t, builder, router.Route{
Methods: []string{http.MethodGet}, Path: testCase.treePattern, Handler: http.NotFoundHandler(),
})
response := httptest.NewRecorder()
mustCompile(t, builder).ServeHTTP(response, httptest.NewRequest(http.MethodGet, testCase.requestTarget, nil))
if response.Code != http.StatusNoContent || response.Header().Get("X-Route") != "root" || response.Header().Get("Location") != "" {
t.Fatalf("status=%d route=%q location=%q", response.Code, response.Header().Get("X-Route"), response.Header().Get("Location"))
}
})
}
}

func TestDocumentedServeMuxDispatchDivergences(t *testing.T) {
t.Parallel()

Expand Down
1 change: 0 additions & 1 deletion compiler.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ func (b *Builder) Compile() (*Router, error) {
methodNotAllowed: b.methodNotAllowed,
automaticOptions: b.automaticOptions,
redirectPolicy: b.redirectPolicy,
canonicalizer: http.NewServeMux(),
}
hostIndexes := make(map[string]int)
for _, route := range routes {
Expand Down
60 changes: 34 additions & 26 deletions docs/specification-decisions.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,14 @@ Superseded decisions remain linked from their replacements.
- **Selected behavior, security and resource consequences, compatibility and wire consequences:**
Delegate supported literal,
`{name}`, `{name...}`, and `{$}` path parsing, specificity, conflict,
extraction, GET-to-HEAD, and redirects to the pinned `ServeMux`. Package
extensions operate outside that matcher; no copied internal source, unsafe,
or registration-order tie-break exists.
extraction, and GET-to-HEAD behavior to `ServeMux`. Canonical and subtree
redirect locations preserve the pinned Go 1.26.6 serialization across later
toolchains. Package extensions operate outside the matcher; no copied matcher,
unsafe, or registration-order tie-break exists.
- **Evidence, public surface, upstream, and reconsideration:**
`TestSupportedMatchingIsDifferentialWithServeMux`,
`TestSupportedMethodsAndLiteralHostsAreDifferentialWithServeMux`, and
`TestPinnedServeMuxRedirectsPreserveGo126EscapedPathBehavior` plus
`FuzzRoutePatternCompilation` cover `Builder.Register`, `Compile`, and
`Router.ServeHTTP`. Reconsider when the minimum Go routing contract changes.

Expand Down Expand Up @@ -55,15 +57,16 @@ Superseded decisions remain linked from their replacements.
"Support literal paths only"
],
"peer_behavior": "Go 1.26.6 ServeMux is the maintained differential peer for supported precedence, escaped segments, redirects, and path-value extraction.",
"selected_behavior": "Delegate supported literal, named wildcard, remainder wildcard, and end-marker parsing, specificity, conflicts, path values, GET-to-HEAD matching, and redirects to Go 1.26.6 ServeMux.",
"rationale": "Delegation preserves the declared Go contract and avoids an unreviewed parallel matcher.",
"selected_behavior": "Delegate supported literal, named wildcard, remainder wildcard, and end-marker parsing, specificity, conflicts, path values, and GET-to-HEAD matching to ServeMux while preserving Go 1.26.6 canonical and subtree redirect serialization across later toolchains.",
"rationale": "Delegation preserves the declared matching contract without an unreviewed parallel matcher, while a narrow redirect boundary prevents later toolchains from silently changing the pinned wire behavior.",
"security_consequences": "No copied unsafe matcher or registration-order tie-break can reinterpret escaped path structure.",
"resource_consequences": "ServeMux owns matcher construction and dispatch within the router's finite registration and request limits.",
"compatibility_consequences": "Supported patterns track the pinned Go 1.26.6 contract; package extensions remain explicitly separate.",
"wire_consequences": "Dispatch, redirects, and extracted path values match ServeMux for the supported surface.",
"wire_consequences": "Dispatch and extracted path values match ServeMux for the supported surface, and redirect locations retain Go 1.26.6 escaped-path serialization.",
"executable_evidence": [
"TestSupportedMatchingIsDifferentialWithServeMux",
"TestSupportedMethodsAndLiteralHostsAreDifferentialWithServeMux"
"TestSupportedMethodsAndLiteralHostsAreDifferentialWithServeMux",
"TestPinnedServeMuxRedirectsPreserveGo126EscapedPathBehavior"
],
"fixture_evidence": [
"compatibility_security_test.go"
Expand All @@ -87,7 +90,7 @@ Superseded decisions remain linked from their replacements.
}
```

Authority URL: https://go.dev/src/net/http/server.go?m=text
Authority URL: https://raw.githubusercontent.com/golang/go/go1.26.6/src/net/http/server.go

</details>

Expand Down Expand Up @@ -173,7 +176,7 @@ Authority URL: https://go.dev/src/net/http/server.go?m=text
}
```

Authority URL: https://go.dev/src/net/http/server.go?m=text
Authority URL: https://raw.githubusercontent.com/golang/go/go1.26.6/src/net/http/server.go

</details>

Expand Down Expand Up @@ -361,17 +364,20 @@ Authority URL: https://www.rfc-editor.org/rfc/rfc9112.txt
disable all canonicalization, clean decoded paths, or classify structural
changes using escaped paths. Routers disagree on `%2F` and trailing slash.
- **Selected behavior, security and resource consequences, compatibility and wire consequences:**
Follow ServeMux canonical and subtree
redirects by default before route/method miss selection. `RejectRedirects`
converts structural redirects to 404 using escaped-path semantics and
standard patterns. Encoded separators and dot text inside a wildcard remain
data; literal and percent-encoded dot segments in registered patterns are
rejected.
Follow Go 1.26.6 ServeMux canonical and subtree redirects by default before
route/method miss selection, preserving that version's Location serialization
on later toolchains. `RejectRedirects` converts structural redirects to 404
using escaped-path semantics and standard patterns. Encoded separators and
dot text inside a wildcard remain data; literal and percent-encoded dot
segments in registered patterns are rejected.
- **Evidence, public surface, upstream, and reconsideration:**
`TestCanonicalRedirectsPrecedeRouteAndMethodSelection`,
`TestPinnedServeMuxRedirectsPreserveGo126EscapedPathBehavior`,
`TestPinnedSubtreeRedirectDoesNotOverrideExplicitRoot`,
`TestRejectRedirectPolicyTreatsEncodedSeparatorsAsWildcardData`, and
`TestRejectRedirectPolicyRejectsSemanticSubtreeRoots` cover `RedirectPolicy`.
Reconsider when Go canonicalization behavior changes.
Reconsider when the minimum Go version changes or a versioned compatibility
decision adopts newer redirect serialization.

<details>
<summary>Machine-auditable decision record</summary>
Expand All @@ -397,14 +403,16 @@ Authority URL: https://www.rfc-editor.org/rfc/rfc9112.txt
"Classify structural changes using escaped paths"
],
"peer_behavior": "Go 1.26.6 ServeMux is the maintained differential peer for canonical redirects, escaped slashes, dot segments, and subtree roots.",
"selected_behavior": "Follow ServeMux canonical and subtree redirects by default before miss selection; when RejectRedirects is selected, convert structural redirects to 404 using escaped-path semantics while keeping encoded separators and dot text inside wildcards as data.",
"rationale": "Default delegation preserves Go behavior and escaped-path classification prevents encoded data from becoming false structure.",
"selected_behavior": "Follow Go 1.26.6 ServeMux canonical and subtree redirects by default before miss selection, preserving that version's redirect serialization across later toolchains; when RejectRedirects is selected, convert structural redirects to 404 using escaped-path semantics while keeping encoded separators and dot text inside wildcards as data.",
"rationale": "Pinned redirect serialization prevents toolchain upgrades from silently changing Location values, and escaped-path classification prevents encoded data from becoming false structure.",
"security_consequences": "Literal and percent-encoded dot segments in registered patterns are rejected and encoded separators cannot bypass structural checks.",
"resource_consequences": "Redirect classification uses bounded request and pattern data.",
"compatibility_consequences": "FollowRedirects remains ServeMux-compatible; RejectRedirects is an explicit defensive divergence.",
"wire_consequences": "Canonical requests redirect by default or return 404 under the explicit rejection policy.",
"compatibility_consequences": "FollowRedirects remains compatible with the pinned Go 1.26.6 ServeMux behavior on every supported toolchain; RejectRedirects is an explicit defensive divergence.",
"wire_consequences": "Canonical requests use Go 1.26.6 redirect locations by default or return 404 under the explicit rejection policy.",
"executable_evidence": [
"TestCanonicalRedirectsPrecedeRouteAndMethodSelection",
"TestPinnedServeMuxRedirectsPreserveGo126EscapedPathBehavior",
"TestPinnedSubtreeRedirectDoesNotOverrideExplicitRoot",
"TestRejectRedirectPolicyTreatsEncodedSeparatorsAsWildcardData",
"TestRejectRedirectPolicyRejectsSemanticSubtreeRoots"
],
Expand All @@ -425,11 +433,11 @@ Authority URL: https://www.rfc-editor.org/rfc/rfc9112.txt
"docs/specification-decisions.md"
],
"upstream_status": "Go 1.26.6 ServeMux is the redirect-behavior authority.",
"reconsider_when": "Go canonicalization behavior changes."
"reconsider_when": "The minimum supported Go version changes or a versioned compatibility decision adopts newer redirect serialization."
}
```

Authority URL: https://go.dev/src/net/http/server.go?m=text
Authority URL: https://raw.githubusercontent.com/golang/go/go1.26.6/src/net/http/server.go

</details>

Expand Down Expand Up @@ -601,7 +609,7 @@ Authority URL: https://www.rfc-editor.org/rfc/rfc9110.txt
}
```

Authority URL: https://go.dev/src/net/http/server.go?m=text
Authority URL: https://raw.githubusercontent.com/golang/go/go1.26.6/src/net/http/server.go

</details>

Expand Down Expand Up @@ -688,7 +696,7 @@ Authority URL: https://go.dev/src/net/http/server.go?m=text
}
```

Authority URL: https://go.dev/src/net/http/server.go?m=text
Authority URL: https://raw.githubusercontent.com/golang/go/go1.26.6/src/net/http/server.go

</details>

Expand Down Expand Up @@ -772,7 +780,7 @@ Authority URL: https://go.dev/src/net/http/server.go?m=text
}
```

Authority URL: https://go.dev/src/net/http/server.go?m=text
Authority URL: https://raw.githubusercontent.com/golang/go/go1.26.6/src/net/http/server.go

</details>

Expand Down Expand Up @@ -859,7 +867,7 @@ Authority URL: https://go.dev/src/net/http/server.go?m=text
}
```

Authority URL: https://go.dev/src/net/http/request.go?m=text
Authority URL: https://raw.githubusercontent.com/golang/go/go1.26.6/src/net/http/request.go

</details>

Expand Down
Loading
Loading