feat(surf): branded HTML error pages for browser navigations

A browser navigation (Sec-Fetch-Mode navigate, or GET/HEAD without it; no
X-Requested-With; Accept naming text/html above every JSON type; not /api,
/_<name> or the admin prefix) that hits a framework-generated 404, 405 or
500 gets a self-contained SummerCMS page: unmatched routes and wrong
methods at the ServeMux, failed Where/WhereIn constraints, overlap-family
misses, the unpublished uploads route and recovered panics. Every other
request keeps the exact stdlib text, wire's opaque JSON 500 or the bare
raw-group 500.

Details (method, path, X-Request-Id) only with app.debug or outside
production. Applications replace the page with a published
surf.ErrorPageRenderer; http.error_pages.enabled false turns it off.
This commit is contained in:
2026-10-20 03:12:25 +02:00
parent 7c2b7fcb79
commit b1236cbfd4
14 changed files with 813 additions and 28 deletions

View File

@@ -66,7 +66,7 @@ func mountUploads(r *Router, app *backpack.App) error {
return fmt.Errorf("surf: route %s %s (plugin %q) is under the uploads prefix storage.uploads.public_path_prefix %s, which storage.uploads.serve mounts", rt.method, rt.path, rt.pluginID, prefix)
}
}
handler := uploadsHandler(app, prefix)
handler := uploadsHandler(app, r.pages, prefix)
r.BindPlugin(uploadsPluginID)
r.GroupRaw(prefix, nil, func(g pact.Router) {
g.Get("/{path...}", handler)
@@ -103,18 +103,19 @@ func uploadsPrefix(raw string) (string, error) {
}
// uploadsHandler resolves the published bucket and database on every request
// and answers 404 while either is missing.
func uploadsHandler(app *backpack.App, prefix string) http.HandlerFunc {
// and answers 404 (the error page for a browser navigation) while either is
// missing.
func uploadsHandler(app *backpack.App, pages *errorPages, prefix string) http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
serveUploads(w, req, http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
bucket, ok := app.Lookup[*blob.Bucket]()
if !ok || bucket == nil {
http.NotFound(w, req)
pages.notFound(w, req)
return
}
gdb, ok := app.Lookup[*gorm.DB]()
if !ok || gdb == nil {
http.NotFound(w, req)
pages.notFound(w, req)
return
}
attach.StaticHandlerPublic(bucket, prefix, gdb).ServeHTTP(w, req)