Repository navigation
Conversation
getValue records a skippedNode whenever it prefers a static child over a wildcard sibling, and rolls back to it when the static branch fails. Two dead ends inside the param case bypassed that rollback and returned not-found directly: - more path remains but the param node has no child to descend into - the param node consumed the whole remaining path but has no handler For the routes "/:p0/:p1" and "/bc/:p1/a", GET /bc/bc preferred the static bc branch, dead-ended on the handler-less :p1 node, and gave up with 404 instead of retrying the skipped /:p0 branch and matching /:p0/:p1 with p0=bc p1=bc. Both dead ends now roll back to the last valid skippedNode, consistent with the existing fallback at the other dead ends of getValue. TestTreeParamFallbackAfterStaticDeadEnd covers each dead end; each subtest fails without the corresponding fix. Signed-off-by: simpleqt <2015900818@qq.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #4919 +/- ##
=======================================
Coverage 98.65% 98.66%
=======================================
Files 48 48
Lines 3188 3210 +22
=======================================
+ Hits 3145 3167 +22
Misses 43 43
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Tales-of-zuna
left a comment
There was a problem hiding this comment.
Thanks for digging into this! I reproduced both 404s on master (GET /bc/bc with /:p0/:p1 + /bc/:p1/a, and GET /bc/bc/2 with /:p0/:p1/:p2 + /bc/:p1), so the bug is real.
Heads-up, though: #4783 (open since August) already fixes these same two dead ends, with the same route pairs and a test of the same name (TestTreeParamFallbackAfterStaticDeadEnd). It also moves the rollback loop into a rollbackToSkippedNode helper, so the five call sites share one copy. It still merges cleanly with current master, and its tests fail on master's tree.go and pass with the fix.
The one difference I found is the extra path != "/" guard here. As far as I can tell it can't change the result at either site: at the first dead end, path == "/" already sets value.tsr, and at the second, end == len(path) means path can't be "/".
Since the "Relation to open PRs" section doesn't mention #4783, it might be worth looking at it. A review or 👍 there would probably help this fix land sooner than a second PR would.
|
Thank you for checking both setups - confirmed here as well: You are right about On #4783: I added it to the "Relation to open PRs" section as you suggested. Two things I verified while doing so, in case they are useful for the decision to back it rather than merge a second PR here:
Happy to rebase onto #4783, drop the parts of this branch it covers, or close this one in favour of it - whatever is least work for you. |
fix(tree): fall back to skipped wildcard when a param branch dead-ends
Relation to open PRs in the same area
The recently opened #4891/#4894/#4906 (trailing-slash redirect preservation when the walk rolls back to a skipped wildcard) touch the same
skippedNodesrollback machinery intree.gobut fix a different behavior: they keep the TSR redirect during an existing rollback, while this PR adds the missing rollback at two dead ends that returned 404 directly. The changes are complementary; a small textual overlap ingetValueis possible and trivially resolvable.Problem
getValue(tree.go) records askippedNodewhenever it prefers a static child over a wildcard sibling, and rolls back to it when the static branch fails. Two dead ends inside thecase param:block bypassed that rollback and returned not-found directly:// ... but we can't);len(n.children) == 1TSR check falls through).Repro
The walk prefers the static
bcbranch, dead-ends on the handler-less:p1node (its handler lives on the child/a), and previously gave up with 404 instead of retrying the skipped/:p0branch.GET /bc/bcshould match/:p0/:p1withp0=bc,p1=bc— exactly likeGET /x/yalready does. The second dead end is reachable with the route pair/:p0/:p1/:p2+/bc/:p1viaGET /bc/bc/2(should match/:p0/:p1/:p2).Root cause
The two param-case dead ends
return valuewithout consultingskippedNodes, inconsistent with the three other dead ends of the same walk (the!n.wildChildblock, thepath == prefixblock, and the final "Nothing found" block), which all roll back to the last validskippedNode.Fix
Both dead ends now run the same rollback loop the other dead ends already use (pop the last skipped node, restore
path/n, truncate params toparamsCount,continue walk), guarded by!value.tsr && path != "/"like the existing sites. The rolled-back node replica intentionally carries noindices, so a retry cannot re-enter the static branch or re-push the same entry; the walk therefore stays bounded and cannot loop.Tests
TestTreeParamFallbackAfterStaticDeadEnd(tree_test.go) covers each dead end in table style with param-value assertions:param consumed the whole path but has no handler— routes/:p0/:p1+/bc/:p1/a; expectsGET /bc/bcto match/:p0/:p1(and/bc/bc/a,/x/yunchanged);more path remains but the param node has no child— routes/:p0/:p1/:p2+/bc/:p1; expectsGET /bc/bc/2to match/:p0/:p1/:p2(and/bc/bc,/x/y/2unchanged).Red/green evidence
Base commit
0f09c3a(master, 2026-10-10), Windows/amd64, Go 1.27.1.RED — pristine base worktree + test file only (
go test -run TestTreeParamFallbackAfterStaticDeadEnd -v .):GREEN — with the fix (
go test -run TestTreeParamFallbackAfterStaticDeadEnd -v .):Package-level: on this Windows machine the suite has four pre-existing failures on unchanged master (
TestSaveUploadedFileWithPermission,TestSaveUploadedFileWithPermissionFailed,TestSaveUploadedFileToExistingDir,TestFileDescriptor).go test ./...reports exactly those 4--- FAILlines with the fix and the same 4 plusTestTreeParamFallbackAfterStaticDeadEndwithout it — no other test changes behavior.go vet .passes;golangci-lint run ./...(v2.6.2, repo.golangci.yml) reports 0 issues.Disclosure
This fix and its regression test were produced with AI assistance; the reproductions, red/green runs and lint results shown above were executed locally against master at
0f09c3a.Checklist
masterbranch.Relation to open PRs
*skippedNodeson entry to getValue (tree.go:428, from fix: reset skipped-nodes stack on getValue entry to prevent slice overflow panic [#4818] #4819). Happy to fold this work into fix(tree): roll back to skipped wildcards at param route dead ends #4783 instead.