extract http error handling
3 files changed, 95 insertions(+), 68 deletions(-)
M internal/http/error.go → internal/http/error.go
@@ -1,7 +1,20 @@ package http +import ( + "fmt" + "net/http" +) + type Error struct { - Error error + Code int Message string - Code int + Cause error +} + +func (e *Error) Error() string { + if e.Message == "" { + e.Message = http.StatusText(e.Code) + } + + return fmt.Sprintf("%d %s", e.Code, e.Message) }
A internal/http/mux.go
@@ -0,0 +1,43 @@ +package http + +import ( + "net/http" + + "go.alanpearce.eu/x/log" +) + +type WebHandler func(http.ResponseWriter, *http.Request) *Error +type ErrorHandler func(*Error, http.ResponseWriter, *http.Request) + +type ServeMux struct { + log *log.Logger + errorHandler ErrorHandler + *http.ServeMux +} + +func NewServeMux() *ServeMux { + return &ServeMux{ + ServeMux: http.NewServeMux(), + errorHandler: func(err *Error, w http.ResponseWriter, _ *http.Request) { + http.Error(w, err.Message, err.Code) + }, + } +} + +func (sm *ServeMux) HandleFunc(pattern string, handler WebHandler) { + sm.ServeMux.HandleFunc(pattern, func(w http.ResponseWriter, r *http.Request) { + defer func() { + if fail := recover(); fail != nil { + w.WriteHeader(http.StatusInternalServerError) + sm.log.Error("runtime panic!", "error", fail) + } + }() + if err := handler(w, r); err != nil { + sm.errorHandler(err, w, r) + } + }) +} + +func (sm *ServeMux) HandleError(fn ErrorHandler) { + sm.errorHandler = fn +}
M internal/website/mux.go → internal/website/mux.go
@@ -45,52 +45,13 @@ reader storage.Reader *server.App } -type webHandler func(http.ResponseWriter, *http.Request) *ihttp.Error - -type WrappedWebHandler struct { - config *config.Config - handler webHandler - log *log.Logger -} - -func wrapHandler(cfg *config.Config, webHandler webHandler, log *log.Logger) WrappedWebHandler { - return WrappedWebHandler{ - config: cfg, - handler: webHandler, - log: log, - } -} - -func (fn WrappedWebHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - defer func() { - if fail := recover(); fail != nil { - w.WriteHeader(http.StatusInternalServerError) - fn.log.Error("runtime panic!", "error", fail) - } - }() - if err := fn.handler(w, r); err != nil { - if strings.Contains(r.Header.Get("Accept"), "text/html") { - w.WriteHeader(err.Code) - err := templates.Error(fn.config, r.URL.Path, err).Render(r.Context(), w) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - } - } else { - http.Error(w, err.Message, err.Code) - } - } -} - func New( opts *Options, log *log.Logger, ) (*Website, error) { - mux := http.NewServeMux() website := &Website{ log: log, - App: &server.App{ - Handler: mux, - }, + App: &server.App{}, } builderOptions := &builder.Options{ Source: opts.Source,@@ -120,6 +81,18 @@ if err != nil { return nil, errors.WithMessage(err, "could not load configuration") } + mux := ihttp.NewServeMux() + mux.HandleError(func(err *ihttp.Error, w http.ResponseWriter, r *http.Request) { + if strings.Contains(r.Header.Get("Accept"), "text/html") { + w.WriteHeader(err.Code) + err := templates.Error(cfg, r.URL.Path, err).Render(r.Context(), w) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + } else { + http.Error(w, err.Message, err.Code) + } + }) if opts.Development { tmpdir, err := os.MkdirTemp("", "website") if err != nil {@@ -174,7 +147,7 @@ if err != nil { return nil, errors.WithMessage(err, "error creating sqlite reader") } - mux.Handle("/", wrapHandler(cfg, func(w http.ResponseWriter, r *http.Request) *ihttp.Error { + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) *ihttp.Error { urlPath, shouldRedirect := website.reader.CanonicalisePath(r.URL.Path) if shouldRedirect { http.Redirect(w, r, urlPath, 302)@@ -209,42 +182,40 @@ w.Header().Add("Content-Type", file.ContentType) http.ServeContent(w, r, file.Path, file.LastModified, file.Encodings[enc]) return nil - }, log)) + }) var acctResource = "acct:" + cfg.Email me := digit.NewResource(acctResource). Link("http://openid.net/specs/connect/1.0/issuer", "", cfg.OIDCHost.String()) - mux.HandleFunc("/.well-known/webfinger", func(w http.ResponseWriter, r *http.Request) { - if r.URL.Query().Get("resource") == acctResource { - obj, err := json.Marshal(me) - if err != nil { - http.Error( - w, - http.StatusText(http.StatusInternalServerError), - http.StatusInternalServerError, - ) + mux.HandleFunc( + "/.well-known/webfinger", + func(w http.ResponseWriter, r *http.Request) *ihttp.Error { + if r.URL.Query().Get("resource") == acctResource { + obj, err := json.Marshal(me) + if err != nil { + + return &ihttp.Error{ + Code: http.StatusInternalServerError, + } + } - return + w.Header().Add("Content-Type", "application/jrd+json") + w.Header().Add("Access-Control-Allow-Origin", "*") + _, err = w.Write(obj) + if err != nil { + log.Warn("error writing webfinger request", "error", err) + } } - w.Header().Add("Content-Type", "application/jrd+json") - w.Header().Add("Access-Control-Allow-Origin", "*") - _, err = w.Write(obj) - if err != nil { - log.Warn("error writing webfinger request", "error", err) - } - } - }) + return nil + }, + ) const oidcPath = "/.well-known/openid-configuration" - mux.HandleFunc( - oidcPath, - func(w http.ResponseWriter, r *http.Request) { - u := cfg.OIDCHost.JoinPath(oidcPath) - http.Redirect(w, r, u.String(), 302) - }) + mux.Handle(oidcPath, http.RedirectHandler(cfg.OIDCHost.JoinPath(oidcPath).String(), 302)) website.config = cfg + website.App.Handler = mux return website, nil }