Preserve identical booking refusals with canonical themed 404
Co-Authored-By: Codex <noreply@openai.com>
3 files changed,
+38
−7
internal/calendar/handlers_booking_util.go+7 −3internal/calendar/routes.go+1 −4internal/calendartest/notfound_test.go+30 −0
CI passed — run details
diff --git a/internal/calendar/handlers_booking_util.go b/internal/calendar/handlers_booking_util.go| index 11ac92d..9e196ac 100644 |
| --- a/internal/calendar/handlers_booking_util.go |
| +++ b/internal/calendar/handlers_booking_util.go |
| @@ -3,6 +3,7 @@ package calendar |
| import ( |
| "net" |
| "net/http" |
| + "net/url" |
| "strconv" |
| "strings" |
| "sync" |
| @@ -53,10 +54,13 @@ func publicHeaders(w http.ResponseWriter) { |
| } |
| // notFoundLikeTheRouter is the SAME 404 an unknown route gets, so a |
| -// wrong token and a wrong path are indistinguishable (B10). chi's |
| -// default NotFound is http.NotFound. |
| +// wrong token and a wrong path are indistinguishable (B10). |
| func notFoundLikeTheRouter(w http.ResponseWriter, r *http.Request) { |
| - http.NotFound(w, r) |
| + page := r.Clone(r.Context()) |
| + page.URL = &url.URL{Path: "/"} |
| + page.RequestURI = "/" |
| + w.Header().Set("Content-Type", "text/html; charset=utf-8") |
| + ErrorPage(w, page, http.StatusNotFound, "") |
| } |
| // zoneChoices is CommonZones plus the host's own zone when it is not |
diff --git a/internal/calendar/routes.go b/internal/calendar/routes.go| index 2c60129..3095c7c 100644 |
| --- a/internal/calendar/routes.go |
| +++ b/internal/calendar/routes.go |
| @@ -26,10 +26,7 @@ func (a *App) routes(h *handlers, authPlugin *rauth.Auth, list allowlist, origin |
| // covered. Origin-checking, not tokens — nothing to mint or forget |
| // in a form. |
| r.Use(csrf.Protect(origin)) |
| - r.NotFound(func(w http.ResponseWriter, r *http.Request) { |
| - w.Header().Set("Content-Type", "text/html; charset=utf-8") |
| - ErrorPage(w, r, http.StatusNotFound, "") |
| - }) |
| + r.NotFound(notFoundLikeTheRouter) |
| // Signed-out. |
| r.Get("/signin", h.signin) |
diff --git a/internal/calendartest/notfound_test.go b/internal/calendartest/notfound_test.go| index ca17d51..82fa574 100644 |
| --- a/internal/calendartest/notfound_test.go |
| +++ b/internal/calendartest/notfound_test.go |
| @@ -51,3 +51,33 @@ func TestNotFoundLeavesMatchedRoutesAlone(t *testing.T) { |
| } |
| } |
| } |
| + |
| +func TestThemedNotFoundKeepsBookingFailuresIdentical(t *testing.T) { |
| + h := newApp(t).Handler() |
| + ordinary := get(t, h, "/unknown-path-sentinel?query-sentinel=1") |
| + for _, path := range []string{"/book/unknown-host-sentinel/unknown-code-sentinel?query-sentinel=1", "/book/m/wrong-token-sentinel?query-sentinel=1"} { |
| + r := get(t, h, path) |
| + if r.Code != http.StatusNotFound || r.Body.String() != ordinary.Body.String() { |
| + t.Errorf("booking refusal differs from ordinary404: status %d", r.Code) |
| + } |
| + if r.Header().Get("Cache-Control") != "no-store" || r.Header().Get("Referrer-Policy") != "no-referrer" { |
| + t.Error("public capability headers changed") |
| + } |
| + if r.Header().Get("Content-Type") != "text/html; charset=utf-8" { |
| + t.Error("booking refusal lacks HTML type") |
| + } |
| + } |
| + if strings.Contains(ordinary.Body.String(), "sentinel") { |
| + t.Error("request coordinate reflected") |
| + } |
| + if !strings.Contains(ordinary.Body.String(), `name="return_to" value="/"`) { |
| + t.Error("mode return is not canonical") |
| + } |
| + req := httptest.NewRequest(http.MethodGet, "/unknown-path-sentinel?query-sentinel=1", nil) |
| + req.URL.RawPath = "/unknown%2Dpath-sentinel" |
| + originalURL, originalURI := *req.URL, req.RequestURI |
| + h.ServeHTTP(httptest.NewRecorder(), req) |
| + if *req.URL != originalURL || req.RequestURI != originalURI { |
| + t.Error("render mutated caller request") |
| + } |
| +} |