Repository: swaggest/rest (go)

Issue:
Gzip middleware blocks websockets.
Currently [`gzip` middleware](https://github.com/swaggest/rest/blob/v0.2.36/response/gzip/middleware.go#L26) hides `http.Hijacker` and prevents connection upgrade.

Pull request:
Add http.Hijacker to gzip middleware, add usecase middleware example
Fixes #107.

Patch:
diff --git a/_examples/advanced-generic/router.go b/_examples/advanced-generic/router.go
index 27043a7..e9fdf16 100644
--- a/_examples/advanced-generic/router.go
+++ b/_examples/advanced-generic/router.go
@@ -5,6 +5,7 @@ package main
 import (
 	"context"
 	"errors"
+	"log"
 	"net/http"
 	"reflect"
 	"strings"
@@ -20,6 +21,7 @@ import (
 	"github.com/swaggest/rest/response/gzip"
 	"github.com/swaggest/rest/web"
 	swgui "github.com/swaggest/swgui/v4emb"
+	"github.com/swaggest/usecase"
 )
 
 func NewRouter() http.Handler {
@@ -32,6 +34,27 @@ func NewRouter() http.Handler {
 		return strings.ReplaceAll(defaultDefName, "Generic", "")
 	})
 
+	// Usecase middlewares can be added to web.Service or chirouter.Wrapper.
+	s.Wrap(nethttp.UseCaseMiddlewares(usecase.MiddlewareFunc(func(next usecase.Interactor) usecase.Interactor {
+		var (
+			hasName usecase.HasName
+			name    = "unknown"
+		)
+
+		if usecase.As(next, &hasName) {
+			name = hasName.Name()
+		}
+
+		return usecase.Interact(func(ctx context.Context, input, output interface{}) error {
+			err := next.Interact(ctx, input, output)
+			if err != nil {
+				log.Printf("usecase %s request (%v) failed: %v\n", name, input, err)
+			}
+
+			return err
+		})
+	})))
+
 	// An example of global schema override to disable additionalProperties for all object schemas.
 	s.OpenAPICollector.Reflector().DefaultOptions = append(s.OpenAPICollector.Reflector().DefaultOptions, func(rc *jsonschema.ReflectContext) {
 		it := rc.InterceptType
diff --git a/go.sum b/go.sum
index 7c27a1b..c919314 100644
--- a/go.sum
+++ b/go.sum
@@ -77,8 +77,6 @@ github.com/swaggest/form/v5 v5.0.2 h1:TiimP7UX3q1nSI5ZGg6tGMmxg8UJB0JNpaZjXWH6V2
 github.com/swaggest/form/v5 v5.0.2/go.mod h1:Ayta1ggwSnDd4zwzv47jQL4RHk7WOTHp65nzwBc0IhU=
 github.com/swaggest/jsonschema-go v0.3.45 h1:h2RhU+K3HbI8c+giX4PWI8R96EW6/wtPHfagpAvdPfc=
 github.com/swaggest/jsonschema-go v0.3.45/go.mod h1:pW2jvwFFD2cQ0jv51bAo8+RLmma7yaUOIcH8s54EfJw=
-github.com/swaggest/openapi-go v0.2.27 h1:IHrxByLf0urVf/KF+J2asLiseR66R5OO3LPymipAGmU=
-github.com/swaggest/openapi-go v0.2.27/go.mod h1:2RAaFLmRFQiR8TQJyIZzh2NTY1ihk3/WxYJFxQ+TfFU=
 github.com/swaggest/openapi-go v0.2.28 h1:PRgJTqRfpsWte7qPqMFJooLu8cWyL91FhpYH2k1353Y=
 github.com/swaggest/openapi-go v0.2.28/go.mod h1:2RAaFLmRFQiR8TQJyIZzh2NTY1ihk3/WxYJFxQ+TfFU=
 github.com/swaggest/refl v1.1.0 h1:a+9a75Kv6ciMozPjVbOfcVTEQe81t2R3emvaD9oGQGc=
diff --git a/response/gzip/middleware.go b/response/gzip/middleware.go
index 76b66bd..540f85c 100644
--- a/response/gzip/middleware.go
+++ b/response/gzip/middleware.go
@@ -6,6 +6,7 @@ import (
 	"compress/gzip"
 	"fmt"
 	"io"
+	"net"
 	"net/http"
 	"strings"
 	"sync"
@@ -26,9 +27,9 @@ const (
 func Middleware(next http.Handler) http.Handler {
 	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 		w = maybeGzipResponseWriter(w, r)
-		if grw, ok := w.(*gzipResponseWriter); ok {
+		if closer, ok := w.(io.Closer); ok {
 			defer func() {
-				err := grw.Close()
+				err := closer.Close()
 				if err != nil {
 					panic(fmt.Sprintf("BUG: cannot close gzip writer: %s", err))
 				}
@@ -89,6 +90,17 @@ func maybeGzipResponseWriter(w http.ResponseWriter, r *http.Request) http.Respon
 		return w
 	}
 
+	if hj, ok := w.(http.Hijacker); ok {
+		zrw := &gzipResponseWriterHijacker{
+			gzipResponseWriter: gzipResponseWriter{
+				ResponseWriter: w,
+			},
+			hijacker: hj,
+		}
+
+		return zrw
+	}
+
 	zrw := &gzipResponseWriter{
 		ResponseWriter: w,
 	}
@@ -106,7 +118,27 @@ type gzipResponseWriter struct {
 	disableCompression    bool
 }
 
-var _ gz.Writer = &gzipResponseWriter{}
+type gzipResponseWriterHijacker struct {
+	gzipResponseWriter
+	hijacker http.Hijacker
+}
+
+func (rw *gzipResponseWriterHijacker) Hijack() (net.Conn, *bufio.ReadWriter, error) {
+	return rw.hijacker.Hijack()
+}
+
+var (
+	_ gz.Writer = &gzipResponseWriter{}
+	_ gz.Writer = &gzipResponseWriterHijacker{}
+
+	_ http.ResponseWriter = &gzipResponseWriter{}
+	_ http.ResponseWriter = &gzipResponseWriterHijacker{}
+
+	_ http.Flusher = &gzipResponseWriter{}
+	_ http.Flusher = &gzipResponseWriterHijacker{}
+
+	_ http.Hijacker = &gzipResponseWriterHijacker{}
+)
 
 func (rw *gzipResponseWriter) GzipWrite(data []byte) (int, error) {
 	if rw.headersWritten {


Test patch:
diff --git a/response/gzip/middleware_test.go b/response/gzip/middleware_test.go
index ffd99d2..ddda589 100644
--- a/response/gzip/middleware_test.go
+++ b/response/gzip/middleware_test.go
@@ -249,3 +249,34 @@ func gzipDecode(t *testing.T, data []byte) []byte {
 
 	return j
 }
+
+func TestMiddleware_hijacker(t *testing.T) {
+	rb := []byte(strings.Repeat("A", 10000) + "!!!")
+	h := gzip.Middleware(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
+		_, ok := rw.(http.Hijacker)
+		require.True(t, ok)
+
+		_, err := rw.Write(rb)
+		assert.NoError(t, err)
+	}))
+
+	srv := httptest.NewServer(h)
+	defer srv.Close()
+
+	r, err := http.NewRequest(http.MethodGet, srv.URL+"/", nil)
+
+	require.NoError(t, err)
+	r.Header.Set("Accept-Encoding", "gzip, deflate, br")
+
+	resp, err := http.DefaultTransport.RoundTrip(r)
+	require.NoError(t, err)
+
+	body, err := ioutil.ReadAll(resp.Body)
+	require.NoError(t, err)
+
+	require.NoError(t, resp.Body.Close())
+
+	assert.Equal(t, "gzip", resp.Header.Get("Content-Encoding"))
+	assert.Less(t, len(body), len(rb)) // Response is compressed.
+	assert.Equal(t, rb, gzipDecode(t, body))
+}