Repository navigation
fix!: URLFor escapes each value for the part of the URL it fills - #38
Merged
Merged
Conversation
URLFor path-escaped every placeholder value, including ones after ? or #
in a composed template such as []any{Page{}, "?q={q}"}. url.PathEscape
leaves &, = and + alone, so "A&B" broke the query, and a value escaped
first with url.QueryEscape came out escaped twice: no input round-tripped.
parseSegments now records whether each segment sits in the path, query
or fragment, switching only at the template's own first ? and #. Values
are held unescaped and escaped once when the URL is joined: path values
as before (wildcards keep their slashes), query and fragment values as
one query component, with spaces as %20 so existing URLs keep their form.
Values auto-filled from the current request come from r.PathValue,
already decoded, and were inserted unescaped: an id of "a/b?c" became
two path segments and a query. They are now escaped like passed values.
The segment cache is now filled only at Mount, once per route. URLFor
used to cache every pattern it saw, so a template composed per request
("?" + v.Encode()) grew the cache without bound.
BREAKING CHANGE: pass placeholder values raw. A value escaped with
url.QueryEscape is now escaped again and reads back escaped, and a
placeholder can no longer inject raw query text such as "&sort=asc";
put that text in the template instead.
Claude-Session: https://claude.ai/code/session_01NpjJeP32ycvAHNFXRgTviG
The new escape category reports url.QueryEscape, url.PathEscape and url.Values.Encode calls passed as URLFor values (positional, key/value or in a literal map), which URLFor now escapes again. It needs no page tree, so it runs outside the per-tree check, and covers gsx url filters through the generated code. Claude-Session: https://claude.ai/code/session_01NpjJeP32ycvAHNFXRgTviG
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #38 +/- ##
==========================================
- Coverage 95.32% 95.22% -0.10%
==========================================
Files 17 17
Lines 1754 1761 +7
==========================================
+ Hits 1672 1677 +5
- Misses 49 50 +1
- Partials 33 34 +1 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
URLForpath-escaped every placeholder value, including placeholders after?or#in a composed template like[]any{Page{}, "?q={q}"}.url.PathEscapeleaves&,=and+alone, so no input round-tripped:url.QueryEscapedA&Bq=A&B→A+ stray keyBq=A%2526B→A%26Bc+dq=c+d→c dq=c%252Bd→c%2BdThe old test at
url_for_test.go:1237locked in/search?q=foo&bar&filter=a=b, which parses to three keys.Changes
Runtime (
fix!)parseSegmentsrecords whether each segment is in the path, query or fragment. The part switches only at the template's own first?/#, never at one inside a value.joinSegments:PathEscape; wildcards keep their slashes), so path-only URLs from passed values are byte-identical;QueryEscape, with+written as%20, which is exact since a literal+becomes%2B).r.PathValue, already decoded) were inserted unescaped, soa/b?cbecame two segments and a query. They're now escaped like passed values.Mountfills the segment cache now, once per route.URLForused to cache every pattern it saw, so a per-request template ("?" + v.Encode(), which the docs now recommend) grew it without bound. Found in review.Lint (
feat(lint))escapecategory flagsurl.QueryEscape,url.PathEscapeandurl.Values.Encode()passed asURLForvalues, whether positional, key/value or in a literal map. It also covers gsxurl(...)through the generated code.alerts.gsx:40.Docs:
docs/urlfor.md,docs/lint.md,skills/structpages/reference.mdand theURLForgodoc.Breaking
Pass placeholder values raw.
url.QueryEscapenow reads back escaped. Theescapelint finds these."?{extra}"with"&sort=asc"). Put that text in the template, e.g.[]any{page, "?" + v.Encode()}.Unchanged on purpose: the documented arg detection where as many args as placeholders means positional.
Tests
url_for_escape_test.gohas round-trip tests that parse the generated URL back and compare decoded parts. They cover query values, keys and partial values, path, fragment, and mixed templates; every arg form; values from the request including underWithURLPrefix; and the cache bound. 16 of them fail onmain, and the cache test fails ifURLForcaches again.tools/lint/escape_integration_test.gocovers flagged and allowed forms, includingsp.URLForand a suppressed line.go test ./...passes and golangci-lint is clean.tools/lint,TestRegion_Integrationfails onmaintoo (missing templ findings), unrelated.An independent review found no correctness bugs in the escaping. Its one medium finding, the cache growth, is fixed here.
https://claude.ai/code/session_01NpjJeP32ycvAHNFXRgTviG