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/auth.go +45−1
| @@ -95,6 +95,34 @@ func (w *Web) principal(r *http.Request) *principal { | |||
| 95 | 95 | return p | |
| 96 | 96 | } | |
| 97 | 97 | ||
| 98 | // statusWriter records whether the response has started. A recover can still | ||
| 99 | // answer with the styled 500 page only while the header is unsent; once bytes | ||
| 100 | // are on the wire the connection is the only thing left to close. | ||
| 101 | type statusWriter struct { | ||
| 102 | http.ResponseWriter | ||
| 103 | wroteHeader bool | ||
| 104 | } | ||
| 105 | |||
| 106 | func (s *statusWriter) WriteHeader(code int) { | ||
| 107 | s.wroteHeader = true | ||
| 108 | s.ResponseWriter.WriteHeader(code) | ||
| 109 | } | ||
| 110 | |||
| 111 | func (s *statusWriter) Write(b []byte) (int, error) { | ||
| 112 | s.wroteHeader = true | ||
| 113 | return s.ResponseWriter.Write(b) | ||
| 114 | } | ||
| 115 | |||
| 116 | // Flush keeps streaming handlers (git's side-band progress) working through | ||
| 117 | // the wrapper. | ||
| 118 | func (s *statusWriter) Flush() { | ||
| 119 | if f, ok := s.ResponseWriter.(http.Flusher); ok { | ||
| 120 | f.Flush() | ||
| 121 | } | ||
| 122 | } | ||
| 123 | |||
| 124 | func (s *statusWriter) Unwrap() http.ResponseWriter { return s.ResponseWriter } | ||
| 125 | |||
| 98 | 126 | // Middleware resolves the session cookie once per request and sets the browser | |
| 99 | 127 | // security headers every response carries. An invalid or expired cookie is | |
| 100 | 128 | // cleared rather than left to be retried on every page. | |
| @@ -123,7 +151,23 @@ func (w *Web) Middleware(next http.Handler) http.Handler { | |||
| 123 | 151 | } | |
| 124 | 152 | } | |
| 125 | 153 | } | |
| 126 | next.ServeHTTP(rw, r) | ||
| 154 | sw := &statusWriter{ResponseWriter: rw} | ||
| 155 | defer func() { | ||
| 156 | v := recover() | ||
| 157 | if v == nil { | ||
| 158 | return | ||
| 159 | } | ||
| 160 | if w.Logger != nil { | ||
| 161 | w.Logger("panic %s %s: %v", r.Method, r.URL.Path, v) | ||
| 162 | } | ||
| 163 | if sw.wroteHeader { | ||
| 164 | // The reply is already partial; let net/http close the connection | ||
| 165 | // rather than append a page to an archive or a git stream. | ||
| 166 | panic(v) | ||
| 167 | } | ||
| 168 | w.serverError(sw, r, "") | ||
| 169 | }() | ||
| 170 | next.ServeHTTP(sw, r) | ||
| 127 | 171 | }) | |
| 128 | 172 | } | |
| 129 | 173 | ||