Add better error page

This commit is contained in:
Eduard Gert
2026-02-05 14:00:51 +01:00
parent 9b0387e7ee
commit 7504e718d7
12 changed files with 217 additions and 63 deletions

View File

@@ -24,10 +24,13 @@ func (l *Logger) Middleware(next http.Handler) http.Handler {
// headers that we wish to use to gather that information on the request.
sourceIp := extractSourceIP(r)
// Generate request ID early so it can be used by error pages.
requestID := xid.New().String()
// Create a mutable struct to capture data from downstream handlers.
// We pass a pointer in the context - the pointer itself flows down immutably,
// but the struct it points to can be mutated by inner handlers.
capturedData := &proxy.CapturedData{}
capturedData := &proxy.CapturedData{RequestID: requestID}
ctx := proxy.WithCapturedData(r.Context(), capturedData)
start := time.Now()
@@ -41,7 +44,7 @@ func (l *Logger) Middleware(next http.Handler) http.Handler {
}
entry := logEntry{
ID: xid.New().String(),
ID: requestID,
ServiceId: capturedData.GetServiceId(),
AccountID: string(capturedData.GetAccountId()),
Host: host,

View File

@@ -19,10 +19,18 @@ const (
// to pass data back up the middleware chain.
type CapturedData struct {
mu sync.RWMutex
RequestID string
ServiceId string
AccountId types.AccountID
}
// GetRequestID safely gets the request ID
func (c *CapturedData) GetRequestID() string {
c.mu.RLock()
defer c.mu.RUnlock()
return c.RequestID
}
// SetServiceId safely sets the service ID
func (c *CapturedData) SetServiceId(serviceId string) {
c.mu.Lock()

View File

@@ -41,8 +41,10 @@ func NewReverseProxy(transport http.RoundTripper, logger *log.Logger) *ReversePr
func (p *ReverseProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
target, serviceId, accountID, exists := p.findTargetForRequest(r)
if !exists {
requestID := getRequestID(r)
web.ServeErrorPage(w, r, http.StatusNotFound, "Service Not Found",
"The requested service could not be found. Please check the URL, try refreshing, or check if the peer is running. If that doesn't work, see our documentation for help.")
"The requested service could not be found. Please check the URL, try refreshing, or check if the peer is running. If that doesn't work, see our documentation for help.",
requestID, web.ErrorStatus{Proxy: true, Peer: false, Destination: false})
return
}
@@ -71,37 +73,51 @@ func (p *ReverseProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// proxyErrorHandler handles errors from the reverse proxy and serves
// user-friendly error pages instead of raw error responses.
func proxyErrorHandler(w http.ResponseWriter, r *http.Request, err error) {
title, message, code := classifyProxyError(err)
web.ServeErrorPage(w, r, code, title, message)
requestID := getRequestID(r)
title, message, code, status := classifyProxyError(err)
web.ServeErrorPage(w, r, code, title, message, requestID, status)
}
// classifyProxyError determines the appropriate error title, message, and HTTP
// status code based on the error type.
func classifyProxyError(err error) (title, message string, code int) {
// getRequestID retrieves the request ID from context or returns empty string.
func getRequestID(r *http.Request) string {
if capturedData := CapturedDataFromContext(r.Context()); capturedData != nil {
return capturedData.GetRequestID()
}
return ""
}
// classifyProxyError determines the appropriate error title, message, HTTP
// status code, and component status based on the error type.
func classifyProxyError(err error) (title, message string, code int, status web.ErrorStatus) {
switch {
case errors.Is(err, context.DeadlineExceeded):
return "Request Timeout",
"The request timed out while trying to reach the service. Please refresh the page and try again.",
http.StatusGatewayTimeout
http.StatusGatewayTimeout,
web.ErrorStatus{Proxy: true, Peer: true, Destination: false}
case errors.Is(err, context.Canceled):
return "Request Canceled",
"The request was canceled before it could be completed. Please refresh the page and try again.",
http.StatusBadGateway
http.StatusBadGateway,
web.ErrorStatus{Proxy: true, Peer: true, Destination: false}
case errors.Is(err, roundtrip.ErrNoAccountID):
return "Configuration Error",
"The request could not be processed due to a configuration issue. Please refresh the page and try again.",
http.StatusInternalServerError
http.StatusInternalServerError,
web.ErrorStatus{Proxy: false, Peer: false, Destination: false}
case strings.Contains(err.Error(), "connection refused"):
return "Service Unavailable",
"The connection to the service was refused. Please verify that the service is running and try again.",
http.StatusBadGateway
http.StatusBadGateway,
web.ErrorStatus{Proxy: true, Peer: true, Destination: false}
default:
return "Peer Not Connected",
"The connection to the peer could not be established. Please ensure the peer is running and connected to the NetBird network.",
http.StatusBadGateway
http.StatusBadGateway,
web.ErrorStatus{Proxy: true, Peer: false, Destination: false}
}
}