Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/git/vole.git

Write over SSH with a registered key:

git clone ssh://git@0.0.0.0:2222/vole.git

A free and open-source forge that lives in your Git repo.

Render the styled error page for every miss and failure

e84a66f5b53c269d8b0c207d0335b537e1646cb3 · Keenan Nemetz · 15d ago · parent f6f4b1db

Render the styled error page for every miss and failure

Handler-level 404s now use the same view as the catch-all, including unknown
detail resources and download paths. Internal 500s render the shared error
layout and log the detail instead of exposing it. A panic before the first
byte is recovered into that page; a partial response is left to net/http.

Also mark label/assignee triage and inline review comments done in the UI plan,
where the slice order was stale.

server/web/errorpage_test.go +100

@@ -0,0 +1,100 @@
1 package web
2
3 import (
4 "fmt"
5 "net/http"
6 "net/http/httptest"
7 "strings"
8 "testing"
9
10 "github.com/rodent-software/vole/forge/config"
11 )
12
13 func newTestWeb(t *testing.T) *Web {
14 t.Helper()
15 w, err := New(nil, nil, nil, nil, &config.Config{})
16 if err != nil {
17 t.Fatalf("new web: %v", err)
18 }
19 return w
20 }
21
22 func TestNotFoundPageIsStyled(t *testing.T) {
23 w := newTestWeb(t)
24 rw := httptest.NewRecorder()
25 w.notFound(rw, httptest.NewRequest("GET", "/r/example/issues/nope", nil))
26
27 if rw.Code != http.StatusNotFound {
28 t.Fatalf("status = %d, want 404", rw.Code)
29 }
30 body := rw.Body.String()
31 for _, want := range []string{"empty-page", "Not found", "Back to repositories"} {
32 if !strings.Contains(body, want) {
33 t.Fatalf("404 page does not show %q:\n%s", want, body)
34 }
35 }
36 }
37
38 func TestServerErrorPageIsStyledAndHidesTheDetail(t *testing.T) {
39 w := newTestWeb(t)
40 var logged string
41 w.Logger = func(format string, args ...any) {
42 logged = fmt.Sprintf(format, args...)
43 }
44
45 rw := httptest.NewRecorder()
46 w.serverError(rw, httptest.NewRequest("GET", "/r/example/issues", nil), "store at /srv/data failed")
47
48 if rw.Code != http.StatusInternalServerError {
49 t.Fatalf("status = %d, want 500", rw.Code)
50 }
51 if got := rw.Header().Get("Content-Type"); !strings.Contains(got, "text/html") {
52 t.Fatalf("content type = %q, want HTML", got)
53 }
54 body := rw.Body.String()
55 for _, want := range []string{"empty-page", "Server error", "Back to repositories"} {
56 if !strings.Contains(body, want) {
57 t.Fatalf("500 page does not show %q:\n%s", want, body)
58 }
59 }
60 if strings.Contains(body, "/srv/data") {
61 t.Fatalf("500 page leaked the internal detail:\n%s", body)
62 }
63 if !strings.Contains(logged, "/srv/data") || !strings.Contains(logged, "/r/example/issues") {
64 t.Fatalf("error detail was not logged with the request: %q", logged)
65 }
66 }
67
68 func TestMiddlewareTurnsAPanicIntoTheServerErrorPage(t *testing.T) {
69 w := newTestWeb(t)
70 w.Logger = func(string, ...any) {}
71
72 h := w.Middleware(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
73 panic("kaboom")
74 }))
75 rw := httptest.NewRecorder()
76 h.ServeHTTP(rw, httptest.NewRequest("GET", "/", nil))
77
78 if rw.Code != http.StatusInternalServerError {
79 t.Fatalf("status = %d, want 500", rw.Code)
80 }
81 if body := rw.Body.String(); !strings.Contains(body, "Server error") || !strings.Contains(body, "empty-page") {
82 t.Fatalf("panic did not render the styled 500 page:\n%s", body)
83 }
84 }
85
86 func TestMiddlewareDoesNotRewriteAPartialResponse(t *testing.T) {
87 w := newTestWeb(t)
88 w.Logger = func(string, ...any) {}
89
90 h := w.Middleware(http.HandlerFunc(func(rw http.ResponseWriter, _ *http.Request) {
91 rw.WriteHeader(http.StatusOK)
92 panic("too late")
93 }))
94 defer func() {
95 if recover() == nil {
96 t.Fatal("panic after the header was written should be left to net/http")
97 }
98 }()
99 h.ServeHTTP(httptest.NewRecorder(), httptest.NewRequest("GET", "/", nil))
100 }