Skip to content

Commit b396408

Browse files
authored
fix: handle urls with multiple slashes (#16527)
Fixes: #9877 This PR introduces another middleware to rewrite URLs when multiple slashes are used.
1 parent 5ec385b commit b396408

File tree

3 files changed

+104
-0
lines changed

3 files changed

+104
-0
lines changed

coderd/coderd.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -788,6 +788,7 @@ func New(options *Options) *API {
788788
httpmw.AttachRequestID,
789789
httpmw.ExtractRealIP(api.RealIPConfig),
790790
httpmw.Logger(api.Logger),
791+
singleSlashMW,
791792
rolestore.CustomRoleMW,
792793
prometheusMW,
793794
// Build-Version is helpful for debugging.
@@ -1731,3 +1732,31 @@ func ReadExperiments(log slog.Logger, raw []string) codersdk.Experiments {
17311732
}
17321733
return exps
17331734
}
1735+
1736+
var multipleSlashesRe = regexp.MustCompile(`/+`)
1737+
1738+
func singleSlashMW(next http.Handler) http.Handler {
1739+
fn := func(w http.ResponseWriter, r *http.Request) {
1740+
var path string
1741+
rctx := chi.RouteContext(r.Context())
1742+
if rctx != nil && rctx.RoutePath != "" {
1743+
path = rctx.RoutePath
1744+
} else {
1745+
path = r.URL.Path
1746+
}
1747+
1748+
// Normalize multiple slashes to a single slash
1749+
newPath := multipleSlashesRe.ReplaceAllString(path, "/")
1750+
1751+
// Apply the cleaned path
1752+
// The approach is consistent with: https://github.com/go-chi/chi/blob/e846b8304c769c4f1a51c9de06bebfaa4576bd88/middleware/strip.go#L24-L28
1753+
if rctx != nil {
1754+
rctx.RoutePath = newPath
1755+
} else {
1756+
r.URL.Path = newPath
1757+
}
1758+
1759+
next.ServeHTTP(w, r)
1760+
}
1761+
return http.HandlerFunc(fn)
1762+
}

coderd/coderd_internal_test.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package coderd
2+
3+
import (
4+
"context"
5+
"net/http"
6+
"net/http/httptest"
7+
"testing"
8+
9+
"github.com/go-chi/chi/v5"
10+
"github.com/stretchr/testify/assert"
11+
)
12+
13+
func TestStripSlashesMW(t *testing.T) {
14+
t.Parallel()
15+
16+
tests := []struct {
17+
name string
18+
inputPath string
19+
wantPath string
20+
}{
21+
{"No changes", "/api/v1/buildinfo", "/api/v1/buildinfo"},
22+
{"Double slashes", "/api//v2//buildinfo", "/api/v2/buildinfo"},
23+
{"Triple slashes", "/api///v2///buildinfo", "/api/v2/buildinfo"},
24+
{"Leading slashes", "///api/v2/buildinfo", "/api/v2/buildinfo"},
25+
{"Root path", "/", "/"},
26+
{"Double slashes root", "//", "/"},
27+
{"Only slashes", "/////", "/"},
28+
}
29+
30+
handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
31+
w.WriteHeader(http.StatusOK)
32+
})
33+
34+
for _, tt := range tests {
35+
tt := tt
36+
37+
t.Run("chi/"+tt.name, func(t *testing.T) {
38+
t.Parallel()
39+
req := httptest.NewRequest("GET", tt.inputPath, nil)
40+
rec := httptest.NewRecorder()
41+
42+
// given
43+
rctx := chi.NewRouteContext()
44+
rctx.RoutePath = tt.inputPath
45+
req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx))
46+
47+
// when
48+
singleSlashMW(handler).ServeHTTP(rec, req)
49+
updatedCtx := chi.RouteContext(req.Context())
50+
51+
// then
52+
assert.Equal(t, tt.inputPath, req.URL.Path)
53+
assert.Equal(t, tt.wantPath, updatedCtx.RoutePath)
54+
})
55+
56+
t.Run("stdlib/"+tt.name, func(t *testing.T) {
57+
t.Parallel()
58+
req := httptest.NewRequest("GET", tt.inputPath, nil)
59+
rec := httptest.NewRecorder()
60+
61+
// when
62+
singleSlashMW(handler).ServeHTTP(rec, req)
63+
64+
// then
65+
assert.Equal(t, tt.wantPath, req.URL.Path)
66+
assert.Nil(t, chi.RouteContext(req.Context()))
67+
})
68+
}
69+
}

site/vite.config.mts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,12 @@ export default defineConfig({
5252
"csrf_token=JXm9hOUdZctWt0ZZGAy9xiS/gxMKYOThdxjjMnMUyn4=; Path=/; HttpOnly; SameSite=Lax",
5353
},
5454
proxy: {
55+
"//": {
56+
changeOrigin: true,
57+
target: process.env.CODER_HOST || "http://localhost:3000",
58+
secure: process.env.NODE_ENV === "production",
59+
rewrite: (path) => path.replace(/\/+/g, "/"),
60+
},
5561
"/api": {
5662
ws: true,
5763
changeOrigin: true,

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy