Ctrl K
ComunidadGuías…

backend/server/server_frontend_routes.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package server

import (
	"io/fs"
	"net/http"
	"strings"

	"github.com/labstack/echo/v5"
	"github.com/labstack/echo/v5/middleware"
)

// registerFrontendRoutes wires up static file serving for the embedded frontend
// against the given filesystem (typically the embedded dist tree). It is extracted
// from embedFrontend so the behavior can be exercised in tests with an in-memory fs.FS.
//
// Layout:
//   - Global static middleware serves the SPA shell (index.html) with HTML5 fallback
//     for client-side routes, but skips /assets/* so the dedicated route below wins.
//   - /assets/* is served by a real route that returns a true 404 for missing files
//     (no HTML5 fallback) and attaches long-cache + gzip middleware.
func registerFrontendRoutes(e *echo.Echo, distFS fs.FS) {
	e.Use(middleware.StaticWithConfig(middleware.StaticConfig{
		Skipper: func(c *echo.Context) bool {
			return defaultAPIRequestSkipper(c) || strings.HasPrefix(c.Request().URL.Path, "/assets/")
		},
		HTML5:      true,
		Filesystem: distFS,
	}))

	assetsFS, err := fs.Sub(distFS, "assets")
	if err != nil {
		panic(err)
	}
	cacheImmutable := func(next echo.HandlerFunc) echo.HandlerFunc {
		return func(c *echo.Context) error {
			// Mark an asset immutable only once the response is known to be a
			// success. In an HA rolling deploy an older replica can 404 a new
			// hashed asset briefly, and the static handler rejects a path with a
			// dot or empty segment outright, so a year of cached "does not exist"
			// has to be impossible. Reading the status rather than re-deriving the
			// file from the path also leaves echo's path rules to echo: deciding
			// from a cleaned path is what put the header on those 404s.
			//
			// 304 keeps the directive so a revalidating CDN does not shorten its
			// TTL; WriteHeader sets Status before it runs these hooks.
			res, err := echo.UnwrapResponse(c.Response())
			if err != nil {
				// Without echo's response the status is not observable, and guessing
				// from the path is exactly what put immutable on those 404s. Serve
				// the asset uncached instead.
				return next(c)
			}
			res.Before(func() {
				if res.Status == http.StatusOK || res.Status == http.StatusNotModified {
					res.Header().Set(echo.HeaderCacheControl, "public, max-age=31536000, immutable")
				}
			})
			return next(c)
		}
	}
	// Register GET + HEAD so the dedicated asset route matches HEAD requests too
	// (curl -I, CDN health checks, cache revalidation). Before this fix, the global
	// StaticWithConfig middleware served any method; a GET-only route would 405 on HEAD.
	e.Match([]string{http.MethodGet, http.MethodHead}, "/assets/*",
		echo.StaticDirectoryHandler(assetsFS, false),
		middleware.GzipWithConfig(middleware.GzipConfig{Level: 5}),
		cacheImmutable,
	)
}

// defaultAPIRequestSkipper is echo skipper for api requests. /bytebase.v1 covers
// Connect-go gRPC handlers so unknown gRPC paths return a structured 404 instead
// of falling through to the SPA's HTML5 fallback (which would return index.html).
func defaultAPIRequestSkipper(c *echo.Context) bool {
	path := c.Request().URL.Path
	return hasPrefixes(path, "/api", "/v1", "/bytebase.v1", "/.well-known", webhookAPIPrefix)
}

func hasPrefixes(src string, prefixes ...string) bool {
	for _, prefix := range prefixes {
		if strings.HasPrefix(src, prefix) {
			return true
		}
	}
	return false
}