Skip to content

Commit

Permalink
gateway: routable static base path (#3010)
Browse files Browse the repository at this point in the history
  • Loading branch information
mikecutalo committed May 20, 2024
1 parent 5fd778a commit 32065d4
Show file tree
Hide file tree
Showing 8 changed files with 183 additions and 52 deletions.
5 changes: 5 additions & 0 deletions api/config/gateway/v1/gateway.proto
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,11 @@ message Assets {
oneof provider {
S3Provider s3 = 1;
}

// Clutch hosts static assets on the `/static/` path, any request to this path will result in a lookup of static
// assets. However if you would like to utilize this route for other pruposes in additon to static assets, you can
// enable this feature. eg: clutch.sh/static || /static/* -> you can now handle this route as you wish.
bool routable_static_path = 2;
}

message Logger {
Expand Down
108 changes: 61 additions & 47 deletions backend/api/config/gateway/v1/gateway.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions backend/api/config/gateway/v1/gateway.pb.validate.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions backend/clutch-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ gateway:
logger:
pretty: true
level: DEBUG
assets:
routableStaticPath: true
accesslog:
# log http 5xx errors by default
status_code_filters:
Expand Down
30 changes: 25 additions & 5 deletions backend/gateway/mux/mux.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,16 @@ import (
)

const (
xHeader = "X-"
xForwardedFor = "X-Forwarded-For"
xForwardedHost = "X-Forwarded-Host"
xHeader = "X-"
xForwardedFor = "X-Forwarded-For"
xForwardedHost = "X-Forwarded-Host"
staticAssetPath = "/static/"
)

var apiPattern = regexp.MustCompile(`^/v\d+/`)
var (
apiPattern = regexp.MustCompile(`^/v\d+/`)
staticRoutePattern = regexp.MustCompile(`^/static*`)
)

type assetHandler struct {
assetCfg *gatewayv1.Assets
Expand Down Expand Up @@ -76,10 +80,17 @@ func (a *assetHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Set the original path.
r.URL.Path = origPath

// if enableStaticBaseRoute is set to true, we wont attempt to serve assets if there is no extension in the path.
// This is to prevent serving the SPA when the user is trying to access a nested route.
if a.isStaticPathRoutable(r.URL.Path) {
r.URL.Path = "/"
a.fileServer.ServeHTTP(w, r)
}

// Serve!
if f, err := a.fileSystem.Open(r.URL.Path); err != nil {
// If not a known static asset and an asset provider is configured, try streaming from the configured provider.
if a.assetCfg != nil && a.assetCfg.Provider != nil && strings.HasPrefix(r.URL.Path, "/static/") {
if a.assetCfg != nil && a.assetCfg.Provider != nil && strings.HasPrefix(r.URL.Path, staticAssetPath) {
// We attach this header simply for observability purposes.
// Otherwise its difficult to know if the assets are being served from the configured provider.
w.Header().Set("x-clutch-asset-passthrough", "true")
Expand Down Expand Up @@ -110,6 +121,15 @@ func (a *assetHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
a.fileServer.ServeHTTP(w, r)
}

func (a *assetHandler) isStaticPathRoutable(urlPath string) bool {
if a.assetCfg != nil && a.assetCfg.RoutableStaticPath {
// If the path is the base route, we need to serve the SPA.
return staticRoutePattern.MatchString(urlPath) && path.Ext(urlPath) == "" && urlPath != "/"
}

return false
}

func (a *assetHandler) assetProviderHandler(ctx context.Context, urlPath string) (io.ReadCloser, error) {
switch a.assetCfg.Provider.(type) {
case *gatewayv1.Assets_S3:
Expand Down
64 changes: 64 additions & 0 deletions backend/gateway/mux/mux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -235,3 +235,67 @@ func TestCustomResponseForwarderAuthCookiesNonBrowser(t *testing.T) {
assert.Equal(t, 200, rec.Code)
assert.Equal(t, "", rec.Header().Get("Location"))
}

func TestRoutableStaticPathEnabled(t *testing.T) {
assetHandler := &assetHandler{
assetCfg: &gatewayv1.Assets{
RoutableStaticPath: true,
},
}

testCases := []struct {
id string
urlPath string
expected bool
}{
{
id: "should not route /",
urlPath: "/",
expected: false,
},
{
id: "should route static assets",
urlPath: "/static/main.js",
expected: false,
},
{
id: "should route static assets",
urlPath: "/static/main.css",
expected: false,
},
{
id: "should serve the base route",
urlPath: "/static",
expected: true,
},
{
id: "should serve the base route with a trailing slash",
urlPath: "/static/",
expected: true,
},
{
id: "should serve the base route with longer paths",
urlPath: "/static/hello",
expected: true,
},
{
id: "should serve the base route with query params",
urlPath: "/static/hello?foo=bar",
expected: true,
},
{
id: "should not route apis",
urlPath: "/v1/getstatic/hello",
expected: false,
},
{
id: "should not route apis",
urlPath: "/v1/staticapi/hello",
expected: false,
},
}

for _, test := range testCases {
assert.Equal(t, test.expected, assetHandler.isStaticPathRoutable(test.urlPath), test.id)
}
}
6 changes: 6 additions & 0 deletions frontend/api/src/index.d.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 18 additions & 0 deletions frontend/api/src/index.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 32065d4

Please sign in to comment.