package httputil import ( "context" "fmt" "net/http" "net/url" "github.com/pomerium/pomerium/internal/log" "github.com/pomerium/pomerium/pkg/contextutil" "github.com/pomerium/pomerium/pkg/telemetry/requestid" "github.com/pomerium/pomerium/ui" ) // HTTPError contains an HTTP status code and wrapped error. type HTTPError struct { // HTTP status codes as registered with IANA. Status int // Err is the wrapped error. Err error Description string // DebugURL is the URL to the debug endpoint. DebugURL *url.URL // The request ID. RequestID string BrandingOptions BrandingOptions } // NewError returns an error that contains a HTTP status and error. func NewError(status int, err error) *HTTPError { return &HTTPError{Status: status, Err: err} } // Error implements the `error` interface. func (e *HTTPError) Error() string { str := StatusText(e.Status) if e.Err != nil { str += ": " + e.Err.Error() } return str } // Unwrap implements the `error` Unwrap interface. func (e *HTTPError) Unwrap() error { return e.Err } // ErrorResponse replies to the request with the specified error message and HTTP code. // It does not otherwise end the request; the caller should ensure no further // writes are done to w. func (e *HTTPError) ErrorResponse(ctx context.Context, w http.ResponseWriter, r *http.Request) { reqID := e.RequestID if e.RequestID == "" { // if empty, try to grab from the request id from the request context reqID = requestid.FromContext(r.Context()) } response := struct { Status int StatusText string `json:"-"` Description string `json:"description,omitempty"` RequestID string `json:",omitempty"` CanDebug bool `json:"-"` DebugURL *url.URL `json:",omitempty"` PolicyEvaluationTraces []contextutil.PolicyEvaluationTrace `json:",omitempty"` }{ Status: e.Status, StatusText: StatusText(e.Status), Description: e.Description, RequestID: reqID, CanDebug: e.Status/100 == 4 && (e.DebugURL != nil || reqID != ""), DebugURL: e.DebugURL, PolicyEvaluationTraces: contextutil.GetPolicyEvaluationTraces(ctx), } // indicate to clients that the error originates from Pomerium, not the app w.Header().Set(HeaderPomeriumResponse, "true") if e.Status >= 400 { log.Ctx(ctx).Error(). Err(e.Err). Int("status", e.Status). Str("status-text", StatusText(e.Status)). Str("request-id", reqID). Msg("httputil: error") } if r.Header.Get("Accept") == "application/json" { RenderJSON(w, e.Status, response) return } m := map[string]any{ "canDebug": response.CanDebug, "description": response.Description, "requestId": response.RequestID, "status": response.Status, "statusText": response.StatusText, "policyEvaluationTraces": response.PolicyEvaluationTraces, } if response.DebugURL != nil { m["debugUrl"] = response.DebugURL.String() } AddBrandingOptionsToMap(m, e.BrandingOptions) w.Header().Set("Content-Type", "text/html; charset=UTF-8") w.WriteHeader(response.Status) if err := ui.ServePage(w, r, "Error", fmt.Sprintf("%d %s", response.Status, response.StatusText), m); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } // WithDescription sets the description in the HTTP error. func (e *HTTPError) WithDescription(description string) *HTTPError { e.Description = description return e }