Skip to content

Commit

Permalink
Implement HTTP early hint support for static files
Browse files Browse the repository at this point in the history
  • Loading branch information
akclace committed Dec 21, 2023
1 parent 47afb7b commit b4a872a
Show file tree
Hide file tree
Showing 8 changed files with 87 additions and 13 deletions.
28 changes: 28 additions & 0 deletions internal/app/load_config.go → internal/app/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,27 @@ func getRequestUrl(r *http.Request) string {
}
}

func (a *App) earlyHints(w http.ResponseWriter, r *http.Request) {
sendHint := false
a.Info().Msgf("Sending early hints for %s", a.sourceFS.StaticFiles())
for _, f := range a.sourceFS.StaticFiles() {
if strings.HasSuffix(f, ".css") {
sendHint = true
w.Header().Add("Link", fmt.Sprintf("<%s>; rel=preload; as=style",
path.Join(a.Path, a.sourceFS.HashName(f))))
} else if strings.HasSuffix(f, ".js") {
sendHint = true
w.Header().Add("Link", fmt.Sprintf("<%s>; rel=preload; as=script",
path.Join(a.Path, a.sourceFS.HashName(f))))
}
}

if sendHint {
a.Trace().Msg("Sending early hints for static files")
w.WriteHeader(http.StatusEarlyHints)
}
}

func (a *App) createHandlerFunc(html, block string, handler starlark.Callable, rtype string) http.HandlerFunc {
goHandler := func(w http.ResponseWriter, r *http.Request) {
thread := &starlark.Thread{
Expand All @@ -313,6 +334,13 @@ func (a *App) createHandlerFunc(html, block string, handler starlark.Callable, r
}

isHtmxRequest := r.Header.Get("HX-Request") == "true" && !(r.Header.Get("HX-Boosted") == "true")

if !a.IsDev && r.Method == http.MethodGet && r.Header.Get("sec-fetch-mode") == "navigate" &&
!(strings.ToLower(rtype) == "json") && !(isHtmxRequest && block != "") {
// Prod mode, for a GET request from newer browsers on a top level HTML page, send http early hints
a.earlyHints(w, r)
}

appPath := a.Path
if appPath == "/" {
appPath = ""
Expand Down
11 changes: 9 additions & 2 deletions internal/app/testhelper.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,13 @@ func CreateTestAppInt(logger *utils.Logger, path string, fileData map[string]str
} else {
fs = &TestReadFS{fileData: fileData}
}
sourceFS := util.NewSourceFs("", fs, isDev)
sourceFS, err := util.NewSourceFs("", fs, isDev)
if err != nil {
return nil, nil, err
}
workFS := util.NewWorkFs("", &TestWriteFS{TestReadFS: &TestReadFS{fileData: map[string]string{}}})
a := NewApp(sourceFS, workFS, logger, createTestAppEntry(path, isDev), &systemConfig)
err := a.Initialize()
err = a.Initialize()
return a, workFS, err
}

Expand Down Expand Up @@ -157,6 +160,10 @@ func (f *TestReadFS) Stat(name string) (fs.FileInfo, error) {
return &TestFileInfo{file}, nil
}

func (d *TestReadFS) StaticFiles() []string {
return []string{} // Not implemented for disk fs, used only in prod mode
}

func (f *TestReadFS) Reset() {
// do nothing
}
Expand Down
4 changes: 4 additions & 0 deletions internal/app/util/disk_fs.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@ func (d *DiskReadFS) Glob(pattern string) (matches []string, err error) {
return fs.Glob(d.fs, pattern)
}

func (d *DiskReadFS) StaticFiles() []string {
return []string{} // Not implemented for disk fs, used only in prod mode
}

func (d *DiskReadFS) Reset() {
// do nothing
}
Expand Down
28 changes: 20 additions & 8 deletions internal/app/util/source_fs.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,10 @@ type SourceFs struct {
Root string
isDev bool

mu sync.RWMutex
nameToHash map[string]string // lookup (path to hash path)
hashToName map[string][2]string // reverse lookup (hash path to path)
staticFiles []string
mu sync.RWMutex
nameToHash map[string]string // lookup (path to hash path)
hashToName map[string][2]string // reverse lookup (hash path to path)
}

var _ utils.ReadableFS = (*SourceFs)(nil)
Expand Down Expand Up @@ -64,16 +65,27 @@ func (w *WritableSourceFs) Remove(name string) error {
return wfs.Remove(name)
}

func NewSourceFs(dir string, fs utils.ReadableFS, isDev bool) *SourceFs {
func NewSourceFs(dir string, fs utils.ReadableFS, isDev bool) (*SourceFs, error) {
var staticFiles []string
if !isDev {
// For prod mode, get the list of static files for early hints
staticFiles = fs.StaticFiles()
}

return &SourceFs{
Root: dir,
ReadableFS: fs,
isDev: isDev,
Root: dir,
ReadableFS: fs,
isDev: isDev,
staticFiles: staticFiles,

// File hashing code based on https://github.com/benbjohnson/hashfs/blob/main/hashfs.go
// Copyright (c) 2020 Ben Johnson. MIT License
nameToHash: make(map[string]string),
hashToName: make(map[string][2]string)}
hashToName: make(map[string][2]string)}, nil
}

func (f *SourceFs) StaticFiles() []string {
return f.staticFiles
}

func (f *SourceFs) ClearCache() {
Expand Down
11 changes: 11 additions & 0 deletions internal/metadata/dbfs.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"io"
"io/fs"
"path"
"strings"
"time"

"github.com/andybalholm/brotli"
Expand Down Expand Up @@ -214,6 +215,16 @@ func (d *DbFs) Glob(pattern string) (matches []string, err error) {
return matchedFiles, nil
}

func (d *DbFs) StaticFiles() []string {
staticFiles := []string{}
for name := range d.fileInfo {
if strings.HasPrefix(name, "static/") {
staticFiles = append(staticFiles, name)
}
}
return staticFiles
}

func (d *DbFs) Reset() {
d.fileStore.Reset()
}
11 changes: 9 additions & 2 deletions internal/server/app_apis.go
Original file line number Diff line number Diff line change
Expand Up @@ -198,12 +198,19 @@ func (s *Server) setupApp(appEntry *utils.AppEntry, tx metadata.Transaction) (*a
if err != nil {
return nil, err
}
sourceFS = util.NewSourceFs("", dbFs, false)
sourceFS, err = util.NewSourceFs("", dbFs, false)
if err != nil {
return nil, err
}
} else {
// Dev mode, use local disk as source
sourceFS = util.NewSourceFs(appEntry.SourceUrl,
var err error
sourceFS, err = util.NewSourceFs(appEntry.SourceUrl,
&util.DiskWriteFS{DiskReadFS: util.NewDiskReadFS(&appLogger, appEntry.SourceUrl)},
appEntry.IsDev)
if err != nil {
return nil, err
}
}

appPath := fmt.Sprintf(os.ExpandEnv("$CL_HOME/run/app/%s"), appEntry.Id)
Expand Down
3 changes: 2 additions & 1 deletion internal/utils/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,8 @@ type ReadableFS interface {
fs.GlobFS
// Stat returns the stats for the named file.
Stat(name string) (fs.FileInfo, error)
Reset() // Used to reset the file system transaction for the DbFs, no-op for others
Reset() // Used to reset the file system transaction for the DbFs, no-op for others
StaticFiles() []string // Return list of static files
}

type CompressedReader interface {
Expand Down
4 changes: 4 additions & 0 deletions tests/test_basics.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@ tests:
exactly: "1" # X-Clace-Compressed is not set, encoding is not set
exit-code: 0

basic164: ## Check HTTP early hints
command: 'curl -kv -H "sec-fetch-mode: navigate" —http2 https://localhost:9155/test2?aaaa'
stderr: "HTTP/2 103"

basic170: # check curl works with password
command: curl -sS -u "admin:abcd" localhost:9154/test2
stdout: "Test app body"
Expand Down

0 comments on commit b4a872a

Please sign in to comment.