Skip to content

Commit

Permalink
- move main line of development to /v1. /v2 is in the v2 branch. The …
Browse files Browse the repository at this point in the history
…only difference is the second optional argment ot onPanic.

- errors produced by PanicMiddleware is always an httperrror.Panic
- add PanicMiddleware example to docs
- update PanicMiddleware tests
  • Loading branch information
johnwarden committed Nov 6, 2022
1 parent f43193d commit 6f10031
Show file tree
Hide file tree
Showing 7 changed files with 181 additions and 72 deletions.
125 changes: 95 additions & 30 deletions README.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion error_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import (

"github.com/pkg/errors"

"github.com/johnwarden/httperror/v2"
"github.com/johnwarden/httperror"
"github.com/stretchr/testify/assert"
)

Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
module github.com/johnwarden/httperror/v2
module github.com/johnwarden/httperror

go 1.19

Expand Down
2 changes: 1 addition & 1 deletion httperror.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
Package httperror is for writing HTTP handlers that return errors instead of handling them directly. See the documentation at https://github.com/johnwarden/httperror/v2
Package httperror is for writing HTTP handlers that return errors instead of handling them directly. See the documentation at https://github.com/johnwarden/httperror
*/
package httperror

Expand Down
60 changes: 38 additions & 22 deletions panic.go
Original file line number Diff line number Diff line change
@@ -1,29 +1,48 @@
package httperror

import (
"errors"
"fmt"
"net/http"
)

var Panic = panicError{}

type panicError struct {
innerError error
message string
}

func (e panicError) Error() string {
if e.innerError != nil {
return "panic: " + e.innerError.Error()
}
return "panic: " + e.message
}

func (e panicError) Unwrap() error {
return e.innerError
}

func (e panicError) Is(other error) bool {
if other == Panic {
return true
}
return errors.Is(e.innerError, other)
}

// PanicMiddleware wraps a [httperror.Handler], returning a new [httperror.HandlerFunc] that
// recovers from panics and returns them as errors. The second argument is an optional
// function that is called if there is a panic. This function can be used, for example, to
// cleanly shutdown the server.
func PanicMiddleware(h Handler, s func(error)) HandlerFunc {
// recovers from panics and returns them as errors. Panic error can be identified using
// errors.Is(err, httperror.Panic)
func PanicMiddleware(h Handler) HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) (err error) {

defer func() {
if r := recover(); r != nil {
isErr := false
if err, isErr = r.(error); !isErr {
err = fmt.Errorf("%v", r)
}
if s != nil {
// the shutdown function must be called in a goroutine. Otherwise, if it is used
// to shutdown the server, we'll get a deadlock with the server shutdown function
// waiting for this request handler to finish, and this request waiting for the
// server shutdown function.
go s(err)
err = panicError{nil, fmt.Sprintf("%v", r)}
} else {
err = panicError{err, ""}
}
}
}()
Expand All @@ -34,20 +53,17 @@ func PanicMiddleware(h Handler, s func(error)) HandlerFunc {
}

// XPanicMiddleware wraps a [httperror.XHandler], returning a new [httperror.XHandlerFunc] that
// recovers from panics and returns them as errors. The second argument is an optional
// function that is called if there is a panic. This function can be used, for example, to
// cleanly shutdown the server.
func XPanicMiddleware[P any](h XHandler[P], s func(error)) XHandlerFunc[P] {
// recovers from panics and returns them as errors. Panic error can be identified using
// errors.Is(err, httperror.Panic)
func XPanicMiddleware[P any](h XHandler[P]) XHandlerFunc[P] {
return func(w http.ResponseWriter, r *http.Request, p P) (err error) {
defer func() {
if r := recover(); r != nil {
isErr := false
if err, isErr = r.(error); !isErr {
err = fmt.Errorf("%v", r)
}
if s != nil {
// the shutdown function must be called in a goroutine. See comment in PanicMiddleware above.
go s(err)
err = panicError{nil, fmt.Sprintf("%v", r)}
} else {
err = panicError{err, ""}
}
}
}()
Expand Down
56 changes: 43 additions & 13 deletions request_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ import (
"bytes"
"errors"
"fmt"
"io/ioutil"
"io"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"

"github.com/johnwarden/httperror/v2"
"github.com/johnwarden/httperror"

"github.com/stretchr/testify/assert"
)
Expand All @@ -24,7 +24,7 @@ func testRequest(h http.Handler, path string) (int, string) {
resp := rr.Result()
defer resp.Body.Close()
// io.Copy(os.Stdout, res.Body)
body, _ := ioutil.ReadAll(resp.Body)
body, _ := io.ReadAll(resp.Body)

return resp.StatusCode, string(body)
}
Expand All @@ -49,16 +49,41 @@ func TestCustomErrorHandler(t *testing.T) {
}

func TestPanic(t *testing.T) {
h := getMeOuttaHere
errChan := make(chan error)
h = httperror.PanicMiddleware(h, func(e error) {errChan <- e})
s, m := testRequest(h, "/")
assert.Equal(t, 500, s, "got 500 status code")
assert.Equal(t, "500 Internal Server Error\n", m, "got 500 text/plain response")
{
h := getMeOuttaHere
h = httperror.PanicMiddleware(h)

var e error
errorHandler := func(w http.ResponseWriter, err error) {
e = err
httperror.DefaultErrorHandler(w, err)
}

s, m := testRequest(httperror.WrapHandlerFunc(h, errorHandler), "/")
assert.Equal(t, 500, s, "got 500 status code")
assert.Equal(t, "500 Internal Server Error\n", m, "got 500 text/plain response")
assert.True(t, errors.Is(e, httperror.Panic))
assert.Equal(t, "panic: Get me outta here!", e.Error())
}

e := <-errChan
{
h := fail
h = httperror.PanicMiddleware(h)

var e error
errorHandler := func(w http.ResponseWriter, err error) {
e = err
httperror.DefaultErrorHandler(w, err)
}

s, m := testRequest(httperror.WrapHandlerFunc(h, errorHandler), "/")
assert.Equal(t, 500, s, "got 500 status code")
assert.Equal(t, "500 Internal Server Error\n", m, "got 500 text/plain response")
assert.True(t, errors.Is(e, httperror.Panic))
assert.True(t, errors.Is(e, sentinalError))
assert.Equal(t, "panic: SOME_ERROR", e.Error())

assert.Equal(t, "Get me outta here!", e.Error())
}
}

func TestApplyStandardMiddleware(t *testing.T) {
Expand Down Expand Up @@ -86,13 +111,18 @@ func TestApplyStandardMiddleware(t *testing.T) {
assert.Equal(t, 200, s)
assert.Equal(t, "Hello, Bill\n", m, "got middleware output")
}

}

var sentinalError = fmt.Errorf("SOME_ERROR")

var getMeOuttaHere = httperror.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
w.Header().Set("Content-Type", "text/plain")
panic("Get me outta here!")
return nil
})

var fail = httperror.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
w.Header().Set("Content-Type", "text/plain")
panic(sentinalError)
})

var okHandler = httperror.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
Expand Down
6 changes: 2 additions & 4 deletions standardmiddleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,7 @@ import (

type contextKey string

var (
key = contextKey("key")
)
var key = contextKey("key")

// StandardMiddleware is a standard http.Handler wrapper.
type StandardMiddleware = func(http.Handler) http.Handler
Expand Down Expand Up @@ -50,6 +48,7 @@ func XApplyStandardMiddleware[P any](h XHandler[P], ms ...StandardMiddleware) XH
return sm.err
}
}

// ApplyStandardMiddleware applies middleware written for a standard
// [http.Handler] to an [httperror.Handler], returning an
// [httperror.Handler]. It is possible to apply standard middleware to
Expand All @@ -60,7 +59,6 @@ func XApplyStandardMiddleware[P any](h XHandler[P], ms ...StandardMiddleware) XH
// could not return an error. This function solves that problem by passing
// errors and parameters through the context.
func ApplyStandardMiddleware(h Handler, ms ...StandardMiddleware) HandlerFunc {

var handler http.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
sm := ctx.Value(key).(*standardMiddleware[any])
Expand Down

0 comments on commit 6f10031

Please sign in to comment.