errors.go

 1package httperr
 2
 3import (
 4	"net/http"
 5	"net/url"
 6
 7	"git.kilimanjaro.io/rtw/pkg/log"
 8	"git.kilimanjaro.io/rtw/pkg/middleware"
 9)
10
11type Handler struct {
12	onError        string
13	onUnauthorized string
14	onNotFound     string
15}
16
17type HandlerOption func(*Handler)
18
19// NewHandler creates a new Handler with defaults of /error, /login, and /404 unless overridden by options.
20func NewHandler(opts ...HandlerOption) *Handler {
21	eh := &Handler{
22		onError:        "/error",
23		onUnauthorized: "/login",
24		onNotFound:     "/404",
25	}
26	for _, opt := range opts {
27		opt(eh)
28	}
29	return eh
30}
31
32// WithOnError sets the redirect path for error errors.
33func WithOnError(onError string) HandlerOption {
34	return func(eh *Handler) {
35		eh.onError = onError
36	}
37}
38
39// WithOnUnauthorized sets the redirect path for unauthorized errors.
40func WithOnUnauthorized(onUnauthorized string) HandlerOption {
41	return func(eh *Handler) {
42		eh.onUnauthorized = onUnauthorized
43	}
44}
45
46// WithOnNotFound sets the redirect path for not found errors.
47func WithOnNotFound(onNotFound string) HandlerOption {
48	return func(eh *Handler) {
49		eh.onNotFound = onNotFound
50	}
51}
52
53func (eh *Handler) Handle(status int, w http.ResponseWriter, r *http.Request, errs ...error) {
54	l := log.FromContext(r.Context())
55	for _, err := range errs {
56		l.Error(err.Error(), "status", status)
57	}
58	// check if a wrapped response writer and status is still pending
59	ww, ok := w.(middleware.WrapResponseWriter)
60	if ok && ww.Status() == 0 {
61		return
62	}
63	// prevent accidental footguns
64	if status >= 200 && status < 300 {
65		return
66	}
67	switch status {
68	case http.StatusUnauthorized, http.StatusForbidden:
69		http.Redirect(w, r, eh.onUnauthorized, http.StatusFound)
70	case http.StatusNotFound:
71		http.Redirect(w, r, eh.onNotFound, http.StatusFound)
72	default:
73		http.Redirect(w, r, eh.onError, http.StatusFound)
74	}
75}
76
77func (eh *Handler) Redirect(path string, w http.ResponseWriter, r *http.Request, query ...string) {
78	if (len(query)%2) != 0 || len(query) == 0 {
79		if r.Header.Get("HX-Request") == "true" {
80			w.Header().Set("HX-Redirect", path)
81			w.WriteHeader(http.StatusOK)
82			return
83		}
84		http.Redirect(w, r, path, http.StatusFound)
85		return
86	}
87	v := url.Values{}
88	for i := 0; i < len(query); i += 2 {
89		v.Set(query[i], query[i+1])
90	}
91	if r.Header.Get("HX-Request") == "true" {
92		w.Header().Set("HX-Redirect", path+"?"+v.Encode())
93		w.WriteHeader(http.StatusOK)
94		return
95	}
96	http.Redirect(w, r, path+"?"+v.Encode(), http.StatusFound)
97}