Repository: swaggest/rest
Pull request #27 (2021-02-25 13:45:08)

Allow custom error handling via HandleErrResponse

github-actions[bot]: ### Unit Test Coverage
total:	(statements)	81.2%
<details><summary>Coverage diff with base branch</summary>

```diff
45c45
< github.com/swaggest/rest/nethttp/handler.go	NewHandler	80.0%
---
> github.com/swaggest/rest/nethttp/handler.go	NewHandler	83.3%
51c51,52
< github.com/swaggest/rest/nethttp/handler.go	handleErrResponse	100.0%
---
> github.com/swaggest/rest/nethttp/handler.go	handleErrResponseDefault	100.0%
> github.com/swaggest/rest/nethttp/handler.go	handleErrResponse	75.0%
64a66
> github.com/swaggest/rest/nethttp/options.go	OptionsMiddleware	100.0%
172c174
< total:	(statements)	81.1%
---
> total:	(statements)	81.2%
```
</details>
<!-- Sticky Pull Request Commentunit-test -->
codecov[bot]: # [Codecov](https://codecov.io/gh/swaggest/rest/pull/27?src=pr&el=h1) Report
> Merging [#27](https://codecov.io/gh/swaggest/rest/pull/27?src=pr&el=desc) (579887b) into [master](https://codecov.io/gh/swaggest/rest/commit/88927c7ef479c82ecc03317fbe14a8da536ba0b5?el=desc) (88927c7) will **increase** coverage by `0.12%`.
> The diff coverage is `90.90%`.

[![Impacted file tree graph](https://codecov.io/gh/swaggest/rest/pull/27/graphs/tree.svg?width=650&height=150&src=pr&token=OpLHxxPdmn)](https://codecov.io/gh/swaggest/rest/pull/27?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master      #27      +/-   ##
==========================================
+ Coverage   73.34%   73.47%   +0.12%     
==========================================
  Files          26       26              
  Lines        1497     1508      +11     
==========================================
+ Hits         1098     1108      +10     
- Misses        252      253       +1     
  Partials      147      147              
```

| Flag | Coverage Δ | |
|---|---|---|
| unittests | `73.47% <90.90%> (+0.12%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/swaggest/rest/pull/27?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [trait.go](https://codecov.io/gh/swaggest/rest/pull/27/diff?src=pr&el=tree#diff-dHJhaXQuZ28=) | `93.33% <ø> (ø)` | |
| [nethttp/handler.go](https://codecov.io/gh/swaggest/rest/pull/27/diff?src=pr&el=tree#diff-bmV0aHR0cC9oYW5kbGVyLmdv) | `86.41% <80.00%> (-0.43%)` | :arrow_down: |
| [nethttp/options.go](https://codecov.io/gh/swaggest/rest/pull/27/diff?src=pr&el=tree#diff-bmV0aHR0cC9vcHRpb25zLmdv) | `86.84% <100.00%> (+2.46%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/swaggest/rest/pull/27?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/swaggest/rest/pull/27?src=pr&el=footer). Last update [88927c7...a4160b5](https://codecov.io/gh/swaggest/rest/pull/27?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

github-actions[bot]: ### Benchmark Result
<details><summary>Benchmark diff with base branch</summary>

```
Using vendor
name                                    old time/op    new time/op    delta
pkg:github.com/swaggest/rest/jsonschema goos:linux goarch:amd64
RequestValidator_ValidateRequestData-2    1.64µs ± 1%    2.12µs ± 4%  +29.60%  (p=0.029 n=4+4)
pkg:github.com/swaggest/rest/request goos:linux goarch:amd64
Decoder_Decode-2                           926ns ± 2%    1055ns ± 1%  +13.95%  (p=0.008 n=5+5)
DecoderFunc_Decode-2                      2.57µs ± 3%    2.87µs ± 3%  +11.57%  (p=0.008 n=5+5)
Decoder_Decode_json-2                     28.7µs ± 2%    32.2µs ± 3%  +12.48%  (p=0.008 n=5+5)
Decoder_Decode_queryObject-2              6.69µs ± 0%    7.33µs ± 2%   +9.45%  (p=0.008 n=5+5)
Decoder_Decode_jsonParam-2                2.31µs ± 2%    2.56µs ± 2%  +11.03%  (p=0.008 n=5+5)
DecoderFactory_SetDecoderFunc-2           2.06µs ± 1%    2.39µs ± 3%  +16.22%  (p=0.008 n=5+5)
pkg:github.com/swaggest/rest/response/gzip goos:linux goarch:amd64
Middleware-2                              16.1µs ± 1%    17.4µs ± 3%   +7.78%  (p=0.008 n=5+5)
Middleware_control-2                      3.86µs ± 2%    4.63µs ± 3%  +20.19%  (p=0.008 n=5+5)

name                                    old alloc/op   new alloc/op   delta
pkg:github.com/swaggest/rest/jsonschema goos:linux goarch:amd64
RequestValidator_ValidateRequestData-2    2.50kB ± 0%    2.50kB ± 0%     ~     (all equal)
pkg:github.com/swaggest/rest/request goos:linux goarch:amd64
Decoder_Decode-2                            440B ± 0%      440B ± 0%     ~     (all equal)
DecoderFunc_Decode-2                      1.51kB ± 0%    1.51kB ± 0%     ~     (all equal)
Decoder_Decode_json-2                     12.3kB ± 0%    12.3kB ± 0%     ~     (all equal)
Decoder_Decode_queryObject-2              2.00kB ± 0%    2.00kB ± 0%     ~     (all equal)
Decoder_Decode_jsonParam-2                  720B ± 0%      720B ± 0%     ~     (all equal)
DecoderFactory_SetDecoderFunc-2           1.02kB ± 0%    1.02kB ± 0%     ~     (all equal)
pkg:github.com/swaggest/rest/response/gzip goos:linux goarch:amd64
Middleware-2                              1.13kB ± 3%    1.11kB ± 2%     ~     (p=0.286 n=5+5)
Middleware_control-2                      11.2kB ± 0%    11.2kB ± 0%     ~     (all equal)

name                                    old allocs/op  new allocs/op  delta
pkg:github.com/swaggest/rest/jsonschema goos:linux goarch:amd64
RequestValidator_ValidateRequestData-2      8.00 ± 0%      8.00 ± 0%     ~     (all equal)
pkg:github.com/swaggest/rest/request goos:linux goarch:amd64
Decoder_Decode-2                            4.00 ± 0%      4.00 ± 0%     ~     (all equal)
DecoderFunc_Decode-2                        12.0 ± 0%      12.0 ± 0%     ~     (all equal)
Decoder_Decode_json-2                        169 ± 0%       169 ± 0%     ~     (all equal)
Decoder_Decode_queryObject-2                36.0 ± 0%      36.0 ± 0%     ~     (all equal)
Decoder_Decode_jsonParam-2                  12.0 ± 0%      12.0 ± 0%     ~     (all equal)
DecoderFactory_SetDecoderFunc-2             16.0 ± 0%      16.0 ± 0%     ~     (all equal)
pkg:github.com/swaggest/rest/response/gzip goos:linux goarch:amd64
Middleware-2                                11.0 ± 0%      11.0 ± 0%     ~     (all equal)
Middleware_control-2                        9.00 ± 0%      9.00 ± 0%     ~     (all equal)
```
</details>

### Examples Benchmark Result
<details><summary>Benchmark diff with base branch</summary>

```
Using vendor
name                    old time/op    new time/op    delta
pkg:github.com/swaggest/rest/_examples/advanced goos:linux goarch:amd64
_directGzip-2             26.0µs ± 1%    30.6µs ± 3%  +17.90%  (p=0.008 n=5+5)
_directGzipHead-2         25.6µs ± 2%    30.0µs ± 3%  +17.55%  (p=0.008 n=5+5)
_noDirectGzip-2            146µs ± 2%     158µs ± 1%   +8.70%  (p=0.008 n=5+5)
_directGzip_decode-2       540µs ± 2%     594µs ± 1%  +10.03%  (p=0.008 n=5+5)
_noDirectGzip_decode-2     146µs ± 2%     163µs ± 2%  +12.12%  (p=0.008 n=5+5)
_jsonBody-2               45.6µs ± 1%    55.1µs ± 3%  +20.78%  (p=0.008 n=5+5)
_jsonBodyValidation-2     54.5µs ± 2%    66.2µs ± 1%  +21.40%  (p=0.008 n=5+5)
_outputHeaders-2          25.7µs ± 3%    29.8µs ± 1%  +15.93%  (p=0.008 n=5+5)
_validation-2             51.4µs ± 2%    59.4µs ± 0%  +15.58%  (p=0.008 n=5+5)
pkg:github.com/swaggest/rest/_examples/task-api/internal/infra/nethttp goos:linux goarch:amd64
_notFoundSrv-2            29.3µs ± 2%    34.1µs ± 4%  +16.11%  (p=0.008 n=5+5)
_ok-2                     30.5µs ± 2%    35.7µs ± 2%  +17.12%  (p=0.008 n=5+5)
_invalidBody-2            44.6µs ± 5%    53.2µs ± 2%  +19.09%  (p=0.008 n=5+5)

name                    old B:rcvd/op  new B:rcvd/op  delta
pkg:github.com/swaggest/rest/_examples/advanced goos:linux goarch:amd64
_directGzip-2                639 ± 0%       639 ± 0%     ~     (all equal)
_directGzipHead-2            183 ± 0%       183 ± 0%     ~     (all equal)
_noDirectGzip-2            1.04k ± 0%     1.04k ± 0%     ~     (all equal)
_directGzip_decode-2         639 ± 0%       639 ± 0%     ~     (all equal)
_noDirectGzip_decode-2     1.04k ± 0%     1.04k ± 0%     ~     (all equal)
_jsonBody-2                  208 ± 0%       208 ± 0%     ~     (all equal)
_jsonBodyValidation-2        194 ± 0%       194 ± 0%     ~     (all equal)
_outputHeaders-2             155 ± 0%       155 ± 0%     ~     (all equal)
_validation-2                177 ± 0%       177 ± 0%     ~     (all equal)
pkg:github.com/swaggest/rest/_examples/task-api/internal/infra/nethttp goos:linux goarch:amd64
_notFoundSrv-2               337 ± 0%       337 ± 0%     ~     (all equal)
_ok-2                        359 ± 0%       359 ± 0%     ~     (all equal)
_invalidBody-2               435 ± 0%       435 ± 0%     ~     (all equal)

name                    old B:sent/op  new B:sent/op  delta
pkg:github.com/swaggest/rest/_examples/advanced goos:linux goarch:amd64
_directGzip-2                103 ± 0%       103 ± 0%     ~     (all equal)
_directGzipHead-2            104 ± 0%       104 ± 0%     ~     (all equal)
_noDirectGzip-2              117 ± 0%       117 ± 0%     ~     (all equal)
_directGzip_decode-2         116 ± 0%       116 ± 0%     ~     (all equal)
_noDirectGzip_decode-2       130 ± 0%       130 ± 0%     ~     (all equal)
_jsonBody-2                  188 ± 0%       188 ± 0%     ~     (all equal)
_jsonBodyValidation-2        192 ± 0%       192 ± 0%     ~     (all equal)
_outputHeaders-2            77.0 ± 0%      77.0 ± 0%     ~     (all equal)
_validation-2                170 ± 0%       170 ± 0%     ~     (all equal)
pkg:github.com/swaggest/rest/_examples/task-api/internal/infra/nethttp goos:linux goarch:amd64
_notFoundSrv-2              74.0 ± 0%      74.0 ± 0%     ~     (all equal)
_ok-2                       74.0 ± 0%      74.0 ± 0%     ~     (all equal)
_invalidBody-2               137 ± 0%       137 ± 0%     ~     (all equal)

name                    old rps        new rps        delta
pkg:github.com/swaggest/rest/_examples/advanced goos:linux goarch:amd64
_directGzip-2              38.5k ± 1%     32.6k ± 3%  -15.16%  (p=0.008 n=5+5)
_directGzipHead-2          39.1k ± 2%     33.3k ± 3%  -14.88%  (p=0.008 n=5+5)
_noDirectGzip-2            6.86k ± 2%     6.31k ± 1%   -8.01%  (p=0.008 n=5+5)
_directGzip_decode-2       1.85k ± 2%     1.68k ± 1%   -9.12%  (p=0.008 n=5+5)
_noDirectGzip_decode-2     6.86k ± 2%     6.12k ± 2%  -10.83%  (p=0.008 n=5+5)
_jsonBody-2                21.9k ± 1%     18.2k ± 3%  -17.19%  (p=0.008 n=5+5)
_jsonBodyValidation-2      18.3k ± 2%     15.1k ± 1%  -17.64%  (p=0.008 n=5+5)
_outputHeaders-2           39.0k ± 3%     33.6k ± 1%  -13.77%  (p=0.008 n=5+5)
_validation-2              19.5k ± 2%     16.8k ± 0%  -13.49%  (p=0.008 n=5+5)
pkg:github.com/swaggest/rest/_examples/task-api/internal/infra/nethttp goos:linux goarch:amd64
_notFoundSrv-2             34.1k ± 2%     29.3k ± 4%  -13.87%  (p=0.008 n=5+5)
_ok-2                      32.8k ± 2%     28.0k ± 2%  -14.61%  (p=0.008 n=5+5)
_invalidBody-2             22.4k ± 5%     18.8k ± 2%  -16.07%  (p=0.008 n=5+5)

name                    old alloc/op   new alloc/op   delta
pkg:github.com/swaggest/rest/_examples/advanced goos:linux goarch:amd64
_directGzip-2             3.86kB ± 0%    3.86kB ± 0%   +0.14%  (p=0.008 n=5+5)
_directGzipHead-2         3.86kB ± 0%    3.86kB ± 0%   +0.10%  (p=0.024 n=5+5)
_noDirectGzip-2           6.26kB ±18%    6.15kB ±19%     ~     (p=0.841 n=5+5)
_directGzip_decode-2       402kB ± 0%     402kB ± 0%     ~     (p=0.127 n=5+5)
_noDirectGzip_decode-2    5.51kB ±12%    5.12kB ± 6%     ~     (p=0.056 n=5+5)
_jsonBody-2               11.7kB ± 0%    11.7kB ± 0%   +0.07%  (p=0.008 n=5+5)
_jsonBodyValidation-2     18.2kB ± 0%    18.2kB ± 0%   +0.06%  (p=0.008 n=5+5)
_outputHeaders-2          3.63kB ± 0%    3.64kB ± 0%   +0.12%  (p=0.008 n=5+5)
_validation-2             16.2kB ± 0%    16.2kB ± 0%     ~     (p=0.381 n=5+5)
pkg:github.com/swaggest/rest/_examples/task-api/internal/infra/nethttp goos:linux goarch:amd64
_notFoundSrv-2            4.81kB ± 0%    4.82kB ± 0%   +0.08%  (p=0.024 n=5+5)
_ok-2                     4.72kB ± 0%    4.72kB ± 0%     ~     (p=0.371 n=4+4)
_invalidBody-2            8.58kB ± 0%    8.58kB ± 0%     ~     (p=0.229 n=4+4)

name                    old allocs/op  new allocs/op  delta
pkg:github.com/swaggest/rest/_examples/advanced goos:linux goarch:amd64
_directGzip-2               40.0 ± 0%      40.0 ± 0%     ~     (all equal)
_directGzipHead-2           40.0 ± 0%      40.0 ± 0%     ~     (all equal)
_noDirectGzip-2             47.8 ± 4%      47.8 ± 4%     ~     (p=1.000 n=5+5)
_directGzip_decode-2         497 ± 0%       497 ± 0%     ~     (p=0.444 n=5+5)
_noDirectGzip_decode-2      48.0 ± 0%      48.0 ± 0%     ~     (all equal)
_jsonBody-2                  103 ± 0%       103 ± 0%     ~     (all equal)
_jsonBodyValidation-2        144 ± 0%       144 ± 0%     ~     (all equal)
_outputHeaders-2            35.0 ± 0%      35.0 ± 0%     ~     (all equal)
_validation-2                131 ± 0%       131 ± 0%     ~     (all equal)
pkg:github.com/swaggest/rest/_examples/task-api/internal/infra/nethttp goos:linux goarch:amd64
_notFoundSrv-2              52.0 ± 0%      52.0 ± 0%     ~     (all equal)
_ok-2                       48.0 ± 0%      48.0 ± 0%     ~     (all equal)
_invalidBody-2              98.0 ± 0%      98.0 ± 0%     ~     (all equal)
```
</details>
<!-- Sticky Pull Request Commentbench -->

Patch:
diff --git a/nethttp/handler.go b/nethttp/handler.go
index d4cd8f1..7daecef 100644
--- a/nethttp/handler.go
+++ b/nethttp/handler.go
@@ -5,7 +5,6 @@ import (
 	"net/http"
 	"reflect"
 
-	"github.com/swaggest/openapi-go/openapi3"
 	"github.com/swaggest/rest"
 	"github.com/swaggest/usecase"
 	"github.com/swaggest/usecase/status"
@@ -22,6 +21,7 @@ func NewHandler(useCase usecase.Interactor, options ...func(h *Handler)) *Handle
 	h := &Handler{
 		options: options,
 	}
+	h.HandleErrResponse = h.handleErrResponseDefault
 	h.SetUseCase(useCase)
 
 	return h
@@ -50,8 +50,8 @@ func (h *Handler) SetUseCase(useCase usecase.Interactor) {
 type Handler struct {
 	rest.HandlerTrait
 
-	// OperationAnnotations are called after operation setup and before adding operation to documentation.
-	OperationAnnotations []func(op *openapi3.Operation) error
+	// HandleErrResponse allows control of error response processing.
+	HandleErrResponse func(w http.ResponseWriter, r *http.Request, err error)
 
 	// requestDecoder maps data from http.Request into structured Go input value.
 	requestDecoder RequestDecoder
@@ -123,7 +123,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	h.responseEncoder.WriteSuccessfulResponse(w, r, output, h.HandlerTrait)
 }
 
-func (h *Handler) handleErrResponse(w http.ResponseWriter, r *http.Request, err error) {
+func (h *Handler) handleErrResponseDefault(w http.ResponseWriter, r *http.Request, err error) {
 	var (
 		code int
 		er   interface{}
@@ -138,6 +138,16 @@ func (h *Handler) handleErrResponse(w http.ResponseWriter, r *http.Request, err
 	h.responseEncoder.WriteErrResponse(w, r, code, er)
 }
 
+func (h *Handler) handleErrResponse(w http.ResponseWriter, r *http.Request, err error) {
+	if h.HandleErrResponse != nil {
+		h.HandleErrResponse(w, r, err)
+
+		return
+	}
+
+	h.handleErrResponseDefault(w, r, err)
+}
+
 func closeMultipartForm(r *http.Request) {
 	if err := r.MultipartForm.RemoveAll(); err != nil {
 		log.Println(err)
diff --git a/nethttp/options.go b/nethttp/options.go
index b3cad14..f2fcf2c 100644
--- a/nethttp/options.go
+++ b/nethttp/options.go
@@ -1,6 +1,7 @@
 package nethttp
 
 import (
+	"net/http"
 	"reflect"
 
 	"github.com/swaggest/openapi-go/openapi3"
@@ -8,6 +9,23 @@ import (
 	"github.com/swaggest/rest"
 )
 
+// OptionsMiddleware applies options to encountered nethttp.Handler.
+func OptionsMiddleware(options ...func(h *Handler)) func(h http.Handler) http.Handler {
+	return func(h http.Handler) http.Handler {
+		var rh *Handler
+
+		if HandlerAs(h, &rh) {
+			rh.options = append(rh.options, options...)
+
+			for _, option := range options {
+				option(rh)
+			}
+		}
+
+		return h
+	}
+}
+
 // AnnotateOperation allows customizations of prepared operations.
 func AnnotateOperation(annotations ...func(operation *openapi3.Operation) error) func(h *Handler) {
 	return func(h *Handler) {
diff --git a/trait.go b/trait.go
index 8b8ca07..bcfea4f 100644
--- a/trait.go
+++ b/trait.go
@@ -5,6 +5,7 @@ import (
 	"encoding/json"
 	"reflect"
 
+	"github.com/swaggest/openapi-go/openapi3"
 	"github.com/swaggest/refl"
 	"github.com/swaggest/usecase"
 )
@@ -33,6 +34,9 @@ type HandlerTrait struct {
 
 	// RespValidator validates decoded response data.
 	RespValidator Validator
+
+	// OperationAnnotations are called after operation setup and before adding operation to documentation.
+	OperationAnnotations []func(op *openapi3.Operation) error
 }
 
 // RestHandler is a an accessor.


Test patch:
diff --git a/nethttp/handler_test.go b/nethttp/handler_test.go
index 37764d2..415f167 100644
--- a/nethttp/handler_test.go
+++ b/nethttp/handler_test.go
@@ -298,3 +298,37 @@ func TestHandler_ServeHTTP_customMapping(t *testing.T) {
 	assert.Equal(t, http.StatusNoContent, rw.Code)
 	assert.Equal(t, "", rw.Body.String())
 }
+
+func TestOptionsMiddleware(t *testing.T) {
+	u := usecase.NewIOI(nil, nil, func(ctx context.Context, input, output interface{}) error {
+		return errors.New("failed")
+	})
+	h := nethttp.NewHandler(u, func(h *nethttp.Handler) {
+		h.MakeErrResp = func(ctx context.Context, err error) (int, interface{}) {
+			return http.StatusExpectationFailed, struct {
+				Foo string `json:"foo"`
+			}{Foo: err.Error()}
+		}
+	})
+	h.SetResponseEncoder(&response.Encoder{})
+
+	var loggedErr error
+
+	rw := httptest.NewRecorder()
+	req, err := http.NewRequest(http.MethodGet, "/", nil)
+	require.NoError(t, err)
+
+	oh := nethttp.OptionsMiddleware(func(h *nethttp.Handler) {
+		handleErrResponse := h.HandleErrResponse
+		h.HandleErrResponse = func(w http.ResponseWriter, r *http.Request, err error) {
+			assert.Equal(t, req, r)
+			loggedErr = err
+			handleErrResponse(w, r, err)
+		}
+	})(h)
+
+	oh.ServeHTTP(rw, req)
+
+	assert.EqualError(t, loggedErr, "failed")
+	assert.Equal(t, `{"foo":"failed"}`+"\n", rw.Body.String())
+}