Skip to content

Commit

Permalink
feat: add error handling funcs
Browse files Browse the repository at this point in the history
  • Loading branch information
crhntr committed Aug 5, 2024
1 parent 14d0272 commit c65767c
Show file tree
Hide file tree
Showing 4 changed files with 389 additions and 7 deletions.
26 changes: 20 additions & 6 deletions handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,16 @@ func newOptions() Options {
})),
receiver: nil,
execute: defaultExecute,
error: defaultError,
error: internalServerErrorErrorFunc,
}
}

func WithStructuredLogger(log *slog.Logger) Options { return newOptions().WithStructuredLogger(log) }
func WithReceiver(r any) Options { return newOptions().WithReceiver(r) }
func WithDataFunc(ex ExecuteFunc[any]) Options { return newOptions().WithDataFunc(ex) }
func WithErrorFunc(ex ExecuteFunc[error]) Options { return newOptions().WithErrorFunc(ex) }
func WithNoopErrorFunc() Options { return newOptions().WithNoopErrorFunc() }
func With500ErrorFunc() Options { return newOptions().With500ErrorFunc() }

func (o Options) WithStructuredLogger(log *slog.Logger) Options {
o.logger = log
Expand All @@ -62,6 +64,23 @@ func (o Options) WithErrorFunc(ex ExecuteFunc[error]) Options {
return o
}

func (o Options) WithNoopErrorFunc() Options {
o.error = noopErrorFunc
return o
}

func (o Options) With500ErrorFunc() Options {
o.error = internalServerErrorErrorFunc
return o
}

func noopErrorFunc(http.ResponseWriter, *http.Request, *template.Template, *slog.Logger, error) {}

func internalServerErrorErrorFunc(res http.ResponseWriter, _ *http.Request, t *template.Template, logger *slog.Logger, err error) {
logger.Error("handler error", "error", err, "template", t.Name())
http.Error(res, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}

func applyOptions(options []Options) *Options {
result := newOptions()
for _, o := range options {
Expand Down Expand Up @@ -385,8 +404,3 @@ func defaultExecute(res http.ResponseWriter, req *http.Request, t *template.Temp
return
}
}

func defaultError(res http.ResponseWriter, _ *http.Request, t *template.Template, logger *slog.Logger, err error) {
logger.Error("handler error", "error", err, "template", t.Name())
http.Error(res, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
65 changes: 64 additions & 1 deletion handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ import (
//counterfeiter:generate -o ./internal/fake/receiver.go --fake-name Receiver . receiver
var _ receiver = (*fake.Receiver)(nil)

//counterfeiter:generate -o ./internal/fake/response_writer.go --fake-name ResponseWriter net/http.ResponseWriter

type (
receiver interface {
ListArticles(ctx context.Context) ([]example.Article, error)
Expand All @@ -38,6 +40,7 @@ type (
NumAuthors() int
CheckAuth(req *http.Request) (string, error)
Handler(http.ResponseWriter, *http.Request) template.HTML
ErrorHandler(http.ResponseWriter, *http.Request) (template.HTML, error)
LogLines(*slog.Logger) int
Template(*template.Template) template.HTML
Type(any) string
Expand Down Expand Up @@ -481,7 +484,7 @@ func TestRoutes(t *testing.T) {
require.ErrorContains(t, err, "the second result must be an error")
})

t.Run("when teh error handler is overwritten", func(t *testing.T) {
t.Run("when the error handler is overwritten", func(t *testing.T) {
//
ts := template.Must(template.New("simple path").Parse(
`{{define "GET / ListArticles(ctx)" }}<h1>{{len .}}</h1>{{end}}`,
Expand All @@ -507,6 +510,66 @@ func TestRoutes(t *testing.T) {
res := rec.Result()
assert.Equal(t, http.StatusBadRequest, res.StatusCode)
})

t.Run("when the noop handler error func is configures", func(t *testing.T) {
//
ts := template.Must(template.New("simple path").Parse(
`{{define "GET / ErrorHandler(response, request)" }}{{.}}{{end}}`,
))
mux := http.NewServeMux()
s := new(fake.Receiver)

const body = `<p id="error">Excuse You</p>`
s.ErrorHandlerStub = func(res http.ResponseWriter, _ *http.Request) (template.HTML, error) {
res.WriteHeader(http.StatusBadRequest)
_, _ = io.WriteString(res, body)
return "", fmt.Errorf("banana")
}

logBuffer := bytes.NewBuffer(nil)
logger := slog.New(slog.NewTextHandler(logBuffer, &slog.HandlerOptions{Level: slog.LevelDebug}))

err := muxt.Handlers(mux, ts, muxt.WithNoopErrorFunc().WithReceiver(s).WithStructuredLogger(logger))
require.NoError(t, err)

req := httptest.NewRequest(http.MethodGet, "/", nil)
res := new(fake.ResponseWriter)
mux.ServeHTTP(res, req)

assert.Equal(t, 1, res.WriteHeaderCallCount())
assert.Equal(t, http.StatusBadRequest, res.WriteHeaderArgsForCall(0))
assert.Equal(t, body, string(res.WriteArgsForCall(0)))
assert.Empty(t, logBuffer.String())
})

t.Run("when the 500 handler error func is configured", func(t *testing.T) {
//
ts := template.Must(template.New("simple path").Parse(
`{{define "GET / ErrorHandler(response, request)" }}{{.}}{{end}}`,
))
mux := http.NewServeMux()
s := new(fake.Receiver)

s.ErrorHandlerStub = func(res http.ResponseWriter, _ *http.Request) (template.HTML, error) {
return "", fmt.Errorf("banana")
}

logBuffer := bytes.NewBuffer(nil)
logger := slog.New(slog.NewTextHandler(logBuffer, &slog.HandlerOptions{Level: slog.LevelDebug}))

err := muxt.Handlers(mux, ts, muxt.With500ErrorFunc().WithReceiver(s).WithStructuredLogger(logger))
require.NoError(t, err)

req := httptest.NewRequest(http.MethodGet, "/", nil)
res := new(fake.ResponseWriter)
res.HeaderReturns(make(http.Header))
mux.ServeHTTP(res, req)

assert.Equal(t, 1, res.WriteHeaderCallCount())
assert.Equal(t, http.StatusInternalServerError, res.WriteHeaderArgsForCall(0))
assert.Equal(t, http.StatusText(http.StatusInternalServerError)+"\n", string(res.WriteArgsForCall(0)))
assert.Contains(t, logBuffer.String(), "error=banana")
})
}

type errorWriter struct {
Expand Down
81 changes: 81 additions & 0 deletions internal/fake/receiver.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit c65767c

Please sign in to comment.