Skip to content

Commit 7df7aae

Browse files
Fix infinite loop on root page if publicPath is set (#3234)
* Add redirect if publicPath prefix is missing * CR feedback Co-authored-by: Ross Nelson <ross.nelson@temporal.io> --------- Co-authored-by: Ross Nelson <ross.nelson@temporal.io>
1 parent 37d0ae4 commit 7df7aae

File tree

2 files changed

+225
-1
lines changed

2 files changed

+225
-1
lines changed

server/server/route/ui.go

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,8 @@ func buildUIIndexHandler(publicPath string, assets fs.FS) (echo.HandlerFunc, err
7676
if err != nil {
7777
return nil, err
7878
}
79-
if publicPath != "" {
79+
hasPublicPath := publicPath != ""
80+
if hasPublicPath {
8081
indexHTML := string(indexHTMLBytes)
8182
indexHTML = strings.ReplaceAll(indexHTML, "base: \"\"", fmt.Sprintf("base: \"%s\"", publicPath))
8283
indexHTML = strings.ReplaceAll(indexHTML, "\"/_app/", fmt.Sprintf("\"%s/_app/", publicPath))
@@ -86,6 +87,18 @@ func buildUIIndexHandler(publicPath string, assets fs.FS) (echo.HandlerFunc, err
8687
}
8788

8889
return func(c echo.Context) (err error) {
90+
hasPublicPathPrefix := strings.HasPrefix(c.Request().RequestURI, publicPath+"/")
91+
isExactPublicPath := c.Request().URL.Path == publicPath
92+
missingPublicPath := hasPublicPath && !hasPublicPathPrefix && !isExactPublicPath
93+
94+
if missingPublicPath {
95+
cleanPath := path.Clean(c.Request().URL.Path)
96+
target := publicPath + cleanPath
97+
if c.Request().URL.RawQuery != "" {
98+
target += "?" + c.Request().URL.RawQuery
99+
}
100+
return c.Redirect(http.StatusPermanentRedirect, target)
101+
}
89102
return c.Stream(200, "text/html", bytes.NewBuffer(indexHTMLBytes))
90103
}, nil
91104
}

server/server/route/ui_test.go

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
package route
2+
3+
import (
4+
"net/http"
5+
"net/http/httptest"
6+
"testing"
7+
"testing/fstest"
8+
9+
"github.com/labstack/echo/v4"
10+
"github.com/stretchr/testify/assert"
11+
)
12+
13+
var testIndexHTML = `<!DOCTYPE html>
14+
<html>
15+
<head>
16+
<meta http-equiv="content-security-policy" content="default-src 'self'">
17+
<script>
18+
__sveltekit_12oxofr = {
19+
base: ""
20+
};
21+
</script>
22+
<link rel="stylesheet" href="/_app/style.css">
23+
</head>
24+
<body></body>
25+
</html>`
26+
27+
func newTestAssets(html string) fstest.MapFS {
28+
return fstest.MapFS{
29+
"index.html": &fstest.MapFile{Data: []byte(html)},
30+
}
31+
}
32+
33+
func TestBuildUIIndexHandler_NoPublicPath(t *testing.T) {
34+
handler, err := buildUIIndexHandler("", newTestAssets(testIndexHTML))
35+
assert.NoError(t, err)
36+
37+
tests := []struct {
38+
name string
39+
path string
40+
}{
41+
{
42+
name: "root path serves index",
43+
path: "/",
44+
},
45+
{
46+
name: "sub page serves index",
47+
path: "/namespaces",
48+
},
49+
{
50+
name: "deep path serves index",
51+
path: "/namespaces/default/workflows",
52+
},
53+
{
54+
name: "path with query serves index",
55+
path: "/namespaces?ns=default",
56+
},
57+
}
58+
59+
for _, tt := range tests {
60+
t.Run(tt.name, func(t *testing.T) {
61+
e := echo.New()
62+
req := httptest.NewRequest(http.MethodGet, tt.path, nil)
63+
rec := httptest.NewRecorder()
64+
c := e.NewContext(req, rec)
65+
66+
err := handler(c)
67+
assert.NoError(t, err)
68+
assert.Equal(t, http.StatusOK, rec.Code)
69+
70+
body := rec.Body.String()
71+
assert.Contains(t, body, `base: ""`)
72+
assert.Contains(t, body, `"/_app/style.css"`)
73+
assert.Contains(t, body, "content-security-policy")
74+
})
75+
}
76+
}
77+
78+
func TestBuildUIIndexHandler_WithPublicPath_RewritesBaseAndAssets(t *testing.T) {
79+
handler, err := buildUIIndexHandler("/temporal", newTestAssets(testIndexHTML))
80+
assert.NoError(t, err)
81+
82+
e := echo.New()
83+
req := httptest.NewRequest(http.MethodGet, "/temporal/", nil)
84+
rec := httptest.NewRecorder()
85+
c := e.NewContext(req, rec)
86+
87+
err = handler(c)
88+
assert.NoError(t, err)
89+
assert.Equal(t, http.StatusOK, rec.Code)
90+
91+
body := rec.Body.String()
92+
assert.Contains(t, body, `base: "/temporal"`)
93+
assert.Contains(t, body, `"/temporal/_app/style.css"`)
94+
assert.NotContains(t, body, "content-security-policy")
95+
}
96+
97+
func TestBuildUIIndexHandler_WithPublicPath_RedirectsWhenPrefixMissing(t *testing.T) {
98+
handler, err := buildUIIndexHandler("/temporal", newTestAssets(testIndexHTML))
99+
assert.NoError(t, err)
100+
101+
tests := []struct {
102+
name string
103+
requestURI string
104+
path string
105+
query string
106+
expectedTarget string
107+
}{
108+
{
109+
name: "root path redirects to public path",
110+
requestURI: "/",
111+
path: "/",
112+
expectedTarget: "/temporal/",
113+
},
114+
{
115+
name: "sub page redirects with prefix",
116+
requestURI: "/namespaces",
117+
path: "/namespaces",
118+
expectedTarget: "/temporal/namespaces",
119+
},
120+
{
121+
name: "sub page preserves query params",
122+
requestURI: "/namespaces?ns=default",
123+
path: "/namespaces",
124+
query: "ns=default",
125+
expectedTarget: "/temporal/namespaces?ns=default",
126+
},
127+
{
128+
name: "deep path redirects with prefix",
129+
requestURI: "/namespaces/default/workflows",
130+
path: "/namespaces/default/workflows",
131+
expectedTarget: "/temporal/namespaces/default/workflows",
132+
},
133+
}
134+
135+
for _, tt := range tests {
136+
t.Run(tt.name, func(t *testing.T) {
137+
e := echo.New()
138+
target := tt.path
139+
if tt.query != "" {
140+
target += "?" + tt.query
141+
}
142+
req := httptest.NewRequest(http.MethodGet, target, nil)
143+
req.RequestURI = tt.requestURI
144+
rec := httptest.NewRecorder()
145+
c := e.NewContext(req, rec)
146+
147+
err := handler(c)
148+
assert.NoError(t, err)
149+
assert.Equal(t, http.StatusPermanentRedirect, rec.Code)
150+
assert.Equal(t, tt.expectedTarget, rec.Header().Get("Location"))
151+
})
152+
}
153+
}
154+
155+
func TestBuildUIIndexHandler_WithPublicPath_ServesWhenPrefixPresent(t *testing.T) {
156+
handler, err := buildUIIndexHandler("/temporal", newTestAssets(testIndexHTML))
157+
assert.NoError(t, err)
158+
159+
tests := []struct {
160+
name string
161+
requestURI string
162+
path string
163+
}{
164+
{
165+
name: "prefixed root",
166+
requestURI: "/temporal/",
167+
path: "/",
168+
},
169+
{
170+
name: "prefixed sub page",
171+
requestURI: "/temporal/namespaces",
172+
path: "/namespaces",
173+
},
174+
{
175+
name: "prefixed with query",
176+
requestURI: "/temporal/namespaces?ns=default",
177+
path: "/namespaces",
178+
},
179+
{
180+
name: "exact public path",
181+
requestURI: "/temporal",
182+
path: "/temporal",
183+
},
184+
{
185+
name: "exact public path with query",
186+
requestURI: "/temporal?q=1",
187+
path: "/temporal",
188+
},
189+
}
190+
191+
for _, tt := range tests {
192+
t.Run(tt.name, func(t *testing.T) {
193+
e := echo.New()
194+
req := httptest.NewRequest(http.MethodGet, tt.path, nil)
195+
req.RequestURI = tt.requestURI
196+
rec := httptest.NewRecorder()
197+
c := e.NewContext(req, rec)
198+
199+
err := handler(c)
200+
assert.NoError(t, err)
201+
assert.Equal(t, http.StatusOK, rec.Code)
202+
assert.Contains(t, rec.Body.String(), `base: "/temporal"`)
203+
})
204+
}
205+
}
206+
207+
func TestBuildUIIndexHandler_MissingIndexHTML(t *testing.T) {
208+
emptyFS := fstest.MapFS{}
209+
_, err := buildUIIndexHandler("", emptyFS)
210+
assert.Error(t, err)
211+
}

0 commit comments

Comments
 (0)