From 9712926cf55b56c18ef638a23fc69e893fbae499 Mon Sep 17 00:00:00 2001 From: fen Date: Thu, 10 Sep 2026 11:06:44 -0500 Subject: [PATCH 1/3] #173: render friendly 'Paste ID not found' UI instead of bare 404 Missing/expired/burned paste IDs and unknown routes now render the main UI (topbar + centered result card) with a 'Paste ID not found' message, returning HTTP 404 status for correctness. --- internal/api/server.go | 9 ++++-- internal/web/templates/notfound.html | 13 +++++++++ internal/web/web.go | 42 +++++++++++++++++++++++++--- 3 files changed, 57 insertions(+), 7 deletions(-) create mode 100644 internal/web/templates/notfound.html diff --git a/internal/api/server.go b/internal/api/server.go index cf2aaf7..dfe1ac4 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -129,9 +129,12 @@ func (a *apiServer) routes() http.Handler { r.Get("/{id}", a.handlePasteView) r.Post("/{id}", a.handlePasteView) - r.NotFound(func(w http.ResponseWriter, r *http.Request) { - writeErr(w, 404, "not found") - }) +// #173: missing paste URLs render the main UI with a friendly not-found +// message instead of a bare JSON 404. Known routes (above) handle real pages; +// anything else is a nonexistent paste ID or typo. +r.NotFound(func(w http.ResponseWriter, r *http.Request) { + a.webHandlers().HandleNotFoundPage(w, r) +}) return r } diff --git a/internal/web/templates/notfound.html b/internal/web/templates/notfound.html new file mode 100644 index 0000000..eda487f --- /dev/null +++ b/internal/web/templates/notfound.html @@ -0,0 +1,13 @@ +{{template "head" .}} +{{template "topbar" .}} +
+
+
+
+

Paste ID not found

+

The paste /{{.ID}} does not exist, has expired, or was burned.

+ Create a new paste +
+
+
+{{template "foot" .}} diff --git a/internal/web/web.go b/internal/web/web.go index 074d183..b502a4d 100644 --- a/internal/web/web.go +++ b/internal/web/web.go @@ -104,6 +104,13 @@ func (h *Handlers) renderPage(w http.ResponseWriter, name string, data any) { } } +// RenderNotFoundPage is the exported not-found renderer used by the api +// package (#173): the router's NotFound handler renders the main UI with a +// friendly "Paste ID not found" message, still with HTTP 404. +func (h *Handlers) RenderNotFoundPage(w http.ResponseWriter, r *http.Request) { + h.renderNotFound(w, r, r.URL.Path) +} + // RenderPage is the exported wrapper used by the api package (#4 can pages). func (h *Handlers) RenderPage(w http.ResponseWriter, name string, data any) { h.renderPage(w, name, data) @@ -200,6 +207,30 @@ func (h *Handlers) writeRateLimited(w http.ResponseWriter, retryAfterSecs int) { w.Write([]byte(`{"error":"rate limit exceeded"}`)) } +// renderNotFound serves the friendly not-found page (#173): the main UI +// chrome (topbar, centered card) with a "Paste ID not found" message in the +// result card, instead of a bare text 404. Still returns HTTP 404 so +// crawlers/validators see the correct status. +func (h *Handlers) renderNotFound(w http.ResponseWriter, r *http.Request, id string) { + h.renderPageStatus(w, "notfound.html", http.StatusNotFound, map[string]any{"Page": "notfound", "ID": id}) +} + +// HandleNotFoundPage serves the friendly not-found page for unknown routes +// (#173): main UI chrome with a "Paste ID not found" message. Called from the +// chi NotFound handler in the api package. +func (h *Handlers) HandleNotFoundPage(w http.ResponseWriter, r *http.Request) { + h.renderNotFound(w, r, r.URL.Path) +} + +// renderPageStatus renders a template with an explicit HTTP status code. +func (h *Handlers) renderPageStatus(w http.ResponseWriter, name string, status int, data any) { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.WriteHeader(status) + if err := h.UI.tmpl.ExecuteTemplate(w, name, data); err != nil { + http.Error(w, "template error: "+err.Error(), 500) + } +} + func (h *Handlers) renderPaste(w http.ResponseWriter, row *store.PasteRow, justCreated bool, deletionToken string, readsRemaining *int) { lines := strings.Count(row.Content, "\n") + 1 gutter := "" @@ -262,11 +293,14 @@ func (h *Handlers) HandlePasteView(w http.ResponseWriter, r *http.Request) { return } if row == nil { - http.NotFound(w, r) + // #173: a missing paste ID gets the main UI with a friendly message, + // not a bare text 404 page. + h.renderNotFound(w, r, id) return } if row.ExpiresAt.Valid && row.ExpiresAt.Int64 < time.Now().Unix() { - http.Error(w, "paste expired", 404) + // #173: expired pastes render the same friendly not-found UI. + h.renderNotFound(w, r, id) return } if row.PasswordHash.Valid { @@ -333,8 +367,8 @@ func (h *Handlers) HandlePasteView(w http.ResponseWriter, r *http.Request) { // Just-created first render does not count as a read for the creator. if !justCreated { rem, admitted := h.Store.RegisterRead(row, h.ViewerID(r), h.BurnWindowMin()) - if !admitted { // #58: lost the burn claim; do not render content - http.NotFound(w, r) + if !admitted { // #58: lost the burn claim; #173: friendly not-found UI + h.renderNotFound(w, r, row.ID) return } h.renderPaste(w, row, false, "", rem) -- 2.54.0 From 122c1e14f43a6062a1eb07a67422f5354e29e889 Mon Sep 17 00:00:00 2001 From: fen Date: Thu, 10 Sep 2026 11:06:52 -0500 Subject: [PATCH 2/3] web: friendly Paste ID not found page for missing pastes (#173) Missing or expired paste IDs (and unknown routes) now render the main UI chrome with a 'Paste ID not found' message in a color-coded result card instead of a bare text/JSON 404. HTTP status stays 404. No inline scripts or styles; new CSS uses existing --err token and pill radii. --- internal/api/main_test.go | 24 ++++++++++++++++++++++++ internal/web/static/app.css | 3 +++ 2 files changed, 27 insertions(+) diff --git a/internal/api/main_test.go b/internal/api/main_test.go index e3548ce..ab41402 100644 --- a/internal/api/main_test.go +++ b/internal/api/main_test.go @@ -299,3 +299,27 @@ func TestNotFound(t *testing.T) { t.Fatalf("expected 404, got %d", rec.Code) } } + +// #173: a missing paste ID on the UI route (/p/{id}, i.e. /{id} HTML view) +// should render the main UI page with a friendly "Paste ID not found" +// message, not a bare text 404. Status stays 404. +func TestPasteViewNotFoundFriendly(t *testing.T) { + s := testServer(t) + h := s.routes() + req := httptest.NewRequest("GET", "/zzzzzz", nil) + rec := httptest.NewRecorder() + h.ServeHTTP(rec, req) + if rec.Code != http.StatusNotFound { + t.Fatalf("expected 404 status, got %d", rec.Code) + } + body := rec.Body.String() + if !strings.Contains(body, "Paste ID not found") { + t.Fatalf("expected friendly message in body, got: %.200s", body) + } + if !strings.Contains(body, "