diff options
Diffstat (limited to 'echo/error_handler.go')
-rw-r--r-- | echo/error_handler.go | 252 |
1 files changed, 155 insertions, 97 deletions
diff --git a/echo/error_handler.go b/echo/error_handler.go index 2316ccd..b44155f 100644 --- a/echo/error_handler.go +++ b/echo/error_handler.go | |||
@@ -4,135 +4,193 @@ import ( | |||
4 | "bytes" | 4 | "bytes" |
5 | "fmt" | 5 | "fmt" |
6 | "html/template" | 6 | "html/template" |
7 | "io/fs" | ||
8 | "net/http" | 7 | "net/http" |
9 | 8 | ||
10 | "github.com/elnormous/contenttype" | 9 | "github.com/elnormous/contenttype" |
11 | "github.com/labstack/echo/v4" | 10 | "github.com/labstack/echo/v4" |
12 | ) | 11 | ) |
13 | 12 | ||
14 | // TODO: This should allow plugging in other content types | 13 | // TODO: This should have some kind of HTML formatting of panics in debug mode |
15 | // TODO: This should also be refactored into something prettier | ||
16 | func ErrorHandler(templates fs.FS, funcs template.FuncMap) func(error, echo.Context) { | ||
17 | handleHtml := func(c echo.Context, he *echo.HTTPError) error { | ||
18 | t, err := template.New("").Funcs(funcs).ParseFS( | ||
19 | templates, | ||
20 | "404.tpl", | ||
21 | "40x.tpl", | ||
22 | "50x.tpl", | ||
23 | "header.tpl", | ||
24 | "footer.tpl", | ||
25 | ) | ||
26 | if err != nil { | ||
27 | return err | ||
28 | } | ||
29 | 14 | ||
30 | path := "50x.tpl" | 15 | type ContentErrorHandler interface { |
31 | if he.Code == 404 { | 16 | Handle(echo.Context, *echo.HTTPError) error |
32 | path = "404.tpl" | 17 | } |
33 | } else if he.Code >= 400 && he.Code <= 499 { | ||
34 | path = "40x.tpl" | ||
35 | } | ||
36 | 18 | ||
37 | buf := bytes.Buffer{} | 19 | type HTMLErrorHandler struct { |
38 | if err = t.ExecuteTemplate(&buf, path, nil); err != nil { | 20 | r *TemplateRenderer |
39 | err = c.String(he.Code, fmt.Sprintf("%s", he.Message)) | 21 | fallback *template.Template |
40 | } | 22 | } |
41 | 23 | ||
42 | return c.HTMLBlob(he.Code, buf.Bytes()) | 24 | func NewHTMLErrorHandler(r *TemplateRenderer) *HTMLErrorHandler { |
25 | t, err := template.New("").Parse("<html><body><h1>Error</h1><pre>{{ .Message }}</pre></body></html>\n") | ||
26 | if err != nil { | ||
27 | panic("NewHTMLErrorHandler: error parsing fallback template") | ||
43 | } | 28 | } |
29 | return &HTMLErrorHandler{r: r, fallback: t} | ||
30 | } | ||
44 | 31 | ||
45 | handlePlain := func(c echo.Context, he *echo.HTTPError) error { | 32 | func (h *HTMLErrorHandler) Handle(c echo.Context, e *echo.HTTPError) error { |
46 | return c.String(he.Code, fmt.Sprintf("%s", he.Message)) | 33 | path := "50x.tpl" |
34 | if e.Code == 404 { | ||
35 | path = "404.tpl" | ||
36 | } else if e.Code >= 400 && e.Code <= 499 { | ||
37 | path = "40x.tpl" | ||
47 | } | 38 | } |
48 | 39 | ||
49 | handleJson := func(c echo.Context, he *echo.HTTPError) error { | 40 | buf := bytes.Buffer{} |
50 | code := he.Code | 41 | if !h.r.HaveTemplate(path) { |
51 | message := he.Message | 42 | c.Logger().Errorf("Error template %s is missing, using fallback", path) |
52 | if m, ok := he.Message.(string); ok { | ||
53 | if c.Echo().Debug { | ||
54 | message = echo.Map{"message": m, "error": he.Error()} | ||
55 | } else { | ||
56 | message = echo.Map{"message": m} | ||
57 | } | ||
58 | } | ||
59 | 43 | ||
60 | return c.JSON(code, message) | 44 | if err := h.fallback.Execute(&buf, e); err != nil { |
45 | c.Logger().Errorf("Error rendering HTML error page: %s", err) | ||
46 | return c.String(e.Code, e.Error()) | ||
47 | } | ||
48 | } else { | ||
49 | if err := h.r.Render(&buf, path, e, c); err != nil { | ||
50 | c.Logger().Errorf("Error rendering HTML error page: %s", err) | ||
51 | return c.String(e.Code, e.Error()) | ||
52 | } | ||
61 | } | 53 | } |
62 | 54 | ||
63 | errorWhileErroring := func(c echo.Context, err interface{}) { | 55 | return c.HTMLBlob(e.Code, buf.Bytes()) |
64 | c.Echo().Logger.Error(err) | 56 | } |
57 | |||
58 | type PlainTextErrorHandler struct{} | ||
59 | |||
60 | func (h *PlainTextErrorHandler) Handle(c echo.Context, e *echo.HTTPError) error { | ||
61 | return c.String(e.Code, fmt.Sprintf("%s", e.Message)) | ||
62 | } | ||
63 | |||
64 | type JSONErrorHandler struct{} | ||
65 | |||
66 | func (h *JSONErrorHandler) Handle(c echo.Context, e *echo.HTTPError) error { | ||
67 | code := e.Code | ||
68 | message := e.Message | ||
69 | if m, ok := e.Message.(string); ok { | ||
65 | if c.Echo().Debug { | 70 | if c.Echo().Debug { |
66 | c.JSON(http.StatusInternalServerError, &echo.HTTPError{ | 71 | message = echo.Map{"message": m, "error": e.Error()} |
67 | Code: http.StatusInternalServerError, | ||
68 | Message: fmt.Sprintf("Error while processing error page. %s", err), | ||
69 | }) | ||
70 | } else { | 72 | } else { |
71 | c.JSON(http.StatusInternalServerError, &echo.HTTPError{ | 73 | message = echo.Map{"message": m} |
72 | Code: http.StatusInternalServerError, | ||
73 | Message: http.StatusText(http.StatusInternalServerError), | ||
74 | }) | ||
75 | } | 74 | } |
76 | } | 75 | } |
77 | 76 | ||
78 | handlers := map[string]func(echo.Context, *echo.HTTPError) error{ | 77 | return c.JSON(code, message) |
79 | "text/plain": handlePlain, | 78 | } |
80 | "text/html": handleHtml, | 79 | |
81 | "text/json": handleJson, | 80 | type failsafeHandler struct{} |
82 | "application/json": handleJson, | 81 | |
82 | func (h *failsafeHandler) Handle(c echo.Context, e *echo.HTTPError) error { | ||
83 | c.Echo().Logger.Error("Error while processing error page: %s", e) | ||
84 | c.JSON(http.StatusInternalServerError, e) | ||
85 | return nil | ||
86 | } | ||
87 | |||
88 | type ErrorHandler struct { | ||
89 | types []contenttype.MediaType | ||
90 | typeMap map[string]ContentErrorHandler | ||
91 | failsafeHandler ContentErrorHandler | ||
92 | } | ||
93 | |||
94 | func NewErrorHandler() *ErrorHandler { | ||
95 | return &ErrorHandler{ | ||
96 | types: []contenttype.MediaType{}, | ||
97 | typeMap: map[string]ContentErrorHandler{}, | ||
98 | failsafeHandler: &failsafeHandler{}, | ||
83 | } | 99 | } |
100 | } | ||
101 | |||
102 | func NewDefaultErrorHandler(tr *TemplateRenderer) *ErrorHandler { | ||
103 | h := NewErrorHandler() | ||
104 | |||
105 | // The order of these is important, especially when negotiating */* | ||
106 | h.AddHandler(&JSONErrorHandler{}, "text/json", "application/json") | ||
107 | h.AddHandler(&PlainTextErrorHandler{}, "text/plain") | ||
108 | h.AddHandler(NewHTMLErrorHandler(tr), "text/html") | ||
109 | |||
110 | return h | ||
111 | } | ||
112 | |||
113 | func NewNoHTMLErrorHandler() *ErrorHandler { | ||
114 | h := NewErrorHandler() | ||
115 | |||
116 | // The order of these is important, especially when negotiating */* | ||
117 | h.AddHandler(&JSONErrorHandler{}, "text/json", "application/json") | ||
118 | h.AddHandler(&PlainTextErrorHandler{}, "text/plain") | ||
119 | |||
120 | return h | ||
121 | } | ||
84 | 122 | ||
85 | // This is hand maintained here because order is important for negotiation, | 123 | func (h *ErrorHandler) AddHandler(eh ContentErrorHandler, contentTypes ...string) { |
86 | // especially in the case of */* | 124 | for _, ct := range contentTypes { |
87 | hIndex := []contenttype.MediaType{ | 125 | h.types = append(h.types, contenttype.NewMediaType(ct)) |
88 | contenttype.NewMediaType("text/json"), | 126 | h.typeMap[ct] = eh |
89 | contenttype.NewMediaType("application/json"), | ||
90 | contenttype.NewMediaType("text/plain"), | ||
91 | contenttype.NewMediaType("text/html"), | ||
92 | } | 127 | } |
128 | } | ||
93 | 129 | ||
94 | return func(err error, c echo.Context) { | 130 | func (h *ErrorHandler) castToHttpError(e error) *echo.HTTPError { |
95 | defer func() { | 131 | he, ok := e.(*echo.HTTPError) |
96 | if r := recover(); r != nil { | 132 | if ok { |
97 | errorWhileErroring(c, r) | 133 | if he.Internal != nil { |
98 | } | 134 | if ihe, ok := he.Internal.(*echo.HTTPError); ok { |
99 | }() | 135 | return ihe |
100 | |||
101 | he, ok := err.(*echo.HTTPError) | ||
102 | if ok { | ||
103 | if he.Internal != nil { | ||
104 | if herr, ok := he.Internal.(*echo.HTTPError); ok { | ||
105 | he = herr | ||
106 | } | ||
107 | } | ||
108 | } else { | ||
109 | he = &echo.HTTPError{ | ||
110 | Code: http.StatusInternalServerError, | ||
111 | Message: http.StatusText(http.StatusInternalServerError), | ||
112 | } | 136 | } |
113 | } | 137 | } |
114 | 138 | return he | |
115 | ct, _, err := contenttype.GetAcceptableMediaType(c.Request(), hIndex) | 139 | } else { |
116 | if err != nil { | 140 | return &echo.HTTPError{ |
117 | c.Echo().Logger.Error("Error negotiating content type in error handler, using json") | 141 | Code: http.StatusInternalServerError, |
118 | ct = contenttype.NewMediaType("text/json") | 142 | Message: http.StatusText(http.StatusInternalServerError), |
119 | } | 143 | } |
144 | } | ||
145 | } | ||
120 | 146 | ||
121 | handle, ok := handlers[ct.String()] | 147 | func (h *ErrorHandler) negotiateContentType(r *http.Request) (ContentErrorHandler, error) { |
122 | if !ok { | 148 | var err error |
123 | c.Echo().Logger.Errorf("Error handler content type %s is unknown", ct.String()) | ||
124 | handle = handleJson | ||
125 | } | ||
126 | 149 | ||
127 | if !c.Response().Committed { | 150 | ct, _, err := contenttype.GetAcceptableMediaType(r, h.types) |
128 | if c.Request().Method == http.MethodHead { // Issue #608 | 151 | if err != nil { |
129 | err = c.NoContent(he.Code) | 152 | err = fmt.Errorf("Error negotiating content type in error handler, falling back to JSON: %w", err) |
153 | ct = contenttype.NewMediaType("text/json") | ||
154 | } | ||
155 | |||
156 | handle, found := h.typeMap[ct.String()] | ||
157 | if !found { | ||
158 | err = fmt.Errorf("Unable to find handler for content type: %s", ct) | ||
159 | handle = h.failsafeHandler | ||
160 | } | ||
161 | |||
162 | return handle, err | ||
163 | } | ||
164 | |||
165 | func (h *ErrorHandler) HandleError(err error, c echo.Context) { | ||
166 | defer func() { | ||
167 | if r := recover(); r != nil { | ||
168 | if err, ok := r.(error); !ok { | ||
169 | h.failsafeHandler.Handle(c, h.castToHttpError(fmt.Errorf("%s", err))) | ||
130 | } else { | 170 | } else { |
131 | err = handle(c, he) | 171 | h.failsafeHandler.Handle(c, h.castToHttpError(err)) |
132 | } | ||
133 | if err != nil { | ||
134 | errorWhileErroring(c, err) | ||
135 | } | 172 | } |
136 | } | 173 | } |
174 | }() | ||
175 | |||
176 | logger := c.Echo().Logger | ||
177 | he := h.castToHttpError(err) | ||
178 | |||
179 | handle, cterr := h.negotiateContentType(c.Request()) | ||
180 | if cterr != nil { | ||
181 | logger.Errorf("%s", cterr) | ||
182 | } | ||
183 | |||
184 | if !c.Response().Committed { | ||
185 | if c.Request().Method == http.MethodHead { // Issue #608 | ||
186 | err = c.NoContent(he.Code) | ||
187 | } else { | ||
188 | err = handle.Handle(c, he) | ||
189 | } | ||
190 | if err != nil { | ||
191 | h.failsafeHandler.Handle(c, h.castToHttpError(err)) | ||
192 | } | ||
193 | } else { | ||
194 | logger.Errorf("Error occurred in committed response: %s", he) | ||
137 | } | 195 | } |
138 | } | 196 | } |