Skip to content

Commit fe7c839

Browse files
authored
fix: Refactor static file server handler and logger (#8)
Refactor the code in staticFileServerHandler.go and logger.go to improve code organization and readability. The changes include: - Move the logger related functions to a separate file, logger.go, for better separation of concerns. - Rename the StaticFilesHandler function to NewStaticFilesHandler to follow the Go convention for constructor functions. - Extract the ServeHTTP method from the StaticFilesHandler function and move it to the StaticFilesHandler struct to improve encapsulation. - Clean up the code by removing unused imports and variables. This commit improves the maintainability and readability of the codebase.
1 parent b5d1de7 commit fe7c839

File tree

3 files changed

+93
-78
lines changed

3 files changed

+93
-78
lines changed

logger.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package spaserve
2+
3+
import (
4+
"context"
5+
"log/slog"
6+
)
7+
8+
type servespaLogger struct {
9+
logger *slog.Logger
10+
}
11+
12+
// newLogger creates a new logger function with the given context and logger.
13+
func newLogger(logger *slog.Logger) *servespaLogger {
14+
return &servespaLogger{logger: logger}
15+
}
16+
17+
func (l servespaLogger) logContext(ctx context.Context, level slog.Level, msg string, attrs ...slog.Attr) {
18+
if l.logger == nil {
19+
return
20+
}
21+
l.logger.LogAttrs(ctx, level, msg, attrs...)
22+
}

staticFileServerHandler.go

Lines changed: 50 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package spaserve
22

33
import (
4-
"context"
54
"errors"
65
"io/fs"
76
"log/slog"
@@ -13,6 +12,14 @@ import (
1312
"github.com/psanford/memfs"
1413
)
1514

15+
type StaticFilesHandler struct {
16+
opts staticFilesHandlerOpts
17+
fileServer http.Handler
18+
mfilesys *memfs.FS
19+
logger *servespaLogger
20+
muxErrHandler func(int, http.ResponseWriter, *http.Request)
21+
}
22+
1623
type staticFilesHandlerOpts struct {
1724
ns string
1825
basePath string
@@ -92,16 +99,13 @@ func WithInjectWebEnv(env any, namespace string) staticFilesHandlerFunc {
9299
// - ctx: the context
93100
// - filesys: the file system to serve files from - this will be copied to a memfs
94101
// - fn: optional functions to configure the handler (e.g. WithLogger, WithBasePath, WithMuxErrorHandler, WithInjectWebEnv)
95-
func StaticFilesHandler(ctx context.Context, filesys fs.FS, fn ...staticFilesHandlerFunc) (http.Handler, error) {
102+
func NewStaticFilesHandler(filesys fs.FS, fn ...staticFilesHandlerFunc) (http.Handler, error) {
96103
// process options
97104
opts := defaultStaticFilesHandlerOpts
98105
for _, f := range fn {
99106
opts = f(opts)
100107
}
101108

102-
logWithAttrs := newLoggerWithContext(ctx, opts.logger)
103-
muxErrHandler := newMuxErrorHandler(opts.muxErrHandler)
104-
105109
var (
106110
mfilesys *memfs.FS
107111
err error
@@ -118,89 +122,69 @@ func StaticFilesHandler(ctx context.Context, filesys fs.FS, fn ...staticFilesHan
118122

119123
// create file server
120124
fileServer := http.FileServer(http.FS(mfilesys))
125+
logger := newLogger(opts.logger)
126+
127+
return &StaticFilesHandler{
128+
opts: opts,
129+
mfilesys: mfilesys,
130+
fileServer: fileServer,
131+
logger: logger,
132+
muxErrHandler: newMuxErrorHandler(opts.muxErrHandler),
133+
}, nil
134+
}
121135

122-
// return handler
123-
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
124-
// serve index.html for root path
125-
if r.URL.Path == "/" {
126-
fileServer.ServeHTTP(w, r)
127-
return
128-
}
136+
func (h *StaticFilesHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
137+
ctx := r.Context()
129138

130-
// warn if base path might be wrong
131-
if len(opts.basePath) > 0 && r.URL.Path[:len(opts.basePath)] != opts.basePath {
132-
logWithAttrs(slog.LevelInfo, "WARNING: base path may not be set correctly",
133-
slog.Attr{Key: "reqPath", Value: slog.StringValue(r.URL.Path)},
134-
slog.Attr{Key: "basePath", Value: slog.StringValue(opts.basePath)},
135-
)
136-
}
139+
// clean path for security and consistency
140+
cleanedPath := path.Clean(r.URL.Path)
141+
cleanedPath = strings.TrimPrefix(cleanedPath, h.opts.basePath)
142+
cleanedPath = strings.TrimPrefix(cleanedPath, "/")
143+
cleanedPath = strings.TrimSuffix(cleanedPath, "/")
137144

138-
// clean path for security and consistency
139-
cleanedPath := path.Clean(r.URL.Path)
140-
cleanedPath = strings.TrimPrefix(cleanedPath, opts.basePath)
145+
h.logger.logContext(ctx, slog.LevelDebug, "request", slog.Attr{Key: "cleanedPath", Value: slog.StringValue(cleanedPath)})
141146

142-
// open file
143-
file, err := mfilesys.Open(cleanedPath)
144-
if file != nil {
145-
defer file.Close()
146-
}
147+
// reconstitute the path
148+
r.URL.Path = "/" + cleanedPath
147149

148-
// ensure leading slash
149-
r.URL.Path = cleanedPath
150-
if r.URL.Path[0] != '/' {
151-
r.URL.Path = "/" + r.URL.Path
152-
}
153-
154-
// if index.html is requested, rewrite to avoid 301 redirect
155-
if r.URL.Path == "/index.html" {
156-
r.URL.Path = "/"
157-
}
150+
// use root path for index.html
151+
if r.URL.Path == "index.html" {
152+
r.URL.Path = "/"
153+
}
158154

155+
// handle non-root paths
156+
if r.URL.Path != "/" {
157+
// open file
158+
file, err := h.mfilesys.Open(cleanedPath)
159+
isErr := err != nil
159160
isErrNotExist := errors.Is(err, os.ErrNotExist)
160161
isFile := path.Ext(cleanedPath) != ""
161-
162-
// if err != nil {
163-
// fmt.Printf("error: %v\n", err)
164-
// fmt.Printf("request path: %s\n", r.URL.Path)
165-
// fmt.Printf("cleaned path: %s\n", cleanedPath)
166-
// fs.WalkDir(mfilesys, ".", func(path string, d fs.DirEntry, err error) error {
167-
// fmt.Printf("path: %s, d: %v, err: %v\n", path, d, err)
168-
// return nil
169-
// })
170-
// }
162+
if file != nil {
163+
file.Close()
164+
}
171165

172166
// return 500 for other errors
173-
if err != nil && !isErrNotExist {
174-
logWithAttrs(slog.LevelError, "could not open file", slog.Attr{Key: "cleanedPath", Value: slog.StringValue(cleanedPath)})
175-
muxErrHandler(http.StatusInternalServerError, w, r)
167+
if isErr && !isErrNotExist {
168+
h.logger.logContext(ctx, slog.LevelError, "could not open file", slog.Attr{Key: "cleanedPath", Value: slog.StringValue(cleanedPath)})
169+
h.muxErrHandler(http.StatusInternalServerError, w, r)
176170
return
177171
}
178172

179173
// return 404 for actual static file requests that don't exist
180-
if err != nil && isErrNotExist && isFile {
181-
logWithAttrs(slog.LevelError, "could not find file", slog.Attr{Key: "cleanedPath", Value: slog.StringValue(cleanedPath)})
182-
muxErrHandler(http.StatusNotFound, w, r)
174+
if isErrNotExist && isFile {
175+
h.logger.logContext(ctx, slog.LevelDebug, "not found, static file", slog.Attr{Key: "cleanedPath", Value: slog.StringValue(cleanedPath)})
176+
h.muxErrHandler(http.StatusNotFound, w, r)
183177
return
184178
}
185179

186180
// serve index.html and let SPA handle undefined routes
187181
if isErrNotExist {
188-
logWithAttrs(slog.LevelDebug, "not found, serve index", slog.Attr{Key: "cleanedPath", Value: slog.StringValue(cleanedPath)})
182+
h.logger.logContext(ctx, slog.LevelDebug, "not found, serve index", slog.Attr{Key: "cleanedPath", Value: slog.StringValue(cleanedPath)})
189183
r.URL.Path = "/"
190184
}
191-
192-
fileServer.ServeHTTP(w, r)
193-
}), nil
194-
}
195-
196-
// newLoggerWithContext creates a new logger function with the given context and logger.
197-
func newLoggerWithContext(ctx context.Context, logger *slog.Logger) func(slog.Level, string, ...slog.Attr) {
198-
return func(level slog.Level, msg string, attrs ...slog.Attr) {
199-
if logger == nil {
200-
return
201-
}
202-
logger.LogAttrs(ctx, level, msg, attrs...)
203185
}
186+
187+
h.fileServer.ServeHTTP(w, r)
204188
}
205189

206190
// newMuxErrorHandler creates a new error handler function with the given muxErrHandler.

staticFileServerHandler_test.go

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package spaserve
22

33
import (
44
"bytes"
5-
"context"
65
"fmt"
76
"log/slog"
87
"net/http"
@@ -14,11 +13,10 @@ import (
1413
)
1514

1615
func TestStaticFilesHandler(t *testing.T) {
17-
ctx := context.TODO()
1816
filesys := os.DirFS(path.Join("testdata", "files"))
1917

2018
t.Run("ServeIndexHTML", func(t *testing.T) {
21-
handler, err := StaticFilesHandler(ctx, filesys)
19+
handler, err := NewStaticFilesHandler(filesys)
2220
if err != nil {
2321
t.Errorf("Unexpected error: %v", err)
2422
}
@@ -31,10 +29,19 @@ func TestStaticFilesHandler(t *testing.T) {
3129
if w.Code != http.StatusOK {
3230
t.Errorf("Expected status code %d, but got %d", http.StatusOK, w.Code)
3331
}
32+
33+
req = httptest.NewRequest(http.MethodGet, "/index.html", nil)
34+
w = httptest.NewRecorder()
35+
handler.ServeHTTP(w, req)
36+
37+
// Assert that index.html is served
38+
if w.Code != http.StatusMovedPermanently {
39+
t.Errorf("Expected status code %d, but got %d", http.StatusMovedPermanently, w.Code)
40+
}
3441
})
3542

3643
t.Run("ServeExistingFile", func(t *testing.T) {
37-
handler, err := StaticFilesHandler(ctx, filesys)
44+
handler, err := NewStaticFilesHandler(filesys)
3845
if err != nil {
3946
t.Errorf("Unexpected error: %v", err)
4047
}
@@ -50,7 +57,7 @@ func TestStaticFilesHandler(t *testing.T) {
5057
})
5158

5259
t.Run("ServeNonExistingFile", func(t *testing.T) {
53-
handler, err := StaticFilesHandler(ctx, filesys)
60+
handler, err := NewStaticFilesHandler(filesys)
5461
if err != nil {
5562
t.Errorf("Unexpected error: %v", err)
5663
}
@@ -66,7 +73,7 @@ func TestStaticFilesHandler(t *testing.T) {
6673
})
6774

6875
t.Run("ServeIndexOnNonExistingFile", func(t *testing.T) {
69-
handler, err := StaticFilesHandler(ctx, filesys)
76+
handler, err := NewStaticFilesHandler(filesys)
7077
if err != nil {
7178
t.Errorf("Unexpected error: %v", err)
7279
}
@@ -135,7 +142,7 @@ func TestStaticFilesHandler(t *testing.T) {
135142
}
136143
}
137144

138-
handler, err := StaticFilesHandler(ctx, filesys, WithBasePath("/static"))
145+
handler, err := NewStaticFilesHandler(filesys, WithBasePath("/static"))
139146
if err != nil {
140147
t.Errorf("Unexpected error: %v", err)
141148
}
@@ -152,7 +159,9 @@ func TestStaticFilesHandler(t *testing.T) {
152159

153160
t.Run("Serve with Logger", func(t *testing.T) {
154161
bufout := new(bytes.Buffer)
155-
logger := slog.New(slog.NewJSONHandler(bufout, &slog.HandlerOptions{}))
162+
logger := slog.New(slog.NewJSONHandler(bufout, &slog.HandlerOptions{
163+
Level: slog.LevelDebug,
164+
}))
156165

157166
// Test WithLogger function
158167
wo := WithLogger(logger)
@@ -162,7 +171,7 @@ func TestStaticFilesHandler(t *testing.T) {
162171
}
163172

164173
// Call the StaticFilesHandler function with the logger
165-
handler, err := StaticFilesHandler(ctx, filesys, wo)
174+
handler, err := NewStaticFilesHandler(filesys, wo)
166175
if err != nil {
167176
t.Errorf("Unexpected error: %v", err)
168177
}
@@ -204,7 +213,7 @@ func TestStaticFilesHandler(t *testing.T) {
204213
t.Error("Expected mux error handler to be set, but got nil")
205214
}
206215

207-
handler, err := StaticFilesHandler(ctx, filesys, WithMuxErrorHandler(customErrorHandler))
216+
handler, err := NewStaticFilesHandler(filesys, WithMuxErrorHandler(customErrorHandler))
208217
if err != nil {
209218
t.Errorf("Unexpected error: %v", err)
210219
}
@@ -271,12 +280,12 @@ func TestStaticFilesHandler(t *testing.T) {
271280
}
272281

273282
// Call the StaticFilesHandler function with the web environment
274-
handler, err := StaticFilesHandler(ctx, filesys, WithInjectWebEnv(env, namespace))
283+
handler, err := NewStaticFilesHandler(filesys, WithInjectWebEnv(env, namespace))
275284
if err != nil {
276285
t.Errorf("Unexpected error: %v", err)
277286
}
278287

279-
req := httptest.NewRequest(http.MethodGet, "/index.html", nil)
288+
req := httptest.NewRequest(http.MethodGet, "/", nil)
280289
w := httptest.NewRecorder()
281290
handler.ServeHTTP(w, req)
282291

0 commit comments

Comments
 (0)