Skip to content

Commit 271ebcb

Browse files
authored
fix: correctly serves websites on start/run (#863)
1 parent 6b2a6d1 commit 271ebcb

5 files changed

Lines changed: 155 additions & 45 deletions

File tree

.github/workflows/dashboard-run-test.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ jobs:
5050
5151
- name: Run Tests
5252
uses: cypress-io/github-action@v5
53+
env:
54+
CYPRESS_NITRIC_TEST_TYPE: "run"
5355
with:
5456
install: false
5557
wait-on: "http://localhost:49152"

.github/workflows/dashboard-start-test.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ jobs:
6464
wait-on-timeout: 180
6565
working-directory: cli/pkg/dashboard/frontend
6666
browser: chrome
67+
env:
68+
CYPRESS_NITRIC_TEST_TYPE: "start"
6769

6870
- uses: actions/upload-artifact@v4
6971
if: failure()

pkg/cloud/websites/websites.go

Lines changed: 131 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
"net/url"
2727
"os"
2828
"path/filepath"
29+
"slices"
2930
"strings"
3031
"sync"
3132

@@ -56,7 +57,6 @@ type (
5657
type LocalWebsiteService struct {
5758
websiteRegLock sync.RWMutex
5859
state State
59-
port int
6060
getApiAddress GetApiAddress
6161
isStartCmd bool
6262

@@ -75,12 +75,12 @@ func (l *LocalWebsiteService) SubscribeToState(fn func(State)) {
7575
}
7676

7777
// register - Register a new website
78-
func (l *LocalWebsiteService) register(website Website) {
78+
func (l *LocalWebsiteService) register(website Website, port int) {
7979
l.websiteRegLock.Lock()
8080
defer l.websiteRegLock.Unlock()
8181

8282
// Emulates the CDN URL used in a deployed environment
83-
publicUrl := fmt.Sprintf("http://localhost:%d/%s", l.port, strings.TrimPrefix(website.BasePath, "/"))
83+
publicUrl := fmt.Sprintf("http://localhost:%d/%s", port, strings.TrimPrefix(website.BasePath, "/"))
8484

8585
l.state[website.Name] = Website{
8686
WebsitePb: website.WebsitePb,
@@ -95,9 +95,9 @@ func (l *LocalWebsiteService) register(website Website) {
9595

9696
type staticSiteHandler struct {
9797
website *Website
98-
port int
9998
devURL string
10099
isStartCmd bool
100+
server *http.Server
101101
}
102102

103103
func (h staticSiteHandler) serveProxy(res http.ResponseWriter, req *http.Request) {
@@ -117,6 +117,17 @@ func (h staticSiteHandler) serveProxy(res http.ResponseWriter, req *http.Request
117117
return
118118
}
119119

120+
// Strip the base path from the request path before proxying
121+
if h.website.BasePath != "/" {
122+
// redirect to base if path is / and there is no query string
123+
if req.RequestURI == "/" {
124+
http.Redirect(res, req, h.website.BasePath, http.StatusFound)
125+
return
126+
}
127+
128+
req.URL.Path = strings.TrimPrefix(req.URL.Path, h.website.BasePath)
129+
}
130+
120131
// Reverse proxy request
121132
proxy := httputil.NewSingleHostReverseProxy(targetUrl)
122133

@@ -152,7 +163,7 @@ func (h staticSiteHandler) serveStatic(res http.ResponseWriter, req *http.Reques
152163
}
153164

154165
if fi.IsDir() {
155-
http.ServeFile(res, req, filepath.Join(h.website.OutputDirectory, h.website.IndexDocument))
166+
http.ServeFile(res, req, filepath.Join(path, h.website.IndexDocument))
156167

157168
return
158169
}
@@ -171,21 +182,9 @@ func (h staticSiteHandler) ServeHTTP(res http.ResponseWriter, req *http.Request)
171182
h.serveStatic(res, req)
172183
}
173184

174-
// Start - Start the local website service
175-
func (l *LocalWebsiteService) Start(websites []Website) error {
176-
newLis, err := netx.GetNextListener(netx.MinPort(5000))
177-
if err != nil {
178-
return err
179-
}
180-
181-
l.port = newLis.Addr().(*net.TCPAddr).Port
182-
183-
_ = newLis.Close()
184-
185-
mux := http.NewServeMux()
186-
187-
// Register the API proxy handler
188-
mux.HandleFunc("/api/{name}/", func(res http.ResponseWriter, req *http.Request) {
185+
// createAPIPathHandler creates a handler for API proxy requests
186+
func (l *LocalWebsiteService) createAPIPathHandler() http.HandlerFunc {
187+
return func(res http.ResponseWriter, req *http.Request) {
189188
apiName := req.PathValue("name")
190189

191190
apiAddress := l.getApiAddress(apiName)
@@ -201,31 +200,126 @@ func (l *LocalWebsiteService) Start(websites []Website) error {
201200
req.URL.Path = targetPath
202201

203202
proxy.ServeHTTP(res, req)
203+
}
204+
}
205+
206+
// createServer creates and configures an HTTP server with the given mux
207+
func (l *LocalWebsiteService) createServer(mux *http.ServeMux, port int) *http.Server {
208+
return &http.Server{
209+
Addr: fmt.Sprintf(":%d", port),
210+
Handler: mux,
211+
}
212+
}
213+
214+
// startServer starts the given server in a goroutine and handles errors
215+
func (l *LocalWebsiteService) startServer(server *http.Server, errChan chan error, errMsg string) {
216+
go func() {
217+
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
218+
select {
219+
case errChan <- fmt.Errorf(errMsg, err):
220+
default:
221+
}
222+
}
223+
}()
224+
}
225+
226+
// Start - Start the local website service
227+
func (l *LocalWebsiteService) Start(websites []Website) error {
228+
errChan := make(chan error, 1)
229+
230+
startPort := 5000
231+
232+
slices.SortFunc(websites, func(a, b Website) int {
233+
return strings.Compare(a.BasePath, b.BasePath)
204234
})
205235

206-
// Register the SPA handler for each website
207-
for i := range websites {
208-
website := &websites[i]
209-
spa := staticSiteHandler{website: website, port: l.port, devURL: website.DevURL, isStartCmd: l.isStartCmd}
236+
if l.isStartCmd {
237+
// In start mode, create individual servers for each website
238+
for i := range websites {
239+
website := &websites[i]
210240

211-
if website.BasePath == "/" {
241+
// Get a new listener for each website, incrementing the port each time
242+
newLis, err := netx.GetNextListener(netx.MinPort(startPort + i))
243+
if err != nil {
244+
return err
245+
}
246+
247+
port := newLis.Addr().(*net.TCPAddr).Port
248+
_ = newLis.Close()
249+
250+
mux := http.NewServeMux()
251+
252+
// Register the API proxy handler for this website
253+
mux.HandleFunc("/api/{name}/", l.createAPIPathHandler())
254+
255+
// Create the SPA handler for this website
256+
spa := staticSiteHandler{
257+
website: website,
258+
devURL: website.DevURL,
259+
isStartCmd: l.isStartCmd,
260+
}
261+
262+
// Register the SPA handler
212263
mux.Handle("/", spa)
213-
} else {
214-
mux.Handle(website.BasePath+"/", http.StripPrefix(website.BasePath+"/", spa))
264+
265+
// Create and start the server
266+
server := l.createServer(mux, port)
267+
268+
// Store the server in the handler for potential cleanup
269+
spa.server = server
270+
271+
// Register the website with its port
272+
l.register(*website, port)
273+
274+
// Start the server in a goroutine
275+
l.startServer(server, errChan, "failed to start server for website %s: %w")
276+
}
277+
} else {
278+
// For static serving, use a single server
279+
newLis, err := netx.GetNextListener(netx.MinPort(startPort))
280+
if err != nil {
281+
return err
215282
}
216-
}
217283

218-
// Start the server with the multiplexer
219-
go func() {
220-
addr := fmt.Sprintf(":%d", l.port)
221-
if err := http.ListenAndServe(addr, mux); err != nil {
222-
fmt.Printf("Failed to start server: %s\n", err)
284+
port := newLis.Addr().(*net.TCPAddr).Port
285+
_ = newLis.Close()
286+
287+
mux := http.NewServeMux()
288+
289+
// Register the API proxy handler
290+
mux.HandleFunc("/api/{name}/", l.createAPIPathHandler())
291+
292+
// Register the SPA handler for each website
293+
for i := range websites {
294+
website := &websites[i]
295+
spa := staticSiteHandler{
296+
website: website,
297+
devURL: website.DevURL,
298+
isStartCmd: l.isStartCmd,
299+
}
300+
301+
if website.BasePath == "/" {
302+
mux.Handle("/", spa)
303+
} else {
304+
mux.Handle(website.BasePath+"/", http.StripPrefix(website.BasePath+"/", spa))
305+
}
223306
}
224-
}()
225307

226-
// Register the websites
227-
for _, website := range websites {
228-
l.register(website)
308+
// Register all websites with the same port
309+
for _, website := range websites {
310+
l.register(website, port)
311+
}
312+
313+
// Create and start the server
314+
server := l.createServer(mux, port)
315+
316+
// Start the server in a goroutine
317+
l.startServer(server, errChan, "failed to start static server: %w")
318+
}
319+
320+
// Return the first error that occurred, if any
321+
if err := <-errChan; err != nil {
322+
return err
229323
}
230324

231325
return nil

pkg/dashboard/frontend/cypress/e2e/websites.cy.ts

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,29 @@ describe('Websites Spec', () => {
2020
cy.get(`[data-rct-item-id="${id}"]`).click()
2121
cy.get('h2').should('contain.text', id)
2222

23-
const pathMap = {
24-
'vite-website': '',
25-
'docs-website': 'docs',
23+
let originMap = {}
24+
25+
if (Cypress.env('NITRIC_TEST_TYPE') === 'run') {
26+
originMap = {
27+
'vite-website': 'http://localhost:5000',
28+
'docs-website': 'http://localhost:5000',
29+
}
30+
} else {
31+
originMap = {
32+
'vite-website': 'http://localhost:5000',
33+
'docs-website': 'http://localhost:5001',
34+
}
2635
}
2736

28-
const url = `http://localhost:5000/${pathMap[id]}`
37+
const pathMap = {
38+
'vite-website': '/',
39+
'docs-website': '/docs',
40+
}
2941

3042
// check iframe url
31-
cy.get('iframe').should('have.attr', 'src', url)
43+
cy.get('iframe').should('have.attr', 'src', originMap[id] + pathMap[id])
3244

33-
cy.visit(url)
45+
cy.visit(originMap[id] + pathMap[id])
3446

3547
const titleMap = {
3648
'vite-website': 'Hello Nitric!',
@@ -39,7 +51,7 @@ describe('Websites Spec', () => {
3951

4052
const title = titleMap[id]
4153

42-
cy.origin('http://localhost:5000', { args: { title } }, ({ title }) => {
54+
cy.origin(originMap[id], { args: { title } }, ({ title }) => {
4355
cy.get('h1').should('have.text', title)
4456
})
4557
})

pkg/project/project.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -827,7 +827,7 @@ func fromProjectConfiguration(projectConfig *ProjectConfiguration, localConfig *
827827
}
828828

829829
if websiteSpec.ErrorPage == "" {
830-
websiteSpec.ErrorPage = "index.html"
830+
websiteSpec.ErrorPage = "404.html"
831831
} else if !strings.HasSuffix(websiteSpec.ErrorPage, ".html") {
832832
return nil, fmt.Errorf("invalid error page %s, must end with .html", websiteSpec.ErrorPage)
833833
}

0 commit comments

Comments
 (0)