Skip to content

Commit 2d9d254

Browse files
committed
test: static/dynamic/named/splat query params
adds missing test + ensures any params from _redirects are preserved this follows what is already supported by Cloudflare Pages implementation: https://developers.cloudflare.com/pages/configuration/redirects/#per-file
1 parent e3970d4 commit 2d9d254

File tree

4 files changed

+154
-2
lines changed

4 files changed

+154
-2
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ The following emojis are used to highlight certain changes:
2222

2323
### Fixed
2424

25+
- `gateway`: query parameters are now supported and preserved in redirects triggered by a [`_redirects`](https://specs.ipfs.tech/http-gateways/web-redirects-file/) file [#886](https://github.com/ipfs/boxo/pull/886)
26+
2527
### Security
2628

2729

gateway/gateway_test.go

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -816,6 +816,141 @@ func TestRedirects(t *testing.T) {
816816
do(http.MethodHead)
817817
})
818818

819+
t.Run("_redirects file redirect preserves both static and dynamic query parameters", func(t *testing.T) {
820+
t.Parallel()
821+
822+
backend, root := newMockBackend(t, "redirects-query-params.car")
823+
backend.namesys["/ipns/example.org"] = newMockNamesysItem(path.FromCid(root), 0)
824+
825+
ts := newTestServerWithConfig(t, backend, Config{
826+
NoDNSLink: false,
827+
PublicGateways: map[string]*PublicGateway{
828+
"example.org": {
829+
UseSubdomains: true,
830+
DeserializedResponses: true,
831+
},
832+
},
833+
DeserializedResponses: true,
834+
})
835+
836+
// Request for missing page with some dynamic parameters
837+
missingPageURL := ts.URL + "/source1/source-file?dynamic-query1=dynamic-value1&dynamic-query2=dynamic-value2"
838+
839+
// Expected response is redirect URL that preserves both static and dynamic parameters
840+
expectedTargetURL := "/target-file?dynamic-query1=dynamic-value1&dynamic-query2=dynamic-value2&static-query1=static-val1&static-query2=static-val2"
841+
842+
do := func(method string) {
843+
// Make initial request to non-existing page that should return redirect that contains both query params from original request AND target URL in _redirects file.
844+
req := mustNewRequest(t, method, missingPageURL, nil)
845+
req.Header.Add("Accept", "text/html")
846+
req.Host = "example.org"
847+
848+
res := mustDoWithoutRedirect(t, req)
849+
defer res.Body.Close()
850+
851+
// Status code should match 301 from _redirects catch-all rule
852+
require.Equal(t, http.StatusMovedPermanently, res.StatusCode)
853+
854+
// Check Redirect target contains all query parameters
855+
redirectURL := res.Header.Get("Location")
856+
require.Equal(t, expectedTargetURL, redirectURL)
857+
858+
}
859+
860+
do(http.MethodGet)
861+
do(http.MethodHead)
862+
})
863+
864+
t.Run("_redirects file redirect supports named placeholders as query parameters", func(t *testing.T) {
865+
t.Parallel()
866+
867+
backend, root := newMockBackend(t, "redirects-query-params.car")
868+
backend.namesys["/ipns/example.org"] = newMockNamesysItem(path.FromCid(root), 0)
869+
870+
ts := newTestServerWithConfig(t, backend, Config{
871+
NoDNSLink: false,
872+
PublicGateways: map[string]*PublicGateway{
873+
"example.org": {
874+
UseSubdomains: true,
875+
DeserializedResponses: true,
876+
},
877+
},
878+
DeserializedResponses: true,
879+
})
880+
881+
// Request for missing page under path that has named placeholders and some dynamic parameters
882+
missingPageURL := ts.URL + "/source2/codeA/nameA?dynamic-query1=dynamic-value1&dynamic-query2=dynamic-value2"
883+
884+
// Expected response is redirect URL that preserves both named and dynamic parameters
885+
expectedTargetURL := "/target-file?code=codeA&dynamic-query1=dynamic-value1&dynamic-query2=dynamic-value2&name=nameA"
886+
887+
do := func(method string) {
888+
// Make initial request to non-existing page that should return redirect that contains both query params from original request AND target URL in _redirects file.
889+
req := mustNewRequest(t, method, missingPageURL, nil)
890+
req.Header.Add("Accept", "text/html")
891+
req.Host = "example.org"
892+
893+
res := mustDoWithoutRedirect(t, req)
894+
defer res.Body.Close()
895+
896+
// Status code should match 301 from _redirects catch-all rule
897+
require.Equal(t, http.StatusMovedPermanently, res.StatusCode)
898+
899+
// Check Redirect target contains all query parameters
900+
redirectURL := res.Header.Get("Location")
901+
require.Equal(t, expectedTargetURL, redirectURL)
902+
903+
}
904+
905+
do(http.MethodGet)
906+
do(http.MethodHead)
907+
})
908+
909+
t.Run("_redirects file redirect supports catch-all splat with dynamic query parameters", func(t *testing.T) {
910+
t.Parallel()
911+
912+
backend, root := newMockBackend(t, "redirects-query-params.car")
913+
backend.namesys["/ipns/example.org"] = newMockNamesysItem(path.FromCid(root), 0)
914+
915+
ts := newTestServerWithConfig(t, backend, Config{
916+
NoDNSLink: false,
917+
PublicGateways: map[string]*PublicGateway{
918+
"example.org": {
919+
UseSubdomains: true,
920+
DeserializedResponses: true,
921+
},
922+
},
923+
DeserializedResponses: true,
924+
})
925+
926+
// Request for missing page under path that is a catch-all splat (incl. some dynamic parameters)
927+
missingPageURL := ts.URL + "/source3/this/is/catch-all/splat-with?dynamic-query1=dynamic-value1&dynamic-query2=dynamic-value2"
928+
929+
// Expected response is redirect URL to a different server that preserves entire splat, including dynamic query parameters
930+
expectedTargetURL := "https://example.net/target3/this/is/catch-all/splat-with?dynamic-query1=dynamic-value1&dynamic-query2=dynamic-value2"
931+
932+
do := func(method string) {
933+
// Make initial request to non-existing page that should return redirect that contains both query params from original request AND target URL in _redirects file.
934+
req := mustNewRequest(t, method, missingPageURL, nil)
935+
req.Header.Add("Accept", "text/html")
936+
req.Host = "example.org"
937+
938+
res := mustDoWithoutRedirect(t, req)
939+
defer res.Body.Close()
940+
941+
// Status code should match 301 from _redirects catch-all rule
942+
require.Equal(t, http.StatusMovedPermanently, res.StatusCode)
943+
944+
// Check Redirect target contains all query parameters
945+
redirectURL := res.Header.Get("Location")
946+
require.Equal(t, expectedTargetURL, redirectURL)
947+
948+
}
949+
950+
do(http.MethodGet)
951+
do(http.MethodHead)
952+
})
953+
819954
t.Run("Superfluous namespace", func(t *testing.T) {
820955
t.Parallel()
821956

gateway/handler_unixfs_redirects.go

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"fmt"
66
"io"
77
"net/http"
8+
"net/url"
89
gopath "path"
910
"strconv"
1011
"strings"
@@ -172,9 +173,23 @@ func (i *handler) handleRedirectsFileRules(w http.ResponseWriter, r *http.Reques
172173
// Or redirect
173174
if rule.Status >= 301 && rule.Status <= 308 {
174175
redirectURL := rule.To
175-
// preserve query parameters from original request
176+
// Preserve query parameters from the original request if they exist
176177
if r.URL.RawQuery != "" {
177-
redirectURL = redirectURL + "?" + r.URL.RawQuery
178+
u, err := url.Parse(rule.To)
179+
if err != nil {
180+
return false, "", err
181+
}
182+
// Merge original query parameters into target
183+
originalQuery, _ := url.ParseQuery(r.URL.RawQuery)
184+
targetQuery := u.Query()
185+
for key, values := range originalQuery {
186+
for _, value := range values {
187+
targetQuery.Add(key, value)
188+
}
189+
}
190+
// Set the merged query parameters back
191+
u.RawQuery = targetQuery.Encode()
192+
redirectURL = u.String()
178193
}
179194
http.Redirect(w, r, redirectURL, rule.Status)
180195
return true, "", nil
749 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)