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:
@@ -234,8 +234,60 @@ func ExampleAPIDocsProvider() {
|
||||
// 200 {"openapi":"3.0.3","info":{"title":"Blog API","version":"1"},"paths":{"/api/blog/posts/{id}":{"get":{"summary":"Show a post"}}}}
|
||||
}
|
||||
|
||||
// themePages is the acme.theme error page.
|
||||
type themePages struct{}
|
||||
|
||||
// RenderErrorPage returns the theme's own 404 and keeps the built-in page
|
||||
// for every other status.
|
||||
func (themePages) RenderErrorPage(r *http.Request, page surf.ErrorPage) ([]byte, bool) {
|
||||
if page.Status != http.StatusNotFound {
|
||||
return nil, false // keep the built-in page
|
||||
}
|
||||
body := `<!doctype html><title>Not found</title><h1>Nothing here</h1><a href="` + page.Home + `">Home</a>`
|
||||
return []byte(body), true
|
||||
}
|
||||
|
||||
// ThemePlugin is the acme.theme plugin; Register publishes its error page.
|
||||
type ThemePlugin struct{}
|
||||
|
||||
func (ThemePlugin) ID() string { return "acme.theme" }
|
||||
func (ThemePlugin) Requires() []string { return nil }
|
||||
func (ThemePlugin) Boot(*backpack.App) error { return nil }
|
||||
|
||||
// Register publishes the theme's error page for surf to look up.
|
||||
func (ThemePlugin) Register(app *backpack.App) error {
|
||||
return app.Publish[surf.ErrorPageRenderer](themePages{})
|
||||
}
|
||||
|
||||
func ExampleErrorPageRenderer() {
|
||||
app := backpack.New(nil)
|
||||
plugin := ThemePlugin{}
|
||||
if err := plugin.Register(app); err != nil { // the runtime calls Register
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
h, err := surf.Assemble(app, []party.Plugin{plugin})
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
browser := httptest.NewRequest("GET", "/nowhere", nil)
|
||||
browser.Header.Set("Accept", "text/html,application/xhtml+xml,*/*;q=0.8")
|
||||
browser.Header.Set("Sec-Fetch-Mode", "navigate")
|
||||
curl := httptest.NewRequest("GET", "/nowhere", nil)
|
||||
curl.Header.Set("Accept", "*/*")
|
||||
for _, req := range []*http.Request{browser, curl} {
|
||||
rec := httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, req)
|
||||
fmt.Println(rec.Code, rec.Header().Get("Content-Type"), strings.TrimSpace(rec.Body.String()))
|
||||
}
|
||||
// Output:
|
||||
// 404 text/html; charset=utf-8 <!doctype html><title>Not found</title><h1>Nothing here</h1><a href="/">Home</a>
|
||||
// 404 text/plain; charset=utf-8 404 page not found
|
||||
}
|
||||
|
||||
// TestDocsDeclarations checks the plugin declarations the Routing and Rate
|
||||
// limiting pages show.
|
||||
// limiting pages show, the error-page override included.
|
||||
func TestDocsDeclarations(t *testing.T) {
|
||||
p := &BlogPlugin{}
|
||||
if _, ok := p.Middlewares()["acme.auth"]; !ok {
|
||||
@@ -247,4 +299,11 @@ func TestDocsDeclarations(t *testing.T) {
|
||||
if spec, err := p.APIDocsSpec(); err != nil || !strings.HasPrefix(string(spec), "openapi:") {
|
||||
t.Fatalf("APIDocsSpec() = %q, %v", spec, err)
|
||||
}
|
||||
req := httptest.NewRequest("GET", "/nowhere", nil)
|
||||
if body, ok := (themePages{}).RenderErrorPage(req, surf.ErrorPage{Status: http.StatusNotFound, Home: "/"}); !ok || !strings.Contains(string(body), `href="/"`) {
|
||||
t.Fatalf("RenderErrorPage(404) = %q, %v", body, ok)
|
||||
}
|
||||
if _, ok := (themePages{}).RenderErrorPage(req, surf.ErrorPage{Status: http.StatusInternalServerError}); ok {
|
||||
t.Fatal("RenderErrorPage(500) should keep the built-in page")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user