From dd6d315cada586d2857bb0b667ca388c7a49795e Mon Sep 17 00:00:00 2001 From: Bryan Phelps Date: Sat, 5 Feb 2022 00:35:32 +0000 Subject: [PATCH 01/41] Add in-memory fs example --- go.mod | 1 + go.sum | 2 ++ nextrouter/nextrouter_test.go | 29 +++++++++++++++++++++++++++++ 3 files changed, 32 insertions(+) create mode 100644 nextrouter/nextrouter_test.go diff --git a/go.mod b/go.mod index 7b2557fa99db8..a2d03eda09857 100644 --- a/go.mod +++ b/go.mod @@ -98,6 +98,7 @@ require ( github.com/pion/udp v0.1.1 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/psanford/memfs v0.0.0-20210214183328-a001468d78ef // indirect github.com/sirupsen/logrus v1.8.1 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect diff --git a/go.sum b/go.sum index a05e8a7a74e34..652414d12c605 100644 --- a/go.sum +++ b/go.sum @@ -1087,6 +1087,8 @@ github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4O github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/psanford/memfs v0.0.0-20210214183328-a001468d78ef h1:NKxTG6GVGbfMXc2mIk+KphcH6hagbVXhcFkbTgYleTI= +github.com/psanford/memfs v0.0.0-20210214183328-a001468d78ef/go.mod h1:tcaRap0jS3eifrEEllL6ZMd9dg8IlDpi2S1oARrQ+NI= github.com/remyoudompheng/bigfft v0.0.0-20190728182440-6a916e37a237/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= diff --git a/nextrouter/nextrouter_test.go b/nextrouter/nextrouter_test.go new file mode 100644 index 0000000000000..e92e6160be43c --- /dev/null +++ b/nextrouter/nextrouter_test.go @@ -0,0 +1,29 @@ +package nextrouter_test + +import ( + "io/fs" + "testing" + + "github.com/psanford/memfs" + "github.com/stretchr/testify/require" +) + +func TestConn(t *testing.T) { + t.Parallel() + + t.Run("Smoke test", func(t *testing.T) { + t.Parallel() + + rootFS := memfs.New() + err := rootFS.MkdirAll("test/a/b", 0777) + require.NoError(t, err) + + rootFS.WriteFile("test/a/b/c.txt", []byte("test123"), 0755) + content, err := fs.ReadFile(rootFS, "test/a/b/c.txt") + require.NoError(t, err) + + require.Equal(t, string(content), "test123") + + //require.Equal(t, 1, 2) + }) +} From 33897e568fc6db40c7a1a5ff9d88b08cf443192a Mon Sep 17 00:00:00 2001 From: Bryan Phelps Date: Sat, 5 Feb 2022 00:41:52 +0000 Subject: [PATCH 02/41] Integrate test server too --- nextrouter/nextrouter_test.go | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/nextrouter/nextrouter_test.go b/nextrouter/nextrouter_test.go index e92e6160be43c..8e6c2dbc74eb1 100644 --- a/nextrouter/nextrouter_test.go +++ b/nextrouter/nextrouter_test.go @@ -1,7 +1,10 @@ package nextrouter_test import ( + "context" "io/fs" + "net/http" + "net/http/httptest" "testing" "github.com/psanford/memfs" @@ -14,8 +17,17 @@ func TestConn(t *testing.T) { t.Run("Smoke test", func(t *testing.T) { t.Parallel() + server := httptest.NewServer(nil) + + req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, server.URL, nil) + require.NoError(t, err) + res, err := http.DefaultClient.Do(req) + require.NoError(t, err) + defer res.Body.Close() + require.Equal(t, res.StatusCode, 404) + rootFS := memfs.New() - err := rootFS.MkdirAll("test/a/b", 0777) + err = rootFS.MkdirAll("test/a/b", 0777) require.NoError(t, err) rootFS.WriteFile("test/a/b/c.txt", []byte("test123"), 0755) From b8d092c56fea1c233dc6add0c1e5e8688c0ca913 Mon Sep 17 00:00:00 2001 From: Bryan Phelps Date: Sat, 5 Feb 2022 01:04:35 +0000 Subject: [PATCH 03/41] Some initial test cases for files at root --- nextrouter/nextrouter.go | 32 +++++++++++++++++++++++ nextrouter/nextrouter_test.go | 48 +++++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+) create mode 100644 nextrouter/nextrouter.go diff --git a/nextrouter/nextrouter.go b/nextrouter/nextrouter.go new file mode 100644 index 0000000000000..b447599ede124 --- /dev/null +++ b/nextrouter/nextrouter.go @@ -0,0 +1,32 @@ +package nextrouter + +import ( + "io/fs" + "net/http" + + "github.com/go-chi/chi/v5" +) + +func serve(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("hi")) +} + +// Handler returns an HTTP handler for serving a next-based static site +func Handler(fileSystem fs.FS) (http.Handler, error) { + + rtr := chi.NewRouter() + + files, err := fs.ReadDir(fileSystem, ".") + if err != nil { + return nil, err + } + + rtr.Route("/", func(r chi.Router) { + for _, file := range files { + name := file.Name() + rtr.Get("/"+name, serve) + } + }) + + return rtr, nil +} diff --git a/nextrouter/nextrouter_test.go b/nextrouter/nextrouter_test.go index 8e6c2dbc74eb1..c31f053c65b02 100644 --- a/nextrouter/nextrouter_test.go +++ b/nextrouter/nextrouter_test.go @@ -9,11 +9,59 @@ import ( "github.com/psanford/memfs" "github.com/stretchr/testify/require" + + "github.com/coder/coder/nextrouter" ) +func request(server *httptest.Server, path string) (*http.Response, error) { + req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, server.URL+path, nil) + if err != nil { + return nil, err + } + res, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err + } + return res, err +} + func TestConn(t *testing.T) { t.Parallel() + t.Run("Serves file at root", func(t *testing.T) { + t.Parallel() + + rootFS := memfs.New() + err := rootFS.WriteFile("test.html", []byte("test123"), 0755) + require.NoError(t, err) + + router, err := nextrouter.Handler(rootFS) + require.NoError(t, err) + server := httptest.NewServer(router) + + res, err := request(server, "/test.html") + require.NoError(t, err) + defer res.Body.Close() + require.Equal(t, res.StatusCode, 200) + }) + + t.Run("404 if file at root is not found", func(t *testing.T) { + t.Parallel() + + rootFS := memfs.New() + err := rootFS.WriteFile("test.html", []byte("test123"), 0755) + require.NoError(t, err) + + router, err := nextrouter.Handler(rootFS) + require.NoError(t, err) + server := httptest.NewServer(router) + + res, err := request(server, "/test-non-existent.html") + require.NoError(t, err) + defer res.Body.Close() + require.Equal(t, res.StatusCode, 404) + }) + t.Run("Smoke test", func(t *testing.T) { t.Parallel() From 994926184b6a5180b56dfe0104e372060fe4df62 Mon Sep 17 00:00:00 2001 From: Bryan Phelps Date: Sat, 5 Feb 2022 01:16:38 +0000 Subject: [PATCH 04/41] Serve contents from filesystem --- nextrouter/nextrouter.go | 19 +++++++++++++++---- nextrouter/nextrouter_test.go | 5 +++++ 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/nextrouter/nextrouter.go b/nextrouter/nextrouter.go index b447599ede124..49923523ca703 100644 --- a/nextrouter/nextrouter.go +++ b/nextrouter/nextrouter.go @@ -7,13 +7,24 @@ import ( "github.com/go-chi/chi/v5" ) -func serve(w http.ResponseWriter, r *http.Request) { - w.Write([]byte("hi")) +func serve(fileSystem fs.FS, filePath string) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + + bytes, err := fs.ReadFile(fileSystem, filePath) + + if err != nil { + http.Error(w, http.StatusText(404), 404) + } + _, err = w.Write(bytes) + + if err != nil { + http.Error(w, http.StatusText(404), 404) + } + } } // Handler returns an HTTP handler for serving a next-based static site func Handler(fileSystem fs.FS) (http.Handler, error) { - rtr := chi.NewRouter() files, err := fs.ReadDir(fileSystem, ".") @@ -24,7 +35,7 @@ func Handler(fileSystem fs.FS) (http.Handler, error) { rtr.Route("/", func(r chi.Router) { for _, file := range files { name := file.Name() - rtr.Get("/"+name, serve) + rtr.Get("/"+name, serve(fileSystem, name)) } }) diff --git a/nextrouter/nextrouter_test.go b/nextrouter/nextrouter_test.go index c31f053c65b02..dcb7d47c0cac6 100644 --- a/nextrouter/nextrouter_test.go +++ b/nextrouter/nextrouter_test.go @@ -2,6 +2,7 @@ package nextrouter_test import ( "context" + "io" "io/fs" "net/http" "net/http/httptest" @@ -42,6 +43,10 @@ func TestConn(t *testing.T) { res, err := request(server, "/test.html") require.NoError(t, err) defer res.Body.Close() + + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + require.Equal(t, string(body), "test123") require.Equal(t, res.StatusCode, 200) }) From fadc0b671e3607f9f8d55a0c2ca27cdbe9f7c439 Mon Sep 17 00:00:00 2001 From: Bryan Phelps Date: Sat, 5 Feb 2022 01:20:09 +0000 Subject: [PATCH 05/41] Factor router out --- nextrouter/nextrouter.go | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/nextrouter/nextrouter.go b/nextrouter/nextrouter.go index 49923523ca703..d4a82a535847c 100644 --- a/nextrouter/nextrouter.go +++ b/nextrouter/nextrouter.go @@ -23,21 +23,28 @@ func serve(fileSystem fs.FS, filePath string) http.HandlerFunc { } } -// Handler returns an HTTP handler for serving a next-based static site -func Handler(fileSystem fs.FS) (http.Handler, error) { - rtr := chi.NewRouter() - - files, err := fs.ReadDir(fileSystem, ".") +func buildRouter(parentFileSystem fs.FS, path string) (chi.Router, error) { + fileSystem, err := fs.Sub(parentFileSystem, path) if err != nil { return nil, err } - + files, err := fs.ReadDir(fileSystem, ".") + rtr := chi.NewRouter() rtr.Route("/", func(r chi.Router) { for _, file := range files { name := file.Name() rtr.Get("/"+name, serve(fileSystem, name)) } }) - return rtr, nil } + +// Handler returns an HTTP handler for serving a next-based static site +func Handler(fileSystem fs.FS) (http.Handler, error) { + + router, err := buildRouter(fileSystem, ".") + if err != nil { + return nil, err + } + return router, nil +} From 771f39d35ad1b367338cf92bfb5650e232ed8bf2 Mon Sep 17 00:00:00 2001 From: Bryan Phelps Date: Sat, 5 Feb 2022 01:25:07 +0000 Subject: [PATCH 06/41] Add recursive router building --- nextrouter/nextrouter.go | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/nextrouter/nextrouter.go b/nextrouter/nextrouter.go index d4a82a535847c..54ed32b75815f 100644 --- a/nextrouter/nextrouter.go +++ b/nextrouter/nextrouter.go @@ -23,28 +23,34 @@ func serve(fileSystem fs.FS, filePath string) http.HandlerFunc { } } -func buildRouter(parentFileSystem fs.FS, path string) (chi.Router, error) { - fileSystem, err := fs.Sub(parentFileSystem, path) +func buildRouter(rtr chi.Router, fileSystem fs.FS, path string) { + files, err := fs.ReadDir(fileSystem, ".") if err != nil { - return nil, err + // TODO(Bryan): Log + return } - files, err := fs.ReadDir(fileSystem, ".") - rtr := chi.NewRouter() + rtr.Route("/", func(r chi.Router) { for _, file := range files { name := file.Name() - rtr.Get("/"+name, serve(fileSystem, name)) + + if file.IsDir() { + sub, err := fs.Sub(fileSystem, name) + if err != nil { + // TODO(Bryan): Log + continue + } + buildRouter(r, sub, path+"/"+name) + } else { + rtr.Get("/"+name, serve(fileSystem, name)) + } } }) - return rtr, nil } // Handler returns an HTTP handler for serving a next-based static site func Handler(fileSystem fs.FS) (http.Handler, error) { - - router, err := buildRouter(fileSystem, ".") - if err != nil { - return nil, err - } - return router, nil + rtr := chi.NewRouter() + buildRouter(rtr, fileSystem, "") + return rtr, nil } From 78406b1391a50963bd0b538dbc60ef4e6abf205b Mon Sep 17 00:00:00 2001 From: Bryan Phelps Date: Sat, 5 Feb 2022 01:39:07 +0000 Subject: [PATCH 07/41] Hook up recursive case --- nextrouter/nextrouter.go | 33 +++++++++++++++++---------------- nextrouter/nextrouter_test.go | 26 +++++++++++++++++++++++++- 2 files changed, 42 insertions(+), 17 deletions(-) diff --git a/nextrouter/nextrouter.go b/nextrouter/nextrouter.go index 54ed32b75815f..d7cce2222a9af 100644 --- a/nextrouter/nextrouter.go +++ b/nextrouter/nextrouter.go @@ -1,6 +1,7 @@ package nextrouter import ( + "fmt" "io/fs" "net/http" @@ -9,7 +10,7 @@ import ( func serve(fileSystem fs.FS, filePath string) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - + fmt.Println("Requesting file: " + filePath) bytes, err := fs.ReadFile(fileSystem, filePath) if err != nil { @@ -23,29 +24,29 @@ func serve(fileSystem fs.FS, filePath string) http.HandlerFunc { } } -func buildRouter(rtr chi.Router, fileSystem fs.FS, path string) { +func buildRouter(rtr chi.Router, fileSystem fs.FS, name string) { files, err := fs.ReadDir(fileSystem, ".") if err != nil { // TODO(Bryan): Log return } - rtr.Route("/", func(r chi.Router) { - for _, file := range files { - name := file.Name() - - if file.IsDir() { - sub, err := fs.Sub(fileSystem, name) - if err != nil { - // TODO(Bryan): Log - continue - } - buildRouter(r, sub, path+"/"+name) - } else { - rtr.Get("/"+name, serve(fileSystem, name)) + for _, file := range files { + name := file.Name() + + if file.IsDir() { + sub, err := fs.Sub(fileSystem, name) + if err != nil { + // TODO(Bryan): Log + continue } + rtr.Route("/"+name, func(r chi.Router) { + buildRouter(r, sub, name) + }) + } else { + rtr.Get("/"+name, serve(fileSystem, name)) } - }) + } } // Handler returns an HTTP handler for serving a next-based static site diff --git a/nextrouter/nextrouter_test.go b/nextrouter/nextrouter_test.go index dcb7d47c0cac6..103c6702dcf70 100644 --- a/nextrouter/nextrouter_test.go +++ b/nextrouter/nextrouter_test.go @@ -26,7 +26,7 @@ func request(server *httptest.Server, path string) (*http.Response, error) { return res, err } -func TestConn(t *testing.T) { +func TestNextRouter(t *testing.T) { t.Parallel() t.Run("Serves file at root", func(t *testing.T) { @@ -50,6 +50,30 @@ func TestConn(t *testing.T) { require.Equal(t, res.StatusCode, 200) }) + t.Run("Serves nested file", func(t *testing.T) { + t.Parallel() + + rootFS := memfs.New() + err := rootFS.MkdirAll("test/a/b", 0777) + require.NoError(t, err) + + rootFS.WriteFile("test/a/b/c.html", []byte("test123"), 0755) + require.NoError(t, err) + + router, err := nextrouter.Handler(rootFS) + require.NoError(t, err) + server := httptest.NewServer(router) + + res, err := request(server, "/test/a/b/c.html") + require.NoError(t, err) + defer res.Body.Close() + + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + require.Equal(t, string(body), "test123") + require.Equal(t, res.StatusCode, 200) + }) + t.Run("404 if file at root is not found", func(t *testing.T) { t.Parallel() From 50c72ba7b84e371363a7cd5efc483ee2387281ed Mon Sep 17 00:00:00 2001 From: Bryan Phelps Date: Sat, 5 Feb 2022 02:17:28 +0000 Subject: [PATCH 08/41] Refactor to handle different file-serving cases --- nextrouter/nextrouter.go | 15 ++++++++++----- nextrouter/nextrouter_test.go | 4 ++++ 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/nextrouter/nextrouter.go b/nextrouter/nextrouter.go index d7cce2222a9af..16957b18b8373 100644 --- a/nextrouter/nextrouter.go +++ b/nextrouter/nextrouter.go @@ -8,10 +8,11 @@ import ( "github.com/go-chi/chi/v5" ) -func serve(fileSystem fs.FS, filePath string) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - fmt.Println("Requesting file: " + filePath) - bytes, err := fs.ReadFile(fileSystem, filePath) +func serveFile(router chi.Router, fileSystem fs.FS, fileName string) { + + handler := func(w http.ResponseWriter, r *http.Request) { + fmt.Println("Requesting file: " + fileName) + bytes, err := fs.ReadFile(fileSystem, fileName) if err != nil { http.Error(w, http.StatusText(404), 404) @@ -22,6 +23,8 @@ func serve(fileSystem fs.FS, filePath string) http.HandlerFunc { http.Error(w, http.StatusText(404), 404) } } + + router.Get("/"+fileName, handler) } func buildRouter(rtr chi.Router, fileSystem fs.FS, name string) { @@ -31,6 +34,7 @@ func buildRouter(rtr chi.Router, fileSystem fs.FS, name string) { return } + fmt.Println("Recursing: " + name) for _, file := range files { name := file.Name() @@ -44,7 +48,8 @@ func buildRouter(rtr chi.Router, fileSystem fs.FS, name string) { buildRouter(r, sub, name) }) } else { - rtr.Get("/"+name, serve(fileSystem, name)) + serveFile(rtr, fileSystem, name) + } } } diff --git a/nextrouter/nextrouter_test.go b/nextrouter/nextrouter_test.go index 103c6702dcf70..11f4d35c30ca0 100644 --- a/nextrouter/nextrouter_test.go +++ b/nextrouter/nextrouter_test.go @@ -68,6 +68,10 @@ func TestNextRouter(t *testing.T) { require.NoError(t, err) defer res.Body.Close() + res, err = request(server, "/test/a/b/c.html") + require.NoError(t, err) + defer res.Body.Close() + body, err := io.ReadAll(res.Body) require.NoError(t, err) require.Equal(t, string(body), "test123") From 7a98a483a2159b033c203579abc005b700a224b5 Mon Sep 17 00:00:00 2001 From: Bryan Phelps Date: Sat, 5 Feb 2022 02:21:49 +0000 Subject: [PATCH 09/41] Handle case w/o html extension --- nextrouter/nextrouter.go | 60 ++++++++++++++++++++++------------- nextrouter/nextrouter_test.go | 21 +++++++++++- 2 files changed, 58 insertions(+), 23 deletions(-) diff --git a/nextrouter/nextrouter.go b/nextrouter/nextrouter.go index 16957b18b8373..5ec664a97d41f 100644 --- a/nextrouter/nextrouter.go +++ b/nextrouter/nextrouter.go @@ -4,27 +4,17 @@ import ( "fmt" "io/fs" "net/http" + "path/filepath" + "strings" "github.com/go-chi/chi/v5" ) -func serveFile(router chi.Router, fileSystem fs.FS, fileName string) { - - handler := func(w http.ResponseWriter, r *http.Request) { - fmt.Println("Requesting file: " + fileName) - bytes, err := fs.ReadFile(fileSystem, fileName) - - if err != nil { - http.Error(w, http.StatusText(404), 404) - } - _, err = w.Write(bytes) - - if err != nil { - http.Error(w, http.StatusText(404), 404) - } - } - - router.Get("/"+fileName, handler) +// Handler returns an HTTP handler for serving a next-based static site +func Handler(fileSystem fs.FS) (http.Handler, error) { + rtr := chi.NewRouter() + buildRouter(rtr, fileSystem, "") + return rtr, nil } func buildRouter(rtr chi.Router, fileSystem fs.FS, name string) { @@ -54,9 +44,35 @@ func buildRouter(rtr chi.Router, fileSystem fs.FS, name string) { } } -// Handler returns an HTTP handler for serving a next-based static site -func Handler(fileSystem fs.FS) (http.Handler, error) { - rtr := chi.NewRouter() - buildRouter(rtr, fileSystem, "") - return rtr, nil +func serveFile(router chi.Router, fileSystem fs.FS, fileName string) { + + // We only handle .html files for now + ext := filepath.Ext(fileName) + if ext != ".html" { + return + } + + fmt.Println("Requesting file: " + fileName) + bytes, err := fs.ReadFile(fileSystem, fileName) + + handler := func(w http.ResponseWriter, r *http.Request) { + + if err != nil { + http.Error(w, http.StatusText(404), 404) + } + _, err = w.Write(bytes) + + if err != nil { + http.Error(w, http.StatusText(404), 404) + } + } + + fileNameWithoutExtension := removeFileExtension(fileName) + + router.Get("/"+fileName, handler) + router.Get("/"+fileNameWithoutExtension, handler) +} + +func removeFileExtension(fileName string) string { + return strings.TrimSuffix(fileName, filepath.Ext(fileName)) } diff --git a/nextrouter/nextrouter_test.go b/nextrouter/nextrouter_test.go index 11f4d35c30ca0..1b38d725e99b6 100644 --- a/nextrouter/nextrouter_test.go +++ b/nextrouter/nextrouter_test.go @@ -31,7 +31,6 @@ func TestNextRouter(t *testing.T) { t.Run("Serves file at root", func(t *testing.T) { t.Parallel() - rootFS := memfs.New() err := rootFS.WriteFile("test.html", []byte("test123"), 0755) require.NoError(t, err) @@ -50,6 +49,26 @@ func TestNextRouter(t *testing.T) { require.Equal(t, res.StatusCode, 200) }) + t.Run("Serves html file without extension", func(t *testing.T) { + t.Parallel() + rootFS := memfs.New() + err := rootFS.WriteFile("test.html", []byte("test-no-extension"), 0755) + require.NoError(t, err) + + router, err := nextrouter.Handler(rootFS) + require.NoError(t, err) + server := httptest.NewServer(router) + + res, err := request(server, "/test") + require.NoError(t, err) + defer res.Body.Close() + + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + require.Equal(t, string(body), "test-no-extension") + require.Equal(t, res.StatusCode, 200) + }) + t.Run("Serves nested file", func(t *testing.T) { t.Parallel() From 1eb24024630136cd107819e0bcf84ca5d2ff8024 Mon Sep 17 00:00:00 2001 From: Bryan Phelps Date: Sat, 5 Feb 2022 02:23:20 +0000 Subject: [PATCH 10/41] Handle redirecting to index.html at root --- nextrouter/nextrouter.go | 5 +++++ nextrouter/nextrouter_test.go | 20 ++++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/nextrouter/nextrouter.go b/nextrouter/nextrouter.go index 5ec664a97d41f..0daf6c5497b30 100644 --- a/nextrouter/nextrouter.go +++ b/nextrouter/nextrouter.go @@ -71,6 +71,11 @@ func serveFile(router chi.Router, fileSystem fs.FS, fileName string) { router.Get("/"+fileName, handler) router.Get("/"+fileNameWithoutExtension, handler) + + // Special case: '/' should serve index.html + if fileName == "index.html" { + router.Get("/", handler) + } } func removeFileExtension(fileName string) string { diff --git a/nextrouter/nextrouter_test.go b/nextrouter/nextrouter_test.go index 1b38d725e99b6..e325741ef2c26 100644 --- a/nextrouter/nextrouter_test.go +++ b/nextrouter/nextrouter_test.go @@ -69,6 +69,26 @@ func TestNextRouter(t *testing.T) { require.Equal(t, res.StatusCode, 200) }) + t.Run("Defaults to index.html at root", func(t *testing.T) { + t.Parallel() + rootFS := memfs.New() + err := rootFS.WriteFile("index.html", []byte("test-root-index"), 0755) + require.NoError(t, err) + + router, err := nextrouter.Handler(rootFS) + require.NoError(t, err) + server := httptest.NewServer(router) + + res, err := request(server, "/") + require.NoError(t, err) + defer res.Body.Close() + + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + require.Equal(t, string(body), "test-root-index") + require.Equal(t, res.StatusCode, 200) + }) + t.Run("Serves nested file", func(t *testing.T) { t.Parallel() From b0fd9a8c5b96ad8053c1d31a928b2e22a413f665 Mon Sep 17 00:00:00 2001 From: Bryan Phelps Date: Sat, 5 Feb 2022 02:24:20 +0000 Subject: [PATCH 11/41] Add test case for nested path --- nextrouter/nextrouter_test.go | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/nextrouter/nextrouter_test.go b/nextrouter/nextrouter_test.go index e325741ef2c26..8a796959b419b 100644 --- a/nextrouter/nextrouter_test.go +++ b/nextrouter/nextrouter_test.go @@ -117,6 +117,30 @@ func TestNextRouter(t *testing.T) { require.Equal(t, res.StatusCode, 200) }) + t.Run("Uses index.html in nested path", func(t *testing.T) { + t.Parallel() + + rootFS := memfs.New() + err := rootFS.MkdirAll("test/a/b/c", 0777) + require.NoError(t, err) + + rootFS.WriteFile("test/a/b/c/index.html", []byte("test-abc-index"), 0755) + require.NoError(t, err) + + router, err := nextrouter.Handler(rootFS) + require.NoError(t, err) + server := httptest.NewServer(router) + + res, err := request(server, "/test/a/b/c") + require.NoError(t, err) + defer res.Body.Close() + + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + require.Equal(t, string(body), "test-abc-index") + require.Equal(t, res.StatusCode, 200) + }) + t.Run("404 if file at root is not found", func(t *testing.T) { t.Parallel() From a5061480b38e2ba8260fe5bf5049c035b16add3d Mon Sep 17 00:00:00 2001 From: Bryan Phelps Date: Sat, 5 Feb 2022 02:24:54 +0000 Subject: [PATCH 12/41] Remove now-unnecessary smoke test --- nextrouter/nextrouter_test.go | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/nextrouter/nextrouter_test.go b/nextrouter/nextrouter_test.go index 8a796959b419b..b8d55441003b4 100644 --- a/nextrouter/nextrouter_test.go +++ b/nextrouter/nextrouter_test.go @@ -157,29 +157,4 @@ func TestNextRouter(t *testing.T) { defer res.Body.Close() require.Equal(t, res.StatusCode, 404) }) - - t.Run("Smoke test", func(t *testing.T) { - t.Parallel() - - server := httptest.NewServer(nil) - - req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, server.URL, nil) - require.NoError(t, err) - res, err := http.DefaultClient.Do(req) - require.NoError(t, err) - defer res.Body.Close() - require.Equal(t, res.StatusCode, 404) - - rootFS := memfs.New() - err = rootFS.MkdirAll("test/a/b", 0777) - require.NoError(t, err) - - rootFS.WriteFile("test/a/b/c.txt", []byte("test123"), 0755) - content, err := fs.ReadFile(rootFS, "test/a/b/c.txt") - require.NoError(t, err) - - require.Equal(t, string(content), "test123") - - //require.Equal(t, 1, 2) - }) } From b59e2a8c7e4398ec42ba5de243069785ceb5b294 Mon Sep 17 00:00:00 2001 From: Bryan Phelps Date: Sat, 5 Feb 2022 02:26:16 +0000 Subject: [PATCH 13/41] Some clean up --- nextrouter/nextrouter.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/nextrouter/nextrouter.go b/nextrouter/nextrouter.go index 0daf6c5497b30..dd951674e3c7b 100644 --- a/nextrouter/nextrouter.go +++ b/nextrouter/nextrouter.go @@ -52,18 +52,18 @@ func serveFile(router chi.Router, fileSystem fs.FS, fileName string) { return } - fmt.Println("Requesting file: " + fileName) bytes, err := fs.ReadFile(fileSystem, fileName) + if err != nil { + // TODO(Bryan): Log here + return + } handler := func(w http.ResponseWriter, r *http.Request) { - - if err != nil { - http.Error(w, http.StatusText(404), 404) - } _, err = w.Write(bytes) if err != nil { - http.Error(w, http.StatusText(404), 404) + // TODO(BRYAN): Log here + http.Error(w, http.StatusText(500), 500) } } From ff75695cfc62b422aa025b106a444756b27a7db5 Mon Sep 17 00:00:00 2001 From: Bryan Phelps Date: Sat, 5 Feb 2022 02:28:49 +0000 Subject: [PATCH 14/41] Refactor to use http.ServeContent --- nextrouter/nextrouter.go | 12 ++++-------- nextrouter/nextrouter_test.go | 1 - 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/nextrouter/nextrouter.go b/nextrouter/nextrouter.go index dd951674e3c7b..2475077174790 100644 --- a/nextrouter/nextrouter.go +++ b/nextrouter/nextrouter.go @@ -1,11 +1,13 @@ package nextrouter import ( + "bytes" "fmt" "io/fs" "net/http" "path/filepath" "strings" + "time" "github.com/go-chi/chi/v5" ) @@ -39,7 +41,6 @@ func buildRouter(rtr chi.Router, fileSystem fs.FS, name string) { }) } else { serveFile(rtr, fileSystem, name) - } } } @@ -52,19 +53,14 @@ func serveFile(router chi.Router, fileSystem fs.FS, fileName string) { return } - bytes, err := fs.ReadFile(fileSystem, fileName) + data, err := fs.ReadFile(fileSystem, fileName) if err != nil { // TODO(Bryan): Log here return } handler := func(w http.ResponseWriter, r *http.Request) { - _, err = w.Write(bytes) - - if err != nil { - // TODO(BRYAN): Log here - http.Error(w, http.StatusText(500), 500) - } + http.ServeContent(w, r, fileName, time.Time{}, bytes.NewReader(data)) } fileNameWithoutExtension := removeFileExtension(fileName) diff --git a/nextrouter/nextrouter_test.go b/nextrouter/nextrouter_test.go index b8d55441003b4..ade55ac22d478 100644 --- a/nextrouter/nextrouter_test.go +++ b/nextrouter/nextrouter_test.go @@ -3,7 +3,6 @@ package nextrouter_test import ( "context" "io" - "io/fs" "net/http" "net/http/httptest" "testing" From a410396537195f16bf40e73d2d34ef4d7f3c6314 Mon Sep 17 00:00:00 2001 From: Bryan Phelps Date: Sat, 5 Feb 2022 02:32:22 +0000 Subject: [PATCH 15/41] Simplify router construction --- nextrouter/nextrouter.go | 4 ++-- nextrouter/nextrouter_test.go | 19 +++++++------------ 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/nextrouter/nextrouter.go b/nextrouter/nextrouter.go index 2475077174790..b21b8e8ab107e 100644 --- a/nextrouter/nextrouter.go +++ b/nextrouter/nextrouter.go @@ -13,10 +13,10 @@ import ( ) // Handler returns an HTTP handler for serving a next-based static site -func Handler(fileSystem fs.FS) (http.Handler, error) { +func Handler(fileSystem fs.FS) http.Handler { rtr := chi.NewRouter() buildRouter(rtr, fileSystem, "") - return rtr, nil + return rtr } func buildRouter(rtr chi.Router, fileSystem fs.FS, name string) { diff --git a/nextrouter/nextrouter_test.go b/nextrouter/nextrouter_test.go index ade55ac22d478..dc82623c36d92 100644 --- a/nextrouter/nextrouter_test.go +++ b/nextrouter/nextrouter_test.go @@ -34,8 +34,7 @@ func TestNextRouter(t *testing.T) { err := rootFS.WriteFile("test.html", []byte("test123"), 0755) require.NoError(t, err) - router, err := nextrouter.Handler(rootFS) - require.NoError(t, err) + router := nextrouter.Handler(rootFS) server := httptest.NewServer(router) res, err := request(server, "/test.html") @@ -54,8 +53,7 @@ func TestNextRouter(t *testing.T) { err := rootFS.WriteFile("test.html", []byte("test-no-extension"), 0755) require.NoError(t, err) - router, err := nextrouter.Handler(rootFS) - require.NoError(t, err) + router := nextrouter.Handler(rootFS) server := httptest.NewServer(router) res, err := request(server, "/test") @@ -74,8 +72,7 @@ func TestNextRouter(t *testing.T) { err := rootFS.WriteFile("index.html", []byte("test-root-index"), 0755) require.NoError(t, err) - router, err := nextrouter.Handler(rootFS) - require.NoError(t, err) + router := nextrouter.Handler(rootFS) server := httptest.NewServer(router) res, err := request(server, "/") @@ -98,8 +95,7 @@ func TestNextRouter(t *testing.T) { rootFS.WriteFile("test/a/b/c.html", []byte("test123"), 0755) require.NoError(t, err) - router, err := nextrouter.Handler(rootFS) - require.NoError(t, err) + router := nextrouter.Handler(rootFS) server := httptest.NewServer(router) res, err := request(server, "/test/a/b/c.html") @@ -126,8 +122,8 @@ func TestNextRouter(t *testing.T) { rootFS.WriteFile("test/a/b/c/index.html", []byte("test-abc-index"), 0755) require.NoError(t, err) - router, err := nextrouter.Handler(rootFS) - require.NoError(t, err) + router := nextrouter.Handler(rootFS) + server := httptest.NewServer(router) res, err := request(server, "/test/a/b/c") @@ -147,8 +143,7 @@ func TestNextRouter(t *testing.T) { err := rootFS.WriteFile("test.html", []byte("test123"), 0755) require.NoError(t, err) - router, err := nextrouter.Handler(rootFS) - require.NoError(t, err) + router := nextrouter.Handler(rootFS) server := httptest.NewServer(router) res, err := request(server, "/test-non-existent.html") From 15c38b960de5c409012600d16c407e3006c655b7 Mon Sep 17 00:00:00 2001 From: Bryan Phelps Date: Sat, 5 Feb 2022 02:32:33 +0000 Subject: [PATCH 16/41] rtr -> router --- nextrouter/nextrouter.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nextrouter/nextrouter.go b/nextrouter/nextrouter.go index b21b8e8ab107e..e3de2f6531bd4 100644 --- a/nextrouter/nextrouter.go +++ b/nextrouter/nextrouter.go @@ -14,9 +14,9 @@ import ( // Handler returns an HTTP handler for serving a next-based static site func Handler(fileSystem fs.FS) http.Handler { - rtr := chi.NewRouter() - buildRouter(rtr, fileSystem, "") - return rtr + router := chi.NewRouter() + buildRouter(router, fileSystem, "") + return router } func buildRouter(rtr chi.Router, fileSystem fs.FS, name string) { From c813b5e84f19f99684795172ff012d7c5302fbc5 Mon Sep 17 00:00:00 2001 From: Bryan Phelps Date: Sat, 5 Feb 2022 02:39:07 +0000 Subject: [PATCH 17/41] Handle serving non-html files --- nextrouter/nextrouter.go | 4 ++++ nextrouter/nextrouter_test.go | 21 +++++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/nextrouter/nextrouter.go b/nextrouter/nextrouter.go index e3de2f6531bd4..2637d3e21aad0 100644 --- a/nextrouter/nextrouter.go +++ b/nextrouter/nextrouter.go @@ -16,6 +16,10 @@ import ( func Handler(fileSystem fs.FS) http.Handler { router := chi.NewRouter() buildRouter(router, fileSystem, "") + + // Fallback to static file server for non-html files + fileHandler := http.FileServer(http.FS(fileSystem)) + router.NotFound(fileHandler.ServeHTTP) return router } diff --git a/nextrouter/nextrouter_test.go b/nextrouter/nextrouter_test.go index dc82623c36d92..68eb9d9b7c42c 100644 --- a/nextrouter/nextrouter_test.go +++ b/nextrouter/nextrouter_test.go @@ -47,6 +47,26 @@ func TestNextRouter(t *testing.T) { require.Equal(t, res.StatusCode, 200) }) + t.Run("Serves non-html files at root", func(t *testing.T) { + t.Parallel() + rootFS := memfs.New() + err := rootFS.WriteFile("test.png", []byte("png-bytes"), 0755) + require.NoError(t, err) + + router := nextrouter.Handler(rootFS) + server := httptest.NewServer(router) + + res, err := request(server, "/test.png") + require.NoError(t, err) + defer res.Body.Close() + + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + require.Equal(t, res.Header.Get("Content-Type"), "image/png") + require.Equal(t, string(body), "png-bytes") + require.Equal(t, res.StatusCode, 200) + }) + t.Run("Serves html file without extension", func(t *testing.T) { t.Parallel() rootFS := memfs.New() @@ -81,6 +101,7 @@ func TestNextRouter(t *testing.T) { body, err := io.ReadAll(res.Body) require.NoError(t, err) + require.Equal(t, res.Header.Get("Content-Type"), "text/html; charset=utf-8") require.Equal(t, string(body), "test-root-index") require.Equal(t, res.StatusCode, 200) }) From 317c0409c588cb18fbe4437adb7827fdd6766492 Mon Sep 17 00:00:00 2001 From: Bryan Phelps Date: Sat, 5 Feb 2022 02:52:19 +0000 Subject: [PATCH 18/41] Handle trailing-slash case --- nextrouter/nextrouter.go | 4 ++++ nextrouter/nextrouter_test.go | 22 ++++++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/nextrouter/nextrouter.go b/nextrouter/nextrouter.go index 2637d3e21aad0..527bc5028795d 100644 --- a/nextrouter/nextrouter.go +++ b/nextrouter/nextrouter.go @@ -75,6 +75,10 @@ func serveFile(router chi.Router, fileSystem fs.FS, fileName string) { // Special case: '/' should serve index.html if fileName == "index.html" { router.Get("/", handler) + } else { + // Otherwise, handling the trailing slash case - + // for examples, `providers.html` should serve `/providers/` + router.Get("/"+fileNameWithoutExtension+"/", handler) } } diff --git a/nextrouter/nextrouter_test.go b/nextrouter/nextrouter_test.go index 68eb9d9b7c42c..9e80642412edb 100644 --- a/nextrouter/nextrouter_test.go +++ b/nextrouter/nextrouter_test.go @@ -47,6 +47,28 @@ func TestNextRouter(t *testing.T) { require.Equal(t, res.StatusCode, 200) }) + // This is a test case for the issue we hit in V1 w/ NextJS migration + t.Run("Prefer file over folder w/ trailing slash", func(t *testing.T) { + t.Parallel() + rootFS := memfs.New() + err := rootFS.MkdirAll("folder", 0777) + require.NoError(t, err) + err = rootFS.WriteFile("folder.html", []byte("folderFile"), 0755) + require.NoError(t, err) + + router := nextrouter.Handler(rootFS) + server := httptest.NewServer(router) + + res, err := request(server, "/folder/") + require.NoError(t, err) + defer res.Body.Close() + + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + require.Equal(t, string(body), "folderFile") + require.Equal(t, res.StatusCode, 200) + }) + t.Run("Serves non-html files at root", func(t *testing.T) { t.Parallel() rootFS := memfs.New() From 133e452c04d3aae4e66e7e1da43d070172bcf4b8 Mon Sep 17 00:00:00 2001 From: Bryan Phelps Date: Sat, 5 Feb 2022 03:04:29 +0000 Subject: [PATCH 19/41] Implement dynamic routing --- nextrouter/nextrouter.go | 31 +++++++++++++++++++++++++++++-- nextrouter/nextrouter_test.go | 21 +++++++++++++++++++++ 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/nextrouter/nextrouter.go b/nextrouter/nextrouter.go index 527bc5028795d..9b16dccb2ce7a 100644 --- a/nextrouter/nextrouter.go +++ b/nextrouter/nextrouter.go @@ -13,13 +13,23 @@ import ( ) // Handler returns an HTTP handler for serving a next-based static site +// This handler respects NextJS-based routing rules: +// https://nextjs.org/docs/routing/dynamic-routes +// +// 1) If a file is of the form `[org]`, it's a dynamic route for a single-parameter +// 2) If a file is of the form `[[...any]]`, it's a dynamic route for any parameters func Handler(fileSystem fs.FS) http.Handler { router := chi.NewRouter() + + // Build up a router that matches NextJS routing rules, for HTML files buildRouter(router, fileSystem, "") - // Fallback to static file server for non-html files + // Fallback to static file server for non-HTML files + // Non-HTML files don't have special routing rules, so we can just leverage + // the built-in http.FileServer for it. fileHandler := http.FileServer(http.FS(fileSystem)) router.NotFound(fileHandler.ServeHTTP) + return router } @@ -50,7 +60,6 @@ func buildRouter(rtr chi.Router, fileSystem fs.FS, name string) { } func serveFile(router chi.Router, fileSystem fs.FS, fileName string) { - // We only handle .html files for now ext := filepath.Ext(fileName) if ext != ".html" { @@ -69,6 +78,12 @@ func serveFile(router chi.Router, fileSystem fs.FS, fileName string) { fileNameWithoutExtension := removeFileExtension(fileName) + // Handle the `[org]` dynamic route case + if isDynamicRoute(fileName) { + router.Get("/{dynamic}", handler) + return + } + router.Get("/"+fileName, handler) router.Get("/"+fileNameWithoutExtension, handler) @@ -82,6 +97,18 @@ func serveFile(router chi.Router, fileSystem fs.FS, fileName string) { } } +func isDynamicRoute(fileName string) bool { + fileWithoutExtension := removeFileExtension(fileName) + + // Assuming ASCII encoding - `len` in go works on bytes + byteLen := len(fileWithoutExtension) + if byteLen < 2 { + return false + } + + return fileWithoutExtension[0] == '[' && fileWithoutExtension[1] != '[' && fileWithoutExtension[byteLen-1] == ']' +} + func removeFileExtension(fileName string) string { return strings.TrimSuffix(fileName, filepath.Ext(fileName)) } diff --git a/nextrouter/nextrouter_test.go b/nextrouter/nextrouter_test.go index 9e80642412edb..b5baf995b9863 100644 --- a/nextrouter/nextrouter_test.go +++ b/nextrouter/nextrouter_test.go @@ -194,4 +194,25 @@ func TestNextRouter(t *testing.T) { defer res.Body.Close() require.Equal(t, res.StatusCode, 404) }) + + t.Run("Serves dynamic-routed file", func(t *testing.T) { + t.Parallel() + rootFS := memfs.New() + err := rootFS.MkdirAll("folder", 0777) + require.NoError(t, err) + err = rootFS.WriteFile("folder/[orgs].html", []byte("test-dynamic-path"), 0755) + require.NoError(t, err) + + router := nextrouter.Handler(rootFS) + server := httptest.NewServer(router) + + res, err := request(server, "/folder/org-1") + require.NoError(t, err) + defer res.Body.Close() + + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + require.Equal(t, string(body), "test-dynamic-path") + require.Equal(t, res.StatusCode, 200) + }) } From 6d8f412c14f57d43017c745e7e9514f5dcc53969 Mon Sep 17 00:00:00 2001 From: Bryan Phelps Date: Sat, 5 Feb 2022 03:06:18 +0000 Subject: [PATCH 20/41] Add test case to verify static paths are preferred over dynamic paths --- nextrouter/nextrouter_test.go | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/nextrouter/nextrouter_test.go b/nextrouter/nextrouter_test.go index b5baf995b9863..9668cb0967db4 100644 --- a/nextrouter/nextrouter_test.go +++ b/nextrouter/nextrouter_test.go @@ -215,4 +215,27 @@ func TestNextRouter(t *testing.T) { require.Equal(t, string(body), "test-dynamic-path") require.Equal(t, res.StatusCode, 200) }) + + t.Run("Static routes should be preferred to dynamic routes", func(t *testing.T) { + t.Parallel() + rootFS := memfs.New() + err := rootFS.MkdirAll("folder", 0777) + require.NoError(t, err) + err = rootFS.WriteFile("folder/[orgs].html", []byte("test-dynamic-path"), 0755) + require.NoError(t, err) + err = rootFS.WriteFile("folder/create.html", []byte("test-create"), 0755) + require.NoError(t, err) + + router := nextrouter.Handler(rootFS) + server := httptest.NewServer(router) + + res, err := request(server, "/folder/create") + require.NoError(t, err) + defer res.Body.Close() + + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + require.Equal(t, string(body), "test-create") + require.Equal(t, res.StatusCode, 200) + }) } From a3ecd7f2b7f2454f9af95b246d853a87d8b9caea Mon Sep 17 00:00:00 2001 From: Bryan Phelps Date: Sat, 5 Feb 2022 03:09:01 +0000 Subject: [PATCH 21/41] Handle dynamic routing for folders --- nextrouter/nextrouter.go | 8 +++++++- nextrouter/nextrouter_test.go | 21 +++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/nextrouter/nextrouter.go b/nextrouter/nextrouter.go index 9b16dccb2ce7a..be417fcb3b3dc 100644 --- a/nextrouter/nextrouter.go +++ b/nextrouter/nextrouter.go @@ -50,7 +50,13 @@ func buildRouter(rtr chi.Router, fileSystem fs.FS, name string) { // TODO(Bryan): Log continue } - rtr.Route("/"+name, func(r chi.Router) { + routeName := name + + if isDynamicRoute(name) { + routeName = "{dynamic}" + } + + rtr.Route("/"+routeName, func(r chi.Router) { buildRouter(r, sub, name) }) } else { diff --git a/nextrouter/nextrouter_test.go b/nextrouter/nextrouter_test.go index 9668cb0967db4..3b9894af8cc4e 100644 --- a/nextrouter/nextrouter_test.go +++ b/nextrouter/nextrouter_test.go @@ -216,6 +216,27 @@ func TestNextRouter(t *testing.T) { require.Equal(t, res.StatusCode, 200) }) + t.Run("Handles dynamic-routed folders", func(t *testing.T) { + t.Parallel() + rootFS := memfs.New() + err := rootFS.MkdirAll("folder/[org]/[project]", 0777) + require.NoError(t, err) + err = rootFS.WriteFile("folder/[org]/[project]/create.html", []byte("test-create"), 0755) + require.NoError(t, err) + + router := nextrouter.Handler(rootFS) + server := httptest.NewServer(router) + + res, err := request(server, "/folder/org-1/project-1/create") + require.NoError(t, err) + defer res.Body.Close() + + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + require.Equal(t, string(body), "test-create") + require.Equal(t, res.StatusCode, 200) + }) + t.Run("Static routes should be preferred to dynamic routes", func(t *testing.T) { t.Parallel() rootFS := memfs.New() From 700c9ef50be4bc4f8702629eb2b667b6550c9612 Mon Sep 17 00:00:00 2001 From: Bryan Phelps Date: Sat, 5 Feb 2022 04:08:20 +0000 Subject: [PATCH 22/41] Handle catch-all routes --- nextrouter/nextrouter.go | 21 +++++++++++++++------ nextrouter/nextrouter_test.go | 21 +++++++++++++++++++++ 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/nextrouter/nextrouter.go b/nextrouter/nextrouter.go index be417fcb3b3dc..7fbffc8d03f54 100644 --- a/nextrouter/nextrouter.go +++ b/nextrouter/nextrouter.go @@ -2,7 +2,6 @@ package nextrouter import ( "bytes" - "fmt" "io/fs" "net/http" "path/filepath" @@ -22,7 +21,7 @@ func Handler(fileSystem fs.FS) http.Handler { router := chi.NewRouter() // Build up a router that matches NextJS routing rules, for HTML files - buildRouter(router, fileSystem, "") + buildRouter(router, fileSystem) // Fallback to static file server for non-HTML files // Non-HTML files don't have special routing rules, so we can just leverage @@ -33,14 +32,13 @@ func Handler(fileSystem fs.FS) http.Handler { return router } -func buildRouter(rtr chi.Router, fileSystem fs.FS, name string) { +func buildRouter(rtr chi.Router, fileSystem fs.FS) { files, err := fs.ReadDir(fileSystem, ".") if err != nil { // TODO(Bryan): Log return } - fmt.Println("Recursing: " + name) for _, file := range files { name := file.Name() @@ -57,7 +55,7 @@ func buildRouter(rtr chi.Router, fileSystem fs.FS, name string) { } rtr.Route("/"+routeName, func(r chi.Router) { - buildRouter(r, sub, name) + buildRouter(r, sub) }) } else { serveFile(rtr, fileSystem, name) @@ -84,8 +82,13 @@ func serveFile(router chi.Router, fileSystem fs.FS, fileName string) { fileNameWithoutExtension := removeFileExtension(fileName) + if isCatchAllRoute(fileNameWithoutExtension) { + router.NotFound(handler) + return + } + // Handle the `[org]` dynamic route case - if isDynamicRoute(fileName) { + if isDynamicRoute(fileNameWithoutExtension) { router.Get("/{dynamic}", handler) return } @@ -115,6 +118,12 @@ func isDynamicRoute(fileName string) bool { return fileWithoutExtension[0] == '[' && fileWithoutExtension[1] != '[' && fileWithoutExtension[byteLen-1] == ']' } +func isCatchAllRoute(fileName string) bool { + fileWithoutExtension := removeFileExtension(fileName) + ret := strings.HasPrefix(fileWithoutExtension, "[[.") + return ret +} + func removeFileExtension(fileName string) string { return strings.TrimSuffix(fileName, filepath.Ext(fileName)) } diff --git a/nextrouter/nextrouter_test.go b/nextrouter/nextrouter_test.go index 3b9894af8cc4e..9ff4009d4fc23 100644 --- a/nextrouter/nextrouter_test.go +++ b/nextrouter/nextrouter_test.go @@ -237,6 +237,27 @@ func TestNextRouter(t *testing.T) { require.Equal(t, res.StatusCode, 200) }) + t.Run("Handles catch-all routes", func(t *testing.T) { + t.Parallel() + rootFS := memfs.New() + err := rootFS.MkdirAll("folder", 0777) + require.NoError(t, err) + err = rootFS.WriteFile("folder/[[...any]].html", []byte("test-catch-all"), 0755) + require.NoError(t, err) + + router := nextrouter.Handler(rootFS) + server := httptest.NewServer(router) + + res, err := request(server, "/folder/org-1/project-1/random") + require.NoError(t, err) + defer res.Body.Close() + + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + require.Equal(t, string(body), "test-catch-all") + require.Equal(t, res.StatusCode, 200) + }) + t.Run("Static routes should be preferred to dynamic routes", func(t *testing.T) { t.Parallel() rootFS := memfs.New() From 05a369ebef5824697fbac3dacb94bb0ff6a8827a Mon Sep 17 00:00:00 2001 From: Bryan Phelps Date: Sat, 5 Feb 2022 04:19:53 +0000 Subject: [PATCH 23/41] Start plumbing in a way to inject template parameters --- nextrouter/nextrouter.go | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/nextrouter/nextrouter.go b/nextrouter/nextrouter.go index 7fbffc8d03f54..12534894b7697 100644 --- a/nextrouter/nextrouter.go +++ b/nextrouter/nextrouter.go @@ -2,6 +2,7 @@ package nextrouter import ( "bytes" + "html/template" "io/fs" "net/http" "path/filepath" @@ -76,8 +77,26 @@ func serveFile(router chi.Router, fileSystem fs.FS, fileName string) { return } + // Create a template from the data - we can inject custom parameters like CSRF here + tpls, err := template.New(fileName).Parse(string(data)) + if err != nil { + // TODO(Bryan): Log here + return + } + handler := func(w http.ResponseWriter, r *http.Request) { - http.ServeContent(w, r, fileName, time.Time{}, bytes.NewReader(data)) + + var buf bytes.Buffer + err := tpls.ExecuteTemplate(&buf, fileName, nil) + + // TODO(Bryan): How to handle an error here? + if err != nil { + // TODO + http.Error(w, "500", 500) + return + } + + http.ServeContent(w, r, fileName, time.Time{}, bytes.NewReader(buf.Bytes())) } fileNameWithoutExtension := removeFileExtension(fileName) From 03a20c1d78f992a32423aa3bf81d7ff37818cf47 Mon Sep 17 00:00:00 2001 From: Bryan Phelps Date: Sat, 5 Feb 2022 04:29:40 +0000 Subject: [PATCH 24/41] Start adding plumbing for templates --- nextrouter/nextrouter.go | 46 +++++++++++++++++++++++-------- nextrouter/nextrouter_test.go | 52 +++++++++++++++++++---------------- 2 files changed, 62 insertions(+), 36 deletions(-) diff --git a/nextrouter/nextrouter.go b/nextrouter/nextrouter.go index 12534894b7697..b8d34676c05cb 100644 --- a/nextrouter/nextrouter.go +++ b/nextrouter/nextrouter.go @@ -12,17 +12,20 @@ import ( "github.com/go-chi/chi/v5" ) +// +type TemplateDataFunc func(*http.Request) interface{} + // Handler returns an HTTP handler for serving a next-based static site // This handler respects NextJS-based routing rules: // https://nextjs.org/docs/routing/dynamic-routes // // 1) If a file is of the form `[org]`, it's a dynamic route for a single-parameter // 2) If a file is of the form `[[...any]]`, it's a dynamic route for any parameters -func Handler(fileSystem fs.FS) http.Handler { +func Handler(fileSystem fs.FS, templateFunc TemplateDataFunc) http.Handler { router := chi.NewRouter() // Build up a router that matches NextJS routing rules, for HTML files - buildRouter(router, fileSystem) + buildRouter(router, fileSystem, templateFunc) // Fallback to static file server for non-HTML files // Non-HTML files don't have special routing rules, so we can just leverage @@ -33,38 +36,49 @@ func Handler(fileSystem fs.FS) http.Handler { return router } -func buildRouter(rtr chi.Router, fileSystem fs.FS) { +// buildRouter recursively traverses the file-system, building routes +// as appropriate for respecting NextJS dynamic rules. +func buildRouter(rtr chi.Router, fileSystem fs.FS, templateFunc TemplateDataFunc) { files, err := fs.ReadDir(fileSystem, ".") if err != nil { // TODO(Bryan): Log return } + // Loop through everything in the current directory... for _, file := range files { name := file.Name() + // ...if it's a directory, create a sub-route by + // recursively calling `buildRouter` if file.IsDir() { sub, err := fs.Sub(fileSystem, name) if err != nil { // TODO(Bryan): Log continue } - routeName := name + // In the special case where the folder is dynamic, + // like `[org]`, we can convert to a chi-style dynamic route + // (which uses `{` instead of `[`) + routeName := name if isDynamicRoute(name) { routeName = "{dynamic}" } rtr.Route("/"+routeName, func(r chi.Router) { - buildRouter(r, sub) + buildRouter(r, sub, templateFunc) }) } else { - serveFile(rtr, fileSystem, name) + // ...otherwise, if it's a file - serve it up! + serveFile(rtr, fileSystem, name, templateFunc) } } } -func serveFile(router chi.Router, fileSystem fs.FS, fileName string) { +// serveFile is responsible for serving up HTML files in our next router +// It handles various special cases, like trailing-slashes or handling routes w/o the .html suffix. +func serveFile(router chi.Router, fileSystem fs.FS, fileName string, templateFunc TemplateDataFunc) { // We only handle .html files for now ext := filepath.Ext(fileName) if ext != ".html" { @@ -84,23 +98,28 @@ func serveFile(router chi.Router, fileSystem fs.FS, fileName string) { return } - handler := func(w http.ResponseWriter, r *http.Request) { - + handler := func(writer http.ResponseWriter, request *http.Request) { var buf bytes.Buffer - err := tpls.ExecuteTemplate(&buf, fileName, nil) + + // See if there are any template parameters we need to inject! + // Things like CSRF tokens, etc... + templateData := templateFunc(request) + + err := tpls.ExecuteTemplate(&buf, fileName, templateData) // TODO(Bryan): How to handle an error here? if err != nil { // TODO - http.Error(w, "500", 500) + http.Error(writer, "500", 500) return } - http.ServeContent(w, r, fileName, time.Time{}, bytes.NewReader(buf.Bytes())) + http.ServeContent(writer, request, fileName, time.Time{}, bytes.NewReader(buf.Bytes())) } fileNameWithoutExtension := removeFileExtension(fileName) + // Handle the `[[...any]]` catch-all case if isCatchAllRoute(fileNameWithoutExtension) { router.NotFound(handler) return @@ -112,7 +131,10 @@ func serveFile(router chi.Router, fileSystem fs.FS, fileName string) { return } + // Handle the basic file cases + // Directly accessing a file, ie `/providers.html` router.Get("/"+fileName, handler) + // Accessing a file without an extension, ie `/providers` router.Get("/"+fileNameWithoutExtension, handler) // Special case: '/' should serve index.html diff --git a/nextrouter/nextrouter_test.go b/nextrouter/nextrouter_test.go index 9ff4009d4fc23..b07063c2eac6f 100644 --- a/nextrouter/nextrouter_test.go +++ b/nextrouter/nextrouter_test.go @@ -13,18 +13,6 @@ import ( "github.com/coder/coder/nextrouter" ) -func request(server *httptest.Server, path string) (*http.Response, error) { - req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, server.URL+path, nil) - if err != nil { - return nil, err - } - res, err := http.DefaultClient.Do(req) - if err != nil { - return nil, err - } - return res, err -} - func TestNextRouter(t *testing.T) { t.Parallel() @@ -34,7 +22,7 @@ func TestNextRouter(t *testing.T) { err := rootFS.WriteFile("test.html", []byte("test123"), 0755) require.NoError(t, err) - router := nextrouter.Handler(rootFS) + router := nextrouter.Handler(rootFS, noopTemplateFunc) server := httptest.NewServer(router) res, err := request(server, "/test.html") @@ -56,7 +44,7 @@ func TestNextRouter(t *testing.T) { err = rootFS.WriteFile("folder.html", []byte("folderFile"), 0755) require.NoError(t, err) - router := nextrouter.Handler(rootFS) + router := nextrouter.Handler(rootFS, noopTemplateFunc) server := httptest.NewServer(router) res, err := request(server, "/folder/") @@ -75,7 +63,7 @@ func TestNextRouter(t *testing.T) { err := rootFS.WriteFile("test.png", []byte("png-bytes"), 0755) require.NoError(t, err) - router := nextrouter.Handler(rootFS) + router := nextrouter.Handler(rootFS, noopTemplateFunc) server := httptest.NewServer(router) res, err := request(server, "/test.png") @@ -95,7 +83,7 @@ func TestNextRouter(t *testing.T) { err := rootFS.WriteFile("test.html", []byte("test-no-extension"), 0755) require.NoError(t, err) - router := nextrouter.Handler(rootFS) + router := nextrouter.Handler(rootFS, noopTemplateFunc) server := httptest.NewServer(router) res, err := request(server, "/test") @@ -114,7 +102,7 @@ func TestNextRouter(t *testing.T) { err := rootFS.WriteFile("index.html", []byte("test-root-index"), 0755) require.NoError(t, err) - router := nextrouter.Handler(rootFS) + router := nextrouter.Handler(rootFS, noopTemplateFunc) server := httptest.NewServer(router) res, err := request(server, "/") @@ -138,7 +126,7 @@ func TestNextRouter(t *testing.T) { rootFS.WriteFile("test/a/b/c.html", []byte("test123"), 0755) require.NoError(t, err) - router := nextrouter.Handler(rootFS) + router := nextrouter.Handler(rootFS, noopTemplateFunc) server := httptest.NewServer(router) res, err := request(server, "/test/a/b/c.html") @@ -165,7 +153,7 @@ func TestNextRouter(t *testing.T) { rootFS.WriteFile("test/a/b/c/index.html", []byte("test-abc-index"), 0755) require.NoError(t, err) - router := nextrouter.Handler(rootFS) + router := nextrouter.Handler(rootFS, noopTemplateFunc) server := httptest.NewServer(router) @@ -186,7 +174,7 @@ func TestNextRouter(t *testing.T) { err := rootFS.WriteFile("test.html", []byte("test123"), 0755) require.NoError(t, err) - router := nextrouter.Handler(rootFS) + router := nextrouter.Handler(rootFS, noopTemplateFunc) server := httptest.NewServer(router) res, err := request(server, "/test-non-existent.html") @@ -203,7 +191,7 @@ func TestNextRouter(t *testing.T) { err = rootFS.WriteFile("folder/[orgs].html", []byte("test-dynamic-path"), 0755) require.NoError(t, err) - router := nextrouter.Handler(rootFS) + router := nextrouter.Handler(rootFS, noopTemplateFunc) server := httptest.NewServer(router) res, err := request(server, "/folder/org-1") @@ -224,7 +212,7 @@ func TestNextRouter(t *testing.T) { err = rootFS.WriteFile("folder/[org]/[project]/create.html", []byte("test-create"), 0755) require.NoError(t, err) - router := nextrouter.Handler(rootFS) + router := nextrouter.Handler(rootFS, noopTemplateFunc) server := httptest.NewServer(router) res, err := request(server, "/folder/org-1/project-1/create") @@ -245,7 +233,7 @@ func TestNextRouter(t *testing.T) { err = rootFS.WriteFile("folder/[[...any]].html", []byte("test-catch-all"), 0755) require.NoError(t, err) - router := nextrouter.Handler(rootFS) + router := nextrouter.Handler(rootFS, noopTemplateFunc) server := httptest.NewServer(router) res, err := request(server, "/folder/org-1/project-1/random") @@ -268,7 +256,7 @@ func TestNextRouter(t *testing.T) { err = rootFS.WriteFile("folder/create.html", []byte("test-create"), 0755) require.NoError(t, err) - router := nextrouter.Handler(rootFS) + router := nextrouter.Handler(rootFS, noopTemplateFunc) server := httptest.NewServer(router) res, err := request(server, "/folder/create") @@ -281,3 +269,19 @@ func TestNextRouter(t *testing.T) { require.Equal(t, res.StatusCode, 200) }) } + +func request(server *httptest.Server, path string) (*http.Response, error) { + req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, server.URL+path, nil) + if err != nil { + return nil, err + } + res, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err + } + return res, err +} + +func noopTemplateFunc(request *http.Request) interface{} { + return nil +} From 199d46827d03e5af07d0b303bd784d177f0ff6a8 Mon Sep 17 00:00:00 2001 From: Bryan Phelps Date: Sat, 5 Feb 2022 04:35:56 +0000 Subject: [PATCH 25/41] Add template functionality --- nextrouter/nextrouter.go | 5 ++++- nextrouter/nextrouter_test.go | 37 +++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/nextrouter/nextrouter.go b/nextrouter/nextrouter.go index b8d34676c05cb..07b5e02083957 100644 --- a/nextrouter/nextrouter.go +++ b/nextrouter/nextrouter.go @@ -12,7 +12,10 @@ import ( "github.com/go-chi/chi/v5" ) -// +// TemplateDataFunc is a function that lets the consumer of `nextrouter` +// inject arbitrary template parameters, based on the request. This is useful +// if the Request object is carrying CSRF tokens, session tokens, etc - +// they can be emitted in the page. type TemplateDataFunc func(*http.Request) interface{} // Handler returns an HTTP handler for serving a next-based static site diff --git a/nextrouter/nextrouter_test.go b/nextrouter/nextrouter_test.go index b07063c2eac6f..13b3e746a13f7 100644 --- a/nextrouter/nextrouter_test.go +++ b/nextrouter/nextrouter_test.go @@ -16,6 +16,43 @@ import ( func TestNextRouter(t *testing.T) { t.Parallel() + t.Run("Injects template parameters", func(t *testing.T) { + t.Parallel() + + rootFS := memfs.New() + err := rootFS.WriteFile("test.html", []byte("{{ .CSRF.Token }}"), 0755) + require.NoError(t, err) + + type csrfState struct { + Token string + } + + type template struct { + CSRF csrfState + } + + // Add custom template function + templateFunc := func(request *http.Request) interface{} { + return template{ + CSRF: csrfState{ + Token: "hello-csrf", + }, + } + } + + router := nextrouter.Handler(rootFS, templateFunc) + server := httptest.NewServer(router) + + res, err := request(server, "/test.html") + require.NoError(t, err) + defer res.Body.Close() + + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + require.Equal(t, string(body), "hello-csrf") + require.Equal(t, res.StatusCode, 200) + }) + t.Run("Serves file at root", func(t *testing.T) { t.Parallel() rootFS := memfs.New() From ca515e47a46a95278d709f4a10ad984908a76570 Mon Sep 17 00:00:00 2001 From: Bryan Phelps Date: Sat, 5 Feb 2022 04:59:54 +0000 Subject: [PATCH 26/41] Set up 404 handling --- nextrouter/nextrouter.go | 30 ++++++++- nextrouter/nextrouter_test.go | 111 ++++++++++++++++++++++------------ 2 files changed, 101 insertions(+), 40 deletions(-) diff --git a/nextrouter/nextrouter.go b/nextrouter/nextrouter.go index 07b5e02083957..acc8294536e77 100644 --- a/nextrouter/nextrouter.go +++ b/nextrouter/nextrouter.go @@ -36,6 +36,9 @@ func Handler(fileSystem fs.FS, templateFunc TemplateDataFunc) http.Handler { fileHandler := http.FileServer(http.FS(fileSystem)) router.NotFound(fileHandler.ServeHTTP) + // Finally, if there is a 404.html available, serve that + serve404IfAvailable(fileSystem, router) + return router } @@ -112,8 +115,7 @@ func serveFile(router chi.Router, fileSystem fs.FS, fileName string, templateFun // TODO(Bryan): How to handle an error here? if err != nil { - // TODO - http.Error(writer, "500", 500) + http.Error(writer, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } @@ -150,6 +152,26 @@ func serveFile(router chi.Router, fileSystem fs.FS, fileName string, templateFun } } +func serve404IfAvailable(fileSystem fs.FS, router chi.Router) { + // Get the file contents + fileBytes, err := fs.ReadFile(fileSystem, "404.html") + if err != nil { + // An error is expected if the file doesn't exist + return + } + + router.NotFound(func(writer http.ResponseWriter, request *http.Request) { + writer.WriteHeader(http.StatusNotFound) + _, err = writer.Write(fileBytes) + if err != nil { + // TODO(Bryan) + return + } + }) +} + +// isDynamicRoute returns true if the file is a NextJS dynamic route, like `[orgs]` +// Returns false if the file is not a dynamic route, or if it is a catch-all route (`[[...any]]`) func isDynamicRoute(fileName string) bool { fileWithoutExtension := removeFileExtension(fileName) @@ -162,12 +184,16 @@ func isDynamicRoute(fileName string) bool { return fileWithoutExtension[0] == '[' && fileWithoutExtension[1] != '[' && fileWithoutExtension[byteLen-1] == ']' } +// isCatchAllRoute returns true if the file is a catch-all route, like `[[...any]]` +// Return false otherwise func isCatchAllRoute(fileName string) bool { fileWithoutExtension := removeFileExtension(fileName) ret := strings.HasPrefix(fileWithoutExtension, "[[.") return ret } +// removeFileExtension removes the extension from a file +// For example, removeFileExtension("index.html") would return "index" func removeFileExtension(fileName string) string { return strings.TrimSuffix(fileName, filepath.Ext(fileName)) } diff --git a/nextrouter/nextrouter_test.go b/nextrouter/nextrouter_test.go index 13b3e746a13f7..d05ed8fa0203f 100644 --- a/nextrouter/nextrouter_test.go +++ b/nextrouter/nextrouter_test.go @@ -16,43 +16,6 @@ import ( func TestNextRouter(t *testing.T) { t.Parallel() - t.Run("Injects template parameters", func(t *testing.T) { - t.Parallel() - - rootFS := memfs.New() - err := rootFS.WriteFile("test.html", []byte("{{ .CSRF.Token }}"), 0755) - require.NoError(t, err) - - type csrfState struct { - Token string - } - - type template struct { - CSRF csrfState - } - - // Add custom template function - templateFunc := func(request *http.Request) interface{} { - return template{ - CSRF: csrfState{ - Token: "hello-csrf", - }, - } - } - - router := nextrouter.Handler(rootFS, templateFunc) - server := httptest.NewServer(router) - - res, err := request(server, "/test.html") - require.NoError(t, err) - defer res.Body.Close() - - body, err := io.ReadAll(res.Body) - require.NoError(t, err) - require.Equal(t, string(body), "hello-csrf") - require.Equal(t, res.StatusCode, 200) - }) - t.Run("Serves file at root", func(t *testing.T) { t.Parallel() rootFS := memfs.New() @@ -220,6 +183,41 @@ func TestNextRouter(t *testing.T) { require.Equal(t, res.StatusCode, 404) }) + t.Run("404 if file at root is not found", func(t *testing.T) { + t.Parallel() + + rootFS := memfs.New() + err := rootFS.WriteFile("test.html", []byte("test123"), 0755) + require.NoError(t, err) + + router := nextrouter.Handler(rootFS, noopTemplateFunc) + server := httptest.NewServer(router) + + res, err := request(server, "/test-non-existent.html") + require.NoError(t, err) + defer res.Body.Close() + require.Equal(t, res.StatusCode, 404) + }) + + t.Run("Serve custom 404.html if available", func(t *testing.T) { + t.Parallel() + + rootFS := memfs.New() + err := rootFS.WriteFile("404.html", []byte("404 custom content"), 0755) + require.NoError(t, err) + + router := nextrouter.Handler(rootFS, noopTemplateFunc) + server := httptest.NewServer(router) + + res, err := request(server, "/test-non-existent.html") + require.NoError(t, err) + defer res.Body.Close() + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + require.Equal(t, res.StatusCode, 404) + require.Equal(t, string(body), "404 custom content") + }) + t.Run("Serves dynamic-routed file", func(t *testing.T) { t.Parallel() rootFS := memfs.New() @@ -305,6 +303,43 @@ func TestNextRouter(t *testing.T) { require.Equal(t, string(body), "test-create") require.Equal(t, res.StatusCode, 200) }) + + t.Run("Injects template parameters", func(t *testing.T) { + t.Parallel() + + rootFS := memfs.New() + err := rootFS.WriteFile("test.html", []byte("{{ .CSRF.Token }}"), 0755) + require.NoError(t, err) + + type csrfState struct { + Token string + } + + type template struct { + CSRF csrfState + } + + // Add custom template function + templateFunc := func(request *http.Request) interface{} { + return template{ + CSRF: csrfState{ + Token: "hello-csrf", + }, + } + } + + router := nextrouter.Handler(rootFS, templateFunc) + server := httptest.NewServer(router) + + res, err := request(server, "/test.html") + require.NoError(t, err) + defer res.Body.Close() + + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + require.Equal(t, string(body), "hello-csrf") + require.Equal(t, res.StatusCode, 200) + }) } func request(server *httptest.Server, path string) (*http.Response, error) { @@ -319,6 +354,6 @@ func request(server *httptest.Server, path string) (*http.Response, error) { return res, err } -func noopTemplateFunc(request *http.Request) interface{} { +func noopTemplateFunc(_ *http.Request) interface{} { return nil } From 6072bad38210711207ce091405a07c95304f8653 Mon Sep 17 00:00:00 2001 From: Bryan Phelps Date: Sat, 5 Feb 2022 05:01:15 +0000 Subject: [PATCH 27/41] Use nextrouter package in site, which simplifies it quite a bit --- site/embed.go | 182 ++++---------------------------------------------- 1 file changed, 13 insertions(+), 169 deletions(-) diff --git a/site/embed.go b/site/embed.go index 69326b22403b7..a6f2c78d12fe2 100644 --- a/site/embed.go +++ b/site/embed.go @@ -1,21 +1,16 @@ package site import ( - "bytes" "embed" "fmt" - "io" "io/fs" "net/http" - "path" - "path/filepath" "strings" - "text/template" // html/template escapes some nonces - "time" "github.com/justinas/nosurf" "github.com/unrolled/secure" - "golang.org/x/xerrors" + + "github.com/coder/coder/nextrouter" ) // The `embed` package ignores recursively including directories @@ -34,46 +29,18 @@ func Handler() http.Handler { panic(err) } - // html files are handled by a text/template. Non-html files - // are served by the default file server. - files, err := htmlFiles(filesystem) - if err != nil { - panic(xerrors.Errorf("Failed to return handler for static files. Html files failed to load: %w", err)) - } - - return secureHeaders(&handler{ - fs: filesystem, - htmlFiles: files, - h: http.FileServer(http.FS(filesystem)), // All other non-html static files - }) -} - -type handler struct { - fs fs.FS - // htmlFiles is the text/template for all *.html files. - // This is needed to support Content Security Policy headers. - // Due to material UI, we are forced to use a nonce to allow inline - // scripts, and that nonce is passed through a template. - // We only do this for html files to reduce the amount of in memory caching - // of duplicate files as `fs`. - htmlFiles *htmlTemplates - h http.Handler -} - -// filePath returns the filepath of the requested file. -func (*handler) filePath(p string) string { - if !strings.HasPrefix(p, "/") { - p = "/" + p + // Render CSP and CSRF in the served pages + templateFunc := func(r *http.Request) interface{} { + return htmlState{ + // Nonce is the CSP nonce for the given request (if there is one present) + CSP: cspState{Nonce: secure.CSPNonce(r.Context())}, + // Token is the CSRF token for the given request + CSRF: csrfState{Token: nosurf.Token(r)}, + } } - return strings.TrimPrefix(path.Clean(p), "/") -} -func (h *handler) exists(filePath string) bool { - f, err := h.fs.Open(filePath) - if err == nil { - _ = f.Close() - } - return err == nil + nextRouterHandler := nextrouter.Handler(filesystem, templateFunc) + return secureHeaders(nextRouterHandler) } type htmlState struct { @@ -89,80 +56,6 @@ type csrfState struct { Token string } -func (h *handler) ServeHTTP(rw http.ResponseWriter, r *http.Request) { - // reqFile is the static file requested - reqFile := h.filePath(r.URL.Path) - state := htmlState{ - // Nonce is the CSP nonce for the given request (if there is one present) - CSP: cspState{Nonce: secure.CSPNonce(r.Context())}, - // Token is the CSRF token for the given request - CSRF: csrfState{Token: nosurf.Token(r)}, - } - - // First check if it's a file we have in our templates - if h.serveHTML(rw, r, reqFile, state) { - return - } - - // If the original file path exists we serve it. - if h.exists(reqFile) { - h.h.ServeHTTP(rw, r) - return - } - - // Serve the file assuming it's an html file - // This matches paths like `/app/terminal.html` - r.URL.Path = strings.TrimSuffix(r.URL.Path, "/") - r.URL.Path += ".html" - - reqFile = h.filePath(r.URL.Path) - // All html files should be served by the htmlFile templates - if h.serveHTML(rw, r, reqFile, state) { - return - } - - // If we don't have the file... we should redirect to `/` - // for our single-page-app. - r.URL.Path = "/" - if h.serveHTML(rw, r, "", state) { - return - } - - // This will send a correct 404 - h.h.ServeHTTP(rw, r) -} - -func (h *handler) serveHTML(rw http.ResponseWriter, r *http.Request, reqPath string, state htmlState) bool { - if data, err := h.htmlFiles.renderWithState(reqPath, state); err == nil { - if reqPath == "" { - // Pass "index.html" to the ServeContent so the ServeContent sets the right content headers. - reqPath = "index.html" - } - http.ServeContent(rw, r, reqPath, time.Time{}, bytes.NewReader(data)) - return true - } - return false -} - -type htmlTemplates struct { - tpls *template.Template -} - -// renderWithState will render the file using the given nonce if the file exists -// as a template. If it does not, it will return an error. -func (t *htmlTemplates) renderWithState(filePath string, state htmlState) ([]byte, error) { - var buf bytes.Buffer - if filePath == "" { - filePath = "index.html" - } - err := t.tpls.ExecuteTemplate(&buf, filePath, state) - if err != nil { - return nil, err - } - - return buf.Bytes(), nil -} - // cspDirectives is a map of all csp fetch directives to their values. // Each directive is a set of values that is joined by a space (' '). // All directives are semi-colon separated as a single string for the csp header. @@ -263,53 +156,4 @@ func secureHeaders(next http.Handler) http.Handler { // Prevent the browser from sending Referer header with requests ReferrerPolicy: "no-referrer", }).Handler(next) -} - -// htmlFiles recursively walks the file system passed finding all *.html files. -// The template returned has all html files parsed. -func htmlFiles(files fs.FS) (*htmlTemplates, error) { - // root is the collection of html templates. All templates are named by their pathing. - // So './404.html' is named '404.html'. './subdir/index.html' is 'subdir/index.html' - root := template.New("") - - rootPath := "." - err := fs.WalkDir(files, rootPath, func(path string, dirEntry fs.DirEntry, err error) error { - if err != nil { - return err - } - - if dirEntry.IsDir() { - return nil - } - - if filepath.Ext(dirEntry.Name()) != ".html" { - return nil - } - - file, err := files.Open(path) - if err != nil { - return err - } - - data, err := io.ReadAll(file) - if err != nil { - return err - } - - tPath := strings.TrimPrefix(path, rootPath+string(filepath.Separator)) - _, err = root.New(tPath).Parse(string(data)) - if err != nil { - return err - } - - return nil - }) - - if err != nil { - return nil, err - } - - return &htmlTemplates{ - tpls: root, - }, nil -} +} \ No newline at end of file From 826eaf59af3bf5a151cff78ed43405d56ca129e3 Mon Sep 17 00:00:00 2001 From: Bryan Phelps Date: Sat, 5 Feb 2022 18:05:30 +0000 Subject: [PATCH 28/41] Update interface to take logger --- coderd/coderd.go | 2 +- nextrouter/nextrouter.go | 55 +++++++++++++++++++++++++---------- nextrouter/nextrouter_test.go | 35 ++++++++++++---------- site/embed.go | 11 +++++-- 4 files changed, 69 insertions(+), 34 deletions(-) diff --git a/coderd/coderd.go b/coderd/coderd.go index 0aedb1d0c6847..bac1ca2ddf8f5 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -109,7 +109,7 @@ func New(options *Options) http.Handler { r.Get("/serve", api.provisionerDaemonsServe) }) }) - r.NotFound(site.Handler().ServeHTTP) + r.NotFound(site.Handler(options.Logger).ServeHTTP) return r } diff --git a/nextrouter/nextrouter.go b/nextrouter/nextrouter.go index acc8294536e77..38f416dd4743b 100644 --- a/nextrouter/nextrouter.go +++ b/nextrouter/nextrouter.go @@ -2,6 +2,7 @@ package nextrouter import ( "bytes" + "context" "html/template" "io/fs" "net/http" @@ -10,8 +11,15 @@ import ( "time" "github.com/go-chi/chi/v5" + + "cdr.dev/slog" ) +type Options struct { + Logger slog.Logger + TemplateDataFunc TemplateDataFunc +} + // TemplateDataFunc is a function that lets the consumer of `nextrouter` // inject arbitrary template parameters, based on the request. This is useful // if the Request object is carrying CSRF tokens, session tokens, etc - @@ -24,11 +32,17 @@ type TemplateDataFunc func(*http.Request) interface{} // // 1) If a file is of the form `[org]`, it's a dynamic route for a single-parameter // 2) If a file is of the form `[[...any]]`, it's a dynamic route for any parameters -func Handler(fileSystem fs.FS, templateFunc TemplateDataFunc) http.Handler { +func Handler(fileSystem fs.FS, options *Options) http.Handler { + if options == nil { + options = &Options{ + Logger: slog.Logger{}, + TemplateDataFunc: nil, + } + } router := chi.NewRouter() // Build up a router that matches NextJS routing rules, for HTML files - buildRouter(router, fileSystem, templateFunc) + buildRouter(router, fileSystem, *options) // Fallback to static file server for non-HTML files // Non-HTML files don't have special routing rules, so we can just leverage @@ -37,17 +51,17 @@ func Handler(fileSystem fs.FS, templateFunc TemplateDataFunc) http.Handler { router.NotFound(fileHandler.ServeHTTP) // Finally, if there is a 404.html available, serve that - serve404IfAvailable(fileSystem, router) + serve404IfAvailable(fileSystem, router, *options) return router } // buildRouter recursively traverses the file-system, building routes // as appropriate for respecting NextJS dynamic rules. -func buildRouter(rtr chi.Router, fileSystem fs.FS, templateFunc TemplateDataFunc) { +func buildRouter(rtr chi.Router, fileSystem fs.FS, options Options) { files, err := fs.ReadDir(fileSystem, ".") if err != nil { - // TODO(Bryan): Log + options.Logger.Warn(context.Background(), "Provided filesystem is empty; unable to build routes") return } @@ -60,7 +74,7 @@ func buildRouter(rtr chi.Router, fileSystem fs.FS, templateFunc TemplateDataFunc if file.IsDir() { sub, err := fs.Sub(fileSystem, name) if err != nil { - // TODO(Bryan): Log + options.Logger.Error(context.Background(), "Unable to call fs.Sub on directory", slog.F("directory_name", name)) continue } @@ -72,35 +86,38 @@ func buildRouter(rtr chi.Router, fileSystem fs.FS, templateFunc TemplateDataFunc routeName = "{dynamic}" } + options.Logger.Info(context.Background(), "Adding route", slog.F("name", name), slog.F("routeName", routeName)) rtr.Route("/"+routeName, func(r chi.Router) { - buildRouter(r, sub, templateFunc) + buildRouter(r, sub, options) }) } else { // ...otherwise, if it's a file - serve it up! - serveFile(rtr, fileSystem, name, templateFunc) + serveFile(rtr, fileSystem, name, options) } } } // serveFile is responsible for serving up HTML files in our next router // It handles various special cases, like trailing-slashes or handling routes w/o the .html suffix. -func serveFile(router chi.Router, fileSystem fs.FS, fileName string, templateFunc TemplateDataFunc) { +func serveFile(router chi.Router, fileSystem fs.FS, fileName string, options Options) { // We only handle .html files for now ext := filepath.Ext(fileName) if ext != ".html" { return } + options.Logger.Debug(context.Background(), "Reading file", slog.F("fileName", fileName)) + data, err := fs.ReadFile(fileSystem, fileName) if err != nil { - // TODO(Bryan): Log here + options.Logger.Error(context.Background(), "Unable to read file", slog.F("fileName", fileName)) return } // Create a template from the data - we can inject custom parameters like CSRF here tpls, err := template.New(fileName).Parse(string(data)) if err != nil { - // TODO(Bryan): Log here + options.Logger.Error(context.Background(), "Unable to create template for file", slog.F("fileName", fileName)) return } @@ -109,12 +126,18 @@ func serveFile(router chi.Router, fileSystem fs.FS, fileName string, templateFun // See if there are any template parameters we need to inject! // Things like CSRF tokens, etc... - templateData := templateFunc(request) + //templateData := struct{}{} + var templateData interface{} + templateData = nil + if options.TemplateDataFunc != nil { + templateData = options.TemplateDataFunc(request) + } + options.Logger.Debug(context.Background(), "Applying template parameters", slog.F("fileName", fileName), slog.F("templateData", templateData)) err := tpls.ExecuteTemplate(&buf, fileName, templateData) - // TODO(Bryan): How to handle an error here? if err != nil { + options.Logger.Error(request.Context(), "Error executing template", slog.F("template_parameters", templateData)) http.Error(writer, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } @@ -126,12 +149,14 @@ func serveFile(router chi.Router, fileSystem fs.FS, fileName string, templateFun // Handle the `[[...any]]` catch-all case if isCatchAllRoute(fileNameWithoutExtension) { + options.Logger.Info(context.Background(), "Registering catch-all route", slog.F("fileName", fileName)) router.NotFound(handler) return } // Handle the `[org]` dynamic route case if isDynamicRoute(fileNameWithoutExtension) { + options.Logger.Info(context.Background(), "Registering dynamic route", slog.F("fileName", fileName)) router.Get("/{dynamic}", handler) return } @@ -152,7 +177,7 @@ func serveFile(router chi.Router, fileSystem fs.FS, fileName string, templateFun } } -func serve404IfAvailable(fileSystem fs.FS, router chi.Router) { +func serve404IfAvailable(fileSystem fs.FS, router chi.Router, options Options) { // Get the file contents fileBytes, err := fs.ReadFile(fileSystem, "404.html") if err != nil { @@ -164,7 +189,7 @@ func serve404IfAvailable(fileSystem fs.FS, router chi.Router) { writer.WriteHeader(http.StatusNotFound) _, err = writer.Write(fileBytes) if err != nil { - // TODO(Bryan) + options.Logger.Error(request.Context(), "Unable to write bytes for 404") return } }) diff --git a/nextrouter/nextrouter_test.go b/nextrouter/nextrouter_test.go index d05ed8fa0203f..e05dd11fe8f97 100644 --- a/nextrouter/nextrouter_test.go +++ b/nextrouter/nextrouter_test.go @@ -10,6 +10,8 @@ import ( "github.com/psanford/memfs" "github.com/stretchr/testify/require" + "cdr.dev/slog" + "github.com/coder/coder/nextrouter" ) @@ -22,7 +24,7 @@ func TestNextRouter(t *testing.T) { err := rootFS.WriteFile("test.html", []byte("test123"), 0755) require.NoError(t, err) - router := nextrouter.Handler(rootFS, noopTemplateFunc) + router := nextrouter.Handler(rootFS, nil) server := httptest.NewServer(router) res, err := request(server, "/test.html") @@ -44,7 +46,7 @@ func TestNextRouter(t *testing.T) { err = rootFS.WriteFile("folder.html", []byte("folderFile"), 0755) require.NoError(t, err) - router := nextrouter.Handler(rootFS, noopTemplateFunc) + router := nextrouter.Handler(rootFS, nil) server := httptest.NewServer(router) res, err := request(server, "/folder/") @@ -63,7 +65,7 @@ func TestNextRouter(t *testing.T) { err := rootFS.WriteFile("test.png", []byte("png-bytes"), 0755) require.NoError(t, err) - router := nextrouter.Handler(rootFS, noopTemplateFunc) + router := nextrouter.Handler(rootFS, nil) server := httptest.NewServer(router) res, err := request(server, "/test.png") @@ -83,7 +85,7 @@ func TestNextRouter(t *testing.T) { err := rootFS.WriteFile("test.html", []byte("test-no-extension"), 0755) require.NoError(t, err) - router := nextrouter.Handler(rootFS, noopTemplateFunc) + router := nextrouter.Handler(rootFS, nil) server := httptest.NewServer(router) res, err := request(server, "/test") @@ -102,7 +104,7 @@ func TestNextRouter(t *testing.T) { err := rootFS.WriteFile("index.html", []byte("test-root-index"), 0755) require.NoError(t, err) - router := nextrouter.Handler(rootFS, noopTemplateFunc) + router := nextrouter.Handler(rootFS, nil) server := httptest.NewServer(router) res, err := request(server, "/") @@ -126,7 +128,7 @@ func TestNextRouter(t *testing.T) { rootFS.WriteFile("test/a/b/c.html", []byte("test123"), 0755) require.NoError(t, err) - router := nextrouter.Handler(rootFS, noopTemplateFunc) + router := nextrouter.Handler(rootFS, nil) server := httptest.NewServer(router) res, err := request(server, "/test/a/b/c.html") @@ -153,7 +155,7 @@ func TestNextRouter(t *testing.T) { rootFS.WriteFile("test/a/b/c/index.html", []byte("test-abc-index"), 0755) require.NoError(t, err) - router := nextrouter.Handler(rootFS, noopTemplateFunc) + router := nextrouter.Handler(rootFS, nil) server := httptest.NewServer(router) @@ -174,7 +176,7 @@ func TestNextRouter(t *testing.T) { err := rootFS.WriteFile("test.html", []byte("test123"), 0755) require.NoError(t, err) - router := nextrouter.Handler(rootFS, noopTemplateFunc) + router := nextrouter.Handler(rootFS, nil) server := httptest.NewServer(router) res, err := request(server, "/test-non-existent.html") @@ -190,7 +192,7 @@ func TestNextRouter(t *testing.T) { err := rootFS.WriteFile("test.html", []byte("test123"), 0755) require.NoError(t, err) - router := nextrouter.Handler(rootFS, noopTemplateFunc) + router := nextrouter.Handler(rootFS, nil) server := httptest.NewServer(router) res, err := request(server, "/test-non-existent.html") @@ -206,7 +208,7 @@ func TestNextRouter(t *testing.T) { err := rootFS.WriteFile("404.html", []byte("404 custom content"), 0755) require.NoError(t, err) - router := nextrouter.Handler(rootFS, noopTemplateFunc) + router := nextrouter.Handler(rootFS, nil) server := httptest.NewServer(router) res, err := request(server, "/test-non-existent.html") @@ -226,7 +228,7 @@ func TestNextRouter(t *testing.T) { err = rootFS.WriteFile("folder/[orgs].html", []byte("test-dynamic-path"), 0755) require.NoError(t, err) - router := nextrouter.Handler(rootFS, noopTemplateFunc) + router := nextrouter.Handler(rootFS, nil) server := httptest.NewServer(router) res, err := request(server, "/folder/org-1") @@ -247,7 +249,7 @@ func TestNextRouter(t *testing.T) { err = rootFS.WriteFile("folder/[org]/[project]/create.html", []byte("test-create"), 0755) require.NoError(t, err) - router := nextrouter.Handler(rootFS, noopTemplateFunc) + router := nextrouter.Handler(rootFS, nil) server := httptest.NewServer(router) res, err := request(server, "/folder/org-1/project-1/create") @@ -268,7 +270,7 @@ func TestNextRouter(t *testing.T) { err = rootFS.WriteFile("folder/[[...any]].html", []byte("test-catch-all"), 0755) require.NoError(t, err) - router := nextrouter.Handler(rootFS, noopTemplateFunc) + router := nextrouter.Handler(rootFS, nil) server := httptest.NewServer(router) res, err := request(server, "/folder/org-1/project-1/random") @@ -291,7 +293,7 @@ func TestNextRouter(t *testing.T) { err = rootFS.WriteFile("folder/create.html", []byte("test-create"), 0755) require.NoError(t, err) - router := nextrouter.Handler(rootFS, noopTemplateFunc) + router := nextrouter.Handler(rootFS, nil) server := httptest.NewServer(router) res, err := request(server, "/folder/create") @@ -328,7 +330,10 @@ func TestNextRouter(t *testing.T) { } } - router := nextrouter.Handler(rootFS, templateFunc) + router := nextrouter.Handler(rootFS, &nextrouter.Options{ + Logger: slog.Logger{}, + TemplateDataFunc: templateFunc, + }) server := httptest.NewServer(router) res, err := request(server, "/test.html") diff --git a/site/embed.go b/site/embed.go index a6f2c78d12fe2..a6f7b1ad09890 100644 --- a/site/embed.go +++ b/site/embed.go @@ -10,6 +10,8 @@ import ( "github.com/justinas/nosurf" "github.com/unrolled/secure" + "cdr.dev/slog" + "github.com/coder/coder/nextrouter" ) @@ -22,7 +24,7 @@ import ( var site embed.FS // Handler returns an HTTP handler for serving the static site. -func Handler() http.Handler { +func Handler(logger slog.Logger) http.Handler { filesystem, err := fs.Sub(site, "out") if err != nil { // This can't happen... Go would throw a compilation error. @@ -39,7 +41,10 @@ func Handler() http.Handler { } } - nextRouterHandler := nextrouter.Handler(filesystem, templateFunc) + nextRouterHandler := nextrouter.Handler(filesystem, &nextrouter.Options{ + Logger: logger, + TemplateDataFunc: templateFunc, + }) return secureHeaders(nextRouterHandler) } @@ -156,4 +161,4 @@ func secureHeaders(next http.Handler) http.Handler { // Prevent the browser from sending Referer header with requests ReferrerPolicy: "no-referrer", }).Handler(next) -} \ No newline at end of file +} From dac34f5e67419ec457006c65f3e072471ff27478 Mon Sep 17 00:00:00 2001 From: Bryan Phelps Date: Sat, 5 Feb 2022 18:06:01 +0000 Subject: [PATCH 29/41] Additional comments --- nextrouter/nextrouter.go | 1 + 1 file changed, 1 insertion(+) diff --git a/nextrouter/nextrouter.go b/nextrouter/nextrouter.go index 38f416dd4743b..aae629ad0819a 100644 --- a/nextrouter/nextrouter.go +++ b/nextrouter/nextrouter.go @@ -15,6 +15,7 @@ import ( "cdr.dev/slog" ) +// Options for configuring a nextrouter type Options struct { Logger slog.Logger TemplateDataFunc TemplateDataFunc From 92d4eb0fc04dd9d9f8b83c7edcf807a7f90cde25 Mon Sep 17 00:00:00 2001 From: Bryan Phelps Date: Sat, 5 Feb 2022 18:11:42 +0000 Subject: [PATCH 30/41] Fix embed_test --- site/embed_test.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/site/embed_test.go b/site/embed_test.go index 4e43f2a56bb37..9378c9cdda0bf 100644 --- a/site/embed_test.go +++ b/site/embed_test.go @@ -9,13 +9,15 @@ import ( "github.com/stretchr/testify/require" + "cdr.dev/slog" + "github.com/coder/coder/site" ) func TestIndexPageRenders(t *testing.T) { t.Parallel() - srv := httptest.NewServer(site.Handler()) + srv := httptest.NewServer(site.Handler(slog.Logger{})) req, err := http.NewRequestWithContext(context.Background(), "GET", srv.URL, nil) require.NoError(t, err) From 5262b7f56315f6941463fc8fbc5651cee2b51f17 Mon Sep 17 00:00:00 2001 From: Bryan Phelps Date: Sat, 5 Feb 2022 18:13:17 +0000 Subject: [PATCH 31/41] Remove now-unused noop template func --- nextrouter/nextrouter_test.go | 5 ----- 1 file changed, 5 deletions(-) diff --git a/nextrouter/nextrouter_test.go b/nextrouter/nextrouter_test.go index e05dd11fe8f97..ae3b3a7b3b46a 100644 --- a/nextrouter/nextrouter_test.go +++ b/nextrouter/nextrouter_test.go @@ -156,7 +156,6 @@ func TestNextRouter(t *testing.T) { require.NoError(t, err) router := nextrouter.Handler(rootFS, nil) - server := httptest.NewServer(router) res, err := request(server, "/test/a/b/c") @@ -358,7 +357,3 @@ func request(server *httptest.Server, path string) (*http.Response, error) { } return res, err } - -func noopTemplateFunc(_ *http.Request) interface{} { - return nil -} From 750bec9e4e7be8a9b1d74a4da5e402bef0fb6259 Mon Sep 17 00:00:00 2001 From: Bryan Phelps Date: Tue, 8 Feb 2022 04:41:55 +0000 Subject: [PATCH 32/41] Switch route addition from info -> debug log level --- nextrouter/nextrouter.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nextrouter/nextrouter.go b/nextrouter/nextrouter.go index aae629ad0819a..7a8b84e9f3c98 100644 --- a/nextrouter/nextrouter.go +++ b/nextrouter/nextrouter.go @@ -87,7 +87,7 @@ func buildRouter(rtr chi.Router, fileSystem fs.FS, options Options) { routeName = "{dynamic}" } - options.Logger.Info(context.Background(), "Adding route", slog.F("name", name), slog.F("routeName", routeName)) + options.Logger.Debug(context.Background(), "Adding route", slog.F("name", name), slog.F("routeName", routeName)) rtr.Route("/"+routeName, func(r chi.Router) { buildRouter(r, sub, options) }) From c15b0469c21c201c97b2aa29c9c4eb96a7a7328b Mon Sep 17 00:00:00 2001 From: Bryan Phelps Date: Tue, 8 Feb 2022 04:46:10 +0000 Subject: [PATCH 33/41] Invert logic to remove layer of indentation --- nextrouter/nextrouter.go | 47 ++++++++++++++++++++-------------------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/nextrouter/nextrouter.go b/nextrouter/nextrouter.go index 7a8b84e9f3c98..83e430d84b59e 100644 --- a/nextrouter/nextrouter.go +++ b/nextrouter/nextrouter.go @@ -70,31 +70,32 @@ func buildRouter(rtr chi.Router, fileSystem fs.FS, options Options) { for _, file := range files { name := file.Name() - // ...if it's a directory, create a sub-route by - // recursively calling `buildRouter` - if file.IsDir() { - sub, err := fs.Sub(fileSystem, name) - if err != nil { - options.Logger.Error(context.Background(), "Unable to call fs.Sub on directory", slog.F("directory_name", name)) - continue - } - - // In the special case where the folder is dynamic, - // like `[org]`, we can convert to a chi-style dynamic route - // (which uses `{` instead of `[`) - routeName := name - if isDynamicRoute(name) { - routeName = "{dynamic}" - } - - options.Logger.Debug(context.Background(), "Adding route", slog.F("name", name), slog.F("routeName", routeName)) - rtr.Route("/"+routeName, func(r chi.Router) { - buildRouter(r, sub, options) - }) - } else { - // ...otherwise, if it's a file - serve it up! + // If we're working with a file - just serve it up + if !file.IsDir() { serveFile(rtr, fileSystem, name, options) + continue + } + + // ...otherwise, if it's a directory, create a sub-route by + // recursively calling `buildRouter` + sub, err := fs.Sub(fileSystem, name) + if err != nil { + options.Logger.Error(context.Background(), "Unable to call fs.Sub on directory", slog.F("directory_name", name)) + continue + } + + // In the special case where the folder is dynamic, + // like `[org]`, we can convert to a chi-style dynamic route + // (which uses `{` instead of `[`) + routeName := name + if isDynamicRoute(name) { + routeName = "{dynamic}" } + + options.Logger.Debug(context.Background(), "Adding route", slog.F("name", name), slog.F("routeName", routeName)) + rtr.Route("/"+routeName, func(r chi.Router) { + buildRouter(r, sub, options) + }) } } From c17fc155686d7f2b5eddd77b69e8b11555bd616f Mon Sep 17 00:00:00 2001 From: Bryan Phelps Date: Tue, 8 Feb 2022 04:51:08 +0000 Subject: [PATCH 34/41] Rename buildRoutes -> registerRoutes --- nextrouter/nextrouter.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/nextrouter/nextrouter.go b/nextrouter/nextrouter.go index 83e430d84b59e..51f72e49a5bfb 100644 --- a/nextrouter/nextrouter.go +++ b/nextrouter/nextrouter.go @@ -43,7 +43,7 @@ func Handler(fileSystem fs.FS, options *Options) http.Handler { router := chi.NewRouter() // Build up a router that matches NextJS routing rules, for HTML files - buildRouter(router, fileSystem, *options) + registerRoutes(router, fileSystem, *options) // Fallback to static file server for non-HTML files // Non-HTML files don't have special routing rules, so we can just leverage @@ -57,9 +57,9 @@ func Handler(fileSystem fs.FS, options *Options) http.Handler { return router } -// buildRouter recursively traverses the file-system, building routes +// registerRoutes recursively traverses the file-system, building routes // as appropriate for respecting NextJS dynamic rules. -func buildRouter(rtr chi.Router, fileSystem fs.FS, options Options) { +func registerRoutes(rtr chi.Router, fileSystem fs.FS, options Options) { files, err := fs.ReadDir(fileSystem, ".") if err != nil { options.Logger.Warn(context.Background(), "Provided filesystem is empty; unable to build routes") @@ -92,9 +92,9 @@ func buildRouter(rtr chi.Router, fileSystem fs.FS, options Options) { routeName = "{dynamic}" } - options.Logger.Debug(context.Background(), "Adding route", slog.F("name", name), slog.F("routeName", routeName)) + options.Logger.Debug(context.Background(), "Registering route", slog.F("name", name), slog.F("routeName", routeName)) rtr.Route("/"+routeName, func(r chi.Router) { - buildRouter(r, sub, options) + registerRoutes(r, sub, options) }) } } From 34e4f46fcc65fa4542b4a83f45165a0a73aa6b9e Mon Sep 17 00:00:00 2001 From: Bryan Phelps Date: Tue, 8 Feb 2022 04:52:55 +0000 Subject: [PATCH 35/41] TemplateDataFunc -> HTMLTemplateHandler --- nextrouter/nextrouter.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nextrouter/nextrouter.go b/nextrouter/nextrouter.go index 51f72e49a5bfb..d1e5138d6d43e 100644 --- a/nextrouter/nextrouter.go +++ b/nextrouter/nextrouter.go @@ -18,14 +18,14 @@ import ( // Options for configuring a nextrouter type Options struct { Logger slog.Logger - TemplateDataFunc TemplateDataFunc + TemplateDataFunc HTMLTemplateHandler } -// TemplateDataFunc is a function that lets the consumer of `nextrouter` +// HTMLTemplateHandler is a function that lets the consumer of `nextrouter` // inject arbitrary template parameters, based on the request. This is useful // if the Request object is carrying CSRF tokens, session tokens, etc - // they can be emitted in the page. -type TemplateDataFunc func(*http.Request) interface{} +type HTMLTemplateHandler func(*http.Request) interface{} // Handler returns an HTTP handler for serving a next-based static site // This handler respects NextJS-based routing rules: From 07435034fed5969cc530a7c7cf1b97eec4d2c8d2 Mon Sep 17 00:00:00 2001 From: Bryan Phelps Date: Tue, 8 Feb 2022 04:54:36 +0000 Subject: [PATCH 36/41] Add return to reduce indentation in serveFile --- nextrouter/nextrouter.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/nextrouter/nextrouter.go b/nextrouter/nextrouter.go index d1e5138d6d43e..1c179dd21756a 100644 --- a/nextrouter/nextrouter.go +++ b/nextrouter/nextrouter.go @@ -172,11 +172,12 @@ func serveFile(router chi.Router, fileSystem fs.FS, fileName string, options Opt // Special case: '/' should serve index.html if fileName == "index.html" { router.Get("/", handler) - } else { - // Otherwise, handling the trailing slash case - - // for examples, `providers.html` should serve `/providers/` - router.Get("/"+fileNameWithoutExtension+"/", handler) + return } + + // Otherwise, handling the trailing slash case - + // for examples, `providers.html` should serve `/providers/` + router.Get("/"+fileNameWithoutExtension+"/", handler) } func serve404IfAvailable(fileSystem fs.FS, router chi.Router, options Options) { From 4585c1360bbdb20bddb3d9cdf6dcec4d27f1610c Mon Sep 17 00:00:00 2001 From: Bryan Phelps Date: Tue, 8 Feb 2022 05:00:03 +0000 Subject: [PATCH 37/41] Remove call to remove file extension --- nextrouter/nextrouter.go | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/nextrouter/nextrouter.go b/nextrouter/nextrouter.go index 1c179dd21756a..db1ed7947a953 100644 --- a/nextrouter/nextrouter.go +++ b/nextrouter/nextrouter.go @@ -200,9 +200,8 @@ func serve404IfAvailable(fileSystem fs.FS, router chi.Router, options Options) { // isDynamicRoute returns true if the file is a NextJS dynamic route, like `[orgs]` // Returns false if the file is not a dynamic route, or if it is a catch-all route (`[[...any]]`) -func isDynamicRoute(fileName string) bool { - fileWithoutExtension := removeFileExtension(fileName) - +// NOTE: The extension should be removed from the file name +func isDynamicRoute(fileWithoutExtension string) bool { // Assuming ASCII encoding - `len` in go works on bytes byteLen := len(fileWithoutExtension) if byteLen < 2 { @@ -214,8 +213,8 @@ func isDynamicRoute(fileName string) bool { // isCatchAllRoute returns true if the file is a catch-all route, like `[[...any]]` // Return false otherwise -func isCatchAllRoute(fileName string) bool { - fileWithoutExtension := removeFileExtension(fileName) +// NOTE: The extension should be removed from the file name +func isCatchAllRoute(fileWithoutExtension string) bool { ret := strings.HasPrefix(fileWithoutExtension, "[[.") return ret } From 8f2b47d6a6ae2245688fa737052043ba7287279b Mon Sep 17 00:00:00 2001 From: Bryan Phelps Date: Tue, 8 Feb 2022 05:03:26 +0000 Subject: [PATCH 38/41] Switch to register 404, return error and warn at toplevel --- nextrouter/nextrouter.go | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/nextrouter/nextrouter.go b/nextrouter/nextrouter.go index db1ed7947a953..ec5e3cd57c842 100644 --- a/nextrouter/nextrouter.go +++ b/nextrouter/nextrouter.go @@ -52,7 +52,11 @@ func Handler(fileSystem fs.FS, options *Options) http.Handler { router.NotFound(fileHandler.ServeHTTP) // Finally, if there is a 404.html available, serve that - serve404IfAvailable(fileSystem, router, *options) + err := register404(fileSystem, router, *options) + if (err != nil) { + // An error may be expected if a 404.html is not present + options.Logger.Warn(context.Background(), "Unable to find 404.html", slog.Error(err)) + } return router } @@ -180,12 +184,12 @@ func serveFile(router chi.Router, fileSystem fs.FS, fileName string, options Opt router.Get("/"+fileNameWithoutExtension+"/", handler) } -func serve404IfAvailable(fileSystem fs.FS, router chi.Router, options Options) { +func register404(fileSystem fs.FS, router chi.Router, options Options) error { // Get the file contents fileBytes, err := fs.ReadFile(fileSystem, "404.html") if err != nil { // An error is expected if the file doesn't exist - return + return err } router.NotFound(func(writer http.ResponseWriter, request *http.Request) { @@ -196,6 +200,8 @@ func serve404IfAvailable(fileSystem fs.FS, router chi.Router, options Options) { return } }) + + return nil } // isDynamicRoute returns true if the file is a NextJS dynamic route, like `[orgs]` From 19fcfac5c10f6a89545caf502c40743c5f031563 Mon Sep 17 00:00:00 2001 From: Bryan Phelps Date: Tue, 8 Feb 2022 05:08:58 +0000 Subject: [PATCH 39/41] Move ./nextrouter -> ./site/nextrouter --- site/embed.go | 2 +- {nextrouter => site/nextrouter}/nextrouter.go | 0 {nextrouter => site/nextrouter}/nextrouter_test.go | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename {nextrouter => site/nextrouter}/nextrouter.go (100%) rename {nextrouter => site/nextrouter}/nextrouter_test.go (99%) diff --git a/site/embed.go b/site/embed.go index a6f7b1ad09890..039dae74614f5 100644 --- a/site/embed.go +++ b/site/embed.go @@ -12,7 +12,7 @@ import ( "cdr.dev/slog" - "github.com/coder/coder/nextrouter" + "github.com/coder/coder/site/nextrouter" ) // The `embed` package ignores recursively including directories diff --git a/nextrouter/nextrouter.go b/site/nextrouter/nextrouter.go similarity index 100% rename from nextrouter/nextrouter.go rename to site/nextrouter/nextrouter.go diff --git a/nextrouter/nextrouter_test.go b/site/nextrouter/nextrouter_test.go similarity index 99% rename from nextrouter/nextrouter_test.go rename to site/nextrouter/nextrouter_test.go index ae3b3a7b3b46a..876b70fe54ece 100644 --- a/nextrouter/nextrouter_test.go +++ b/site/nextrouter/nextrouter_test.go @@ -12,7 +12,7 @@ import ( "cdr.dev/slog" - "github.com/coder/coder/nextrouter" + "github.com/coder/coder/site/nextrouter" ) func TestNextRouter(t *testing.T) { From 834392361144fd5b4e06a53c5fb42c5e565f7081 Mon Sep 17 00:00:00 2001 From: Bryan Phelps Date: Tue, 8 Feb 2022 05:18:46 +0000 Subject: [PATCH 40/41] Return error from handler --- site/embed.go | 7 ++++- site/nextrouter/nextrouter.go | 28 +++++++++++-------- site/nextrouter/nextrouter_test.go | 45 ++++++++++++++++++++---------- 3 files changed, 53 insertions(+), 27 deletions(-) diff --git a/site/embed.go b/site/embed.go index 039dae74614f5..eccbb26009d53 100644 --- a/site/embed.go +++ b/site/embed.go @@ -41,10 +41,15 @@ func Handler(logger slog.Logger) http.Handler { } } - nextRouterHandler := nextrouter.Handler(filesystem, &nextrouter.Options{ + nextRouterHandler, err := nextrouter.Handler(filesystem, &nextrouter.Options{ Logger: logger, TemplateDataFunc: templateFunc, }) + if err != nil { + // There was an error setting up our file system handler. + // This likely means a problem with our embedded file system. + panic(err) + } return secureHeaders(nextRouterHandler) } diff --git a/site/nextrouter/nextrouter.go b/site/nextrouter/nextrouter.go index ec5e3cd57c842..8d5f4de637471 100644 --- a/site/nextrouter/nextrouter.go +++ b/site/nextrouter/nextrouter.go @@ -33,7 +33,7 @@ type HTMLTemplateHandler func(*http.Request) interface{} // // 1) If a file is of the form `[org]`, it's a dynamic route for a single-parameter // 2) If a file is of the form `[[...any]]`, it's a dynamic route for any parameters -func Handler(fileSystem fs.FS, options *Options) http.Handler { +func Handler(fileSystem fs.FS, options *Options) (http.Handler, error) { if options == nil { options = &Options{ Logger: slog.Logger{}, @@ -43,7 +43,10 @@ func Handler(fileSystem fs.FS, options *Options) http.Handler { router := chi.NewRouter() // Build up a router that matches NextJS routing rules, for HTML files - registerRoutes(router, fileSystem, *options) + err := registerRoutes(router, fileSystem, *options) + if err != nil { + return nil, err + } // Fallback to static file server for non-HTML files // Non-HTML files don't have special routing rules, so we can just leverage @@ -52,22 +55,21 @@ func Handler(fileSystem fs.FS, options *Options) http.Handler { router.NotFound(fileHandler.ServeHTTP) // Finally, if there is a 404.html available, serve that - err := register404(fileSystem, router, *options) - if (err != nil) { + err = register404(fileSystem, router, *options) + if err != nil { // An error may be expected if a 404.html is not present options.Logger.Warn(context.Background(), "Unable to find 404.html", slog.Error(err)) } - return router + return router, nil } // registerRoutes recursively traverses the file-system, building routes // as appropriate for respecting NextJS dynamic rules. -func registerRoutes(rtr chi.Router, fileSystem fs.FS, options Options) { +func registerRoutes(rtr chi.Router, fileSystem fs.FS, options Options) error { files, err := fs.ReadDir(fileSystem, ".") if err != nil { - options.Logger.Warn(context.Background(), "Provided filesystem is empty; unable to build routes") - return + return err } // Loop through everything in the current directory... @@ -84,8 +86,7 @@ func registerRoutes(rtr chi.Router, fileSystem fs.FS, options Options) { // recursively calling `buildRouter` sub, err := fs.Sub(fileSystem, name) if err != nil { - options.Logger.Error(context.Background(), "Unable to call fs.Sub on directory", slog.F("directory_name", name)) - continue + return err } // In the special case where the folder is dynamic, @@ -98,9 +99,14 @@ func registerRoutes(rtr chi.Router, fileSystem fs.FS, options Options) { options.Logger.Debug(context.Background(), "Registering route", slog.F("name", name), slog.F("routeName", routeName)) rtr.Route("/"+routeName, func(r chi.Router) { - registerRoutes(r, sub, options) + err := registerRoutes(r, sub, options) + if err != nil { + options.Logger.Error(context.Background(), "Error registering route", slog.F("name", routeName), slog.Error(err)) + } }) } + + return nil } // serveFile is responsible for serving up HTML files in our next router diff --git a/site/nextrouter/nextrouter_test.go b/site/nextrouter/nextrouter_test.go index 876b70fe54ece..96ebf3d7ef40b 100644 --- a/site/nextrouter/nextrouter_test.go +++ b/site/nextrouter/nextrouter_test.go @@ -24,7 +24,8 @@ func TestNextRouter(t *testing.T) { err := rootFS.WriteFile("test.html", []byte("test123"), 0755) require.NoError(t, err) - router := nextrouter.Handler(rootFS, nil) + router, err := nextrouter.Handler(rootFS, nil) + require.NoError(t, err) server := httptest.NewServer(router) res, err := request(server, "/test.html") @@ -46,7 +47,8 @@ func TestNextRouter(t *testing.T) { err = rootFS.WriteFile("folder.html", []byte("folderFile"), 0755) require.NoError(t, err) - router := nextrouter.Handler(rootFS, nil) + router, err := nextrouter.Handler(rootFS, nil) + require.NoError(t, err) server := httptest.NewServer(router) res, err := request(server, "/folder/") @@ -65,7 +67,8 @@ func TestNextRouter(t *testing.T) { err := rootFS.WriteFile("test.png", []byte("png-bytes"), 0755) require.NoError(t, err) - router := nextrouter.Handler(rootFS, nil) + router, err := nextrouter.Handler(rootFS, nil) + require.NoError(t, err) server := httptest.NewServer(router) res, err := request(server, "/test.png") @@ -85,7 +88,8 @@ func TestNextRouter(t *testing.T) { err := rootFS.WriteFile("test.html", []byte("test-no-extension"), 0755) require.NoError(t, err) - router := nextrouter.Handler(rootFS, nil) + router, err := nextrouter.Handler(rootFS, nil) + require.NoError(t, err) server := httptest.NewServer(router) res, err := request(server, "/test") @@ -104,7 +108,8 @@ func TestNextRouter(t *testing.T) { err := rootFS.WriteFile("index.html", []byte("test-root-index"), 0755) require.NoError(t, err) - router := nextrouter.Handler(rootFS, nil) + router, err := nextrouter.Handler(rootFS, nil) + require.NoError(t, err) server := httptest.NewServer(router) res, err := request(server, "/") @@ -128,7 +133,8 @@ func TestNextRouter(t *testing.T) { rootFS.WriteFile("test/a/b/c.html", []byte("test123"), 0755) require.NoError(t, err) - router := nextrouter.Handler(rootFS, nil) + router, err := nextrouter.Handler(rootFS, nil) + require.NoError(t, err) server := httptest.NewServer(router) res, err := request(server, "/test/a/b/c.html") @@ -155,7 +161,8 @@ func TestNextRouter(t *testing.T) { rootFS.WriteFile("test/a/b/c/index.html", []byte("test-abc-index"), 0755) require.NoError(t, err) - router := nextrouter.Handler(rootFS, nil) + router, err := nextrouter.Handler(rootFS, nil) + require.NoError(t, err) server := httptest.NewServer(router) res, err := request(server, "/test/a/b/c") @@ -175,7 +182,8 @@ func TestNextRouter(t *testing.T) { err := rootFS.WriteFile("test.html", []byte("test123"), 0755) require.NoError(t, err) - router := nextrouter.Handler(rootFS, nil) + router, err := nextrouter.Handler(rootFS, nil) + require.NoError(t, err) server := httptest.NewServer(router) res, err := request(server, "/test-non-existent.html") @@ -191,7 +199,8 @@ func TestNextRouter(t *testing.T) { err := rootFS.WriteFile("test.html", []byte("test123"), 0755) require.NoError(t, err) - router := nextrouter.Handler(rootFS, nil) + router, err := nextrouter.Handler(rootFS, nil) + require.NoError(t, err) server := httptest.NewServer(router) res, err := request(server, "/test-non-existent.html") @@ -207,7 +216,8 @@ func TestNextRouter(t *testing.T) { err := rootFS.WriteFile("404.html", []byte("404 custom content"), 0755) require.NoError(t, err) - router := nextrouter.Handler(rootFS, nil) + router, err := nextrouter.Handler(rootFS, nil) + require.NoError(t, err) server := httptest.NewServer(router) res, err := request(server, "/test-non-existent.html") @@ -227,7 +237,8 @@ func TestNextRouter(t *testing.T) { err = rootFS.WriteFile("folder/[orgs].html", []byte("test-dynamic-path"), 0755) require.NoError(t, err) - router := nextrouter.Handler(rootFS, nil) + router, err := nextrouter.Handler(rootFS, nil) + require.NoError(t, err) server := httptest.NewServer(router) res, err := request(server, "/folder/org-1") @@ -248,7 +259,8 @@ func TestNextRouter(t *testing.T) { err = rootFS.WriteFile("folder/[org]/[project]/create.html", []byte("test-create"), 0755) require.NoError(t, err) - router := nextrouter.Handler(rootFS, nil) + router, err := nextrouter.Handler(rootFS, nil) + require.NoError(t, err) server := httptest.NewServer(router) res, err := request(server, "/folder/org-1/project-1/create") @@ -269,7 +281,8 @@ func TestNextRouter(t *testing.T) { err = rootFS.WriteFile("folder/[[...any]].html", []byte("test-catch-all"), 0755) require.NoError(t, err) - router := nextrouter.Handler(rootFS, nil) + router, err := nextrouter.Handler(rootFS, nil) + require.NoError(t, err) server := httptest.NewServer(router) res, err := request(server, "/folder/org-1/project-1/random") @@ -292,7 +305,8 @@ func TestNextRouter(t *testing.T) { err = rootFS.WriteFile("folder/create.html", []byte("test-create"), 0755) require.NoError(t, err) - router := nextrouter.Handler(rootFS, nil) + router, err := nextrouter.Handler(rootFS, nil) + require.NoError(t, err) server := httptest.NewServer(router) res, err := request(server, "/folder/create") @@ -329,10 +343,11 @@ func TestNextRouter(t *testing.T) { } } - router := nextrouter.Handler(rootFS, &nextrouter.Options{ + router, err := nextrouter.Handler(rootFS, &nextrouter.Options{ Logger: slog.Logger{}, TemplateDataFunc: templateFunc, }) + require.NoError(t, err) server := httptest.NewServer(router) res, err := request(server, "/test.html") From 4a5c35361d82e84d75e755d613a3621b15c8b522 Mon Sep 17 00:00:00 2001 From: Bryan Phelps Date: Tue, 8 Feb 2022 05:21:04 +0000 Subject: [PATCH 41/41] Change dynamic route logging from Info -> Debug --- site/nextrouter/nextrouter.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/nextrouter/nextrouter.go b/site/nextrouter/nextrouter.go index 8d5f4de637471..adf9a00cd23d4 100644 --- a/site/nextrouter/nextrouter.go +++ b/site/nextrouter/nextrouter.go @@ -168,7 +168,7 @@ func serveFile(router chi.Router, fileSystem fs.FS, fileName string, options Opt // Handle the `[org]` dynamic route case if isDynamicRoute(fileNameWithoutExtension) { - options.Logger.Info(context.Background(), "Registering dynamic route", slog.F("fileName", fileName)) + options.Logger.Debug(context.Background(), "Registering dynamic route", slog.F("fileName", fileName)) router.Get("/{dynamic}", handler) return }