diff --git a/proxy/internal/accesslog/middleware.go b/proxy/internal/accesslog/middleware.go
index 706de7d3c..b5d1c0e9d 100644
--- a/proxy/internal/accesslog/middleware.go
+++ b/proxy/internal/accesslog/middleware.go
@@ -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,
diff --git a/proxy/internal/proxy/context.go b/proxy/internal/proxy/context.go
index 36da03d30..4131a4ae6 100644
--- a/proxy/internal/proxy/context.go
+++ b/proxy/internal/proxy/context.go
@@ -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()
diff --git a/proxy/internal/proxy/reverseproxy.go b/proxy/internal/proxy/reverseproxy.go
index d64073941..da692cd16 100644
--- a/proxy/internal/proxy/reverseproxy.go
+++ b/proxy/internal/proxy/reverseproxy.go
@@ -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}
}
}
diff --git a/proxy/web/dist/index.html b/proxy/web/dist/index.html
index a57ba7ea5..e69a2787e 100644
--- a/proxy/web/dist/index.html
+++ b/proxy/web/dist/index.html
@@ -6,8 +6,8 @@
NetBird Service
-
-
+
+
diff --git a/proxy/web/public/robots.txt b/proxy/web/public/robots.txt
new file mode 100644
index 000000000..1f53798bb
--- /dev/null
+++ b/proxy/web/public/robots.txt
@@ -0,0 +1,2 @@
+User-agent: *
+Disallow: /
diff --git a/proxy/web/src/ErrorPage.tsx b/proxy/web/src/ErrorPage.tsx
new file mode 100644
index 000000000..c335e2ae1
--- /dev/null
+++ b/proxy/web/src/ErrorPage.tsx
@@ -0,0 +1,66 @@
+import { useEffect, useState } from "react";
+import {BookText, RotateCw, Server, Globe, UserIcon, WaypointsIcon} from "lucide-react";
+import { Title } from "@/components/Title";
+import { Description } from "@/components/Description";
+import Button from "@/components/Button";
+import { PoweredByNetBird } from "@/components/PoweredByNetBird";
+import { StatusCard } from "@/components/StatusCard";
+import type { ErrorData } from "@/data";
+
+export function ErrorPage({ code, title, message, proxy = true, peer = true, destination = true, requestId }: ErrorData) {
+ useEffect(() => {
+ document.title = `${title} - NetBird Service`;
+ }, [title]);
+
+ const [timestamp] = useState(() => new Date().toISOString());
+
+ return (
+
+ {/* Error Code */}
+