Repository: gin-gonic/gin
Pull request #3489 (2023-02-04 19:02:21)

chore(http): add support for go1.20 http.rwUnwrapper to gin.responseWriter
This PR implements the go1.20 `http.rwUnwrapper` interface on the private gin.responseWriter type:
```go
type rwUnwrapper interface {
	Unwrap() ResponseWriter
}
```

This will allow code using go1.20 to use the http.ResponseController utility type added in go1.20.

```go
engine.GET("/path", func (c *gin.Context) {
  controller := http.NewResponseController(c.Writer)
  controller.SetWriteDeadline(time.Second)
  // ...
}
```



codecov[bot]: # [Codecov](https://codecov.io/gh/gin-gonic/gin/pull/3489?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gin-gonic) Report
> Merging [#3489](https://codecov.io/gh/gin-gonic/gin/pull/3489?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gin-gonic) (7019960) into [master](https://codecov.io/gh/gin-gonic/gin/commit/c5fd06361b934070e99978c34e1eaef05632bb5b?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gin-gonic) (c5fd063) will **increase** coverage by `0.00%`.
> The diff coverage is `100.00%`.

```diff
@@           Coverage Diff           @@
##           master    #3489   +/-   ##
=======================================
  Coverage   98.63%   98.63%           
=======================================
  Files          42       42           
  Lines        3140     3142    +2     
=======================================
+ Hits         3097     3099    +2     
  Misses         29       29           
  Partials       14       14           
```

| Flag | Coverage Δ | |
|---|---|---|
|  | `98.63% <100.00%> (+<0.01%)` | :arrow_up: |
| go-1.16 | `∅ <ø> (?)` | |
| go-1.17 | `98.53% <100.00%> (+<0.01%)` | :arrow_up: |
| go-1.18 | `98.53% <100.00%> (+<0.01%)` | :arrow_up: |
| go-1.19 | `98.63% <100.00%> (+<0.01%)` | :arrow_up: |
| macos-latest | `98.53% <100.00%> (+<0.01%)` | :arrow_up: |
| ubuntu-latest | `98.63% <100.00%> (+<0.01%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gin-gonic#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/gin-gonic/gin/pull/3489?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gin-gonic) | Coverage Δ | |
|---|---|---|
| [response\_writer.go](https://codecov.io/gh/gin-gonic/gin/pull/3489?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gin-gonic#diff-cmVzcG9uc2Vfd3JpdGVyLmdv) | `93.87% <100.00%> (+0.26%)` | :arrow_up: |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gin-gonic). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gin-gonic)


Patch:
diff --git a/.github/workflows/gin.yml b/.github/workflows/gin.yml
index 9bd2698..fac97d4 100644
--- a/.github/workflows/gin.yml
+++ b/.github/workflows/gin.yml
@@ -31,7 +31,7 @@ jobs:
     strategy:
       matrix:
         os: [ubuntu-latest, macos-latest]
-        go: [1.16, 1.17, 1.18, 1.19]
+        go: ['1.16', '1.17', '1.18', '1.19', '1.20']
         test-tags: ['', '-tags nomsgpack', '-tags "sonic avx"', '-tags go_json']
         include:
           - os: ubuntu-latest
diff --git a/context.go b/context.go
index 0474252..556f8ac 100644
--- a/context.go
+++ b/context.go
@@ -924,7 +924,9 @@ func (c *Context) Render(code int, r render.Render) {
 	}
 
 	if err := r.Render(c.Writer); err != nil {
-		panic(err)
+		// Pushing error to c.Errors
+		_ = c.Error(err)
+		c.Abort()
 	}
 }
 
diff --git a/go.mod b/go.mod
index ea2c94a..51353f5 100644
--- a/go.mod
+++ b/go.mod
@@ -3,7 +3,7 @@ module github.com/gin-gonic/gin
 go 1.18
 
 require (
-	github.com/bytedance/sonic v1.7.0
+	github.com/bytedance/sonic v1.7.1
 	github.com/gin-contrib/sse v0.1.0
 	github.com/go-playground/validator/v10 v10.11.2
 	github.com/goccy/go-json v0.10.0
@@ -11,7 +11,7 @@ require (
 	github.com/mattn/go-isatty v0.0.17
 	github.com/pelletier/go-toml/v2 v2.0.6
 	github.com/stretchr/testify v1.8.1
-	github.com/ugorji/go/codec v1.2.8
+	github.com/ugorji/go/codec v1.2.9
 	golang.org/x/net v0.5.0
 	google.golang.org/protobuf v1.28.1
 	gopkg.in/yaml.v3 v3.0.1
diff --git a/go.sum b/go.sum
index 09341d4..01f9495 100644
--- a/go.sum
+++ b/go.sum
@@ -1,6 +1,6 @@
 github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
-github.com/bytedance/sonic v1.7.0 h1:P7DyGrkLbVDzcuqagPsSFnAwwljjhmB3qVF5wzmHOxE=
-github.com/bytedance/sonic v1.7.0/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
+github.com/bytedance/sonic v1.7.1 h1:UYWEKUHQDye89c2U6zvrvuxWdGCI/wCrZITFQmKGtGc=
+github.com/bytedance/sonic v1.7.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
 github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
 github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
 github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
@@ -56,8 +56,8 @@ github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKs
 github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
 github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
 github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
-github.com/ugorji/go/codec v1.2.8 h1:sgBJS6COt0b/P40VouWKdseidkDgHxYGm0SAglUHfP0=
-github.com/ugorji/go/codec v1.2.8/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
+github.com/ugorji/go/codec v1.2.9 h1:rmenucSohSTiyL09Y+l2OCk+FrMxGMzho2+tjr5ticU=
+github.com/ugorji/go/codec v1.2.9/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
 golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
 golang.org/x/arch v0.0.0-20220412001346-fc48f9fe4c15 h1:GVfVkciLYxn5mY5EncwAe0SXUn9Rm81rRkZ0TTmn/cU=
 golang.org/x/arch v0.0.0-20220412001346-fc48f9fe4c15/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
diff --git a/render/json.go b/render/json.go
index af678e8..fc8dea4 100644
--- a/render/json.go
+++ b/render/json.go
@@ -53,11 +53,8 @@ var (
 )
 
 // Render (JSON) writes data with custom ContentType.
-func (r JSON) Render(w http.ResponseWriter) (err error) {
-	if err = WriteJSON(w, r.Data); err != nil {
-		panic(err)
-	}
-	return
+func (r JSON) Render(w http.ResponseWriter) error {
+	return WriteJSON(w, r.Data)
 }
 
 // WriteContentType (JSON) writes JSON ContentType.
diff --git a/response_writer.go b/response_writer.go
index 5cec1f6..753a0b0 100644
--- a/response_writer.go
+++ b/response_writer.go
@@ -51,6 +51,10 @@ type responseWriter struct {
 
 var _ ResponseWriter = (*responseWriter)(nil)
 
+func (w *responseWriter) Unwrap() http.ResponseWriter {
+	return w.ResponseWriter
+}
+
 func (w *responseWriter) reset(writer http.ResponseWriter) {
 	w.ResponseWriter = writer
 	w.size = noWritten
diff --git a/routergroup.go b/routergroup.go
index dfbdd7b..c833fe8 100644
--- a/routergroup.go
+++ b/routergroup.go
@@ -42,6 +42,7 @@ type IRoutes interface {
 	PUT(string, ...HandlerFunc) IRoutes
 	OPTIONS(string, ...HandlerFunc) IRoutes
 	HEAD(string, ...HandlerFunc) IRoutes
+	Match([]string, string, ...HandlerFunc) IRoutes
 
 	StaticFile(string, string) IRoutes
 	StaticFileFS(string, string, http.FileSystem) IRoutes
@@ -151,6 +152,15 @@ func (group *RouterGroup) Any(relativePath string, handlers ...HandlerFunc) IRou
 	return group.returnObj()
 }
 
+// Match registers a route that matches the specified methods that you declared.
+func (group *RouterGroup) Match(methods []string, relativePath string, handlers ...HandlerFunc) IRoutes {
+	for _, method := range methods {
+		group.handle(method, relativePath, handlers)
+	}
+
+	return group.returnObj()
+}
+
 // StaticFile registers a single route in order to serve a single file of the local filesystem.
 // router.StaticFile("favicon.ico", "./resources/favicon.ico")
 func (group *RouterGroup) StaticFile(relativePath, filepath string) IRoutes {


Test patch:
diff --git a/context_test.go b/context_test.go
index 827ee0f..1ab6b33 100644
--- a/context_test.go
+++ b/context_test.go
@@ -32,6 +32,8 @@ import (
 
 var _ context.Context = (*Context)(nil)
 
+var errTestRender = errors.New("TestRender")
+
 // Unit tests TODO
 // func (c *Context) File(filepath string) {
 // func (c *Context) Negotiate(code int, config Negotiate) {
@@ -643,25 +645,21 @@ func TestContextBodyAllowedForStatus(t *testing.T) {
 	assert.True(t, true, bodyAllowedForStatus(http.StatusInternalServerError))
 }
 
-type TestPanicRender struct{}
+type TestRender struct{}
 
-func (*TestPanicRender) Render(http.ResponseWriter) error {
-	return errors.New("TestPanicRender")
+func (*TestRender) Render(http.ResponseWriter) error {
+	return errTestRender
 }
 
-func (*TestPanicRender) WriteContentType(http.ResponseWriter) {}
+func (*TestRender) WriteContentType(http.ResponseWriter) {}
 
-func TestContextRenderPanicIfErr(t *testing.T) {
-	defer func() {
-		r := recover()
-		assert.Equal(t, "TestPanicRender", fmt.Sprint(r))
-	}()
+func TestContextRenderIfErr(t *testing.T) {
 	w := httptest.NewRecorder()
 	c, _ := CreateTestContext(w)
 
-	c.Render(http.StatusOK, &TestPanicRender{})
+	c.Render(http.StatusOK, &TestRender{})
 
-	assert.Fail(t, "Panic not detected")
+	assert.Equal(t, errorMsgs{&Error{Err: errTestRender, Type: 1}}, c.Errors)
 }
 
 // Tests that the response is serialized as JSON
diff --git a/render/render_test.go b/render/render_test.go
index ebb7d41..1925525 100644
--- a/render/render_test.go
+++ b/render/render_test.go
@@ -40,12 +40,12 @@ func TestRenderJSON(t *testing.T) {
 	assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
 }
 
-func TestRenderJSONPanics(t *testing.T) {
+func TestRenderJSONError(t *testing.T) {
 	w := httptest.NewRecorder()
 	data := make(chan int)
 
 	// json: unsupported type: chan int
-	assert.Panics(t, func() { assert.NoError(t, (JSON{data}).Render(w)) })
+	assert.Error(t, (JSON{data}).Render(w))
 }
 
 func TestRenderIndentedJSON(t *testing.T) {
diff --git a/response_writer_test.go b/response_writer_test.go
index 6fa5ec7..9fd5e87 100644
--- a/response_writer_test.go
+++ b/response_writer_test.go
@@ -30,6 +30,12 @@ func init() {
 	SetMode(TestMode)
 }
 
+func TestResponseWriterUnwrap(t *testing.T) {
+	testWriter := httptest.NewRecorder()
+	writer := &responseWriter{ResponseWriter: testWriter}
+	assert.Same(t, testWriter, writer.Unwrap())
+}
+
 func TestResponseWriterReset(t *testing.T) {
 	testWriter := httptest.NewRecorder()
 	writer := &responseWriter{}
diff --git a/routergroup_test.go b/routergroup_test.go
index 41f9637..6848063 100644
--- a/routergroup_test.go
+++ b/routergroup_test.go
@@ -186,6 +186,7 @@ func testRoutesInterface(t *testing.T, r IRoutes) {
 	assert.Equal(t, r, r.PUT("/", handler))
 	assert.Equal(t, r, r.OPTIONS("/", handler))
 	assert.Equal(t, r, r.HEAD("/", handler))
+	assert.Equal(t, r, r.Match([]string{http.MethodPut, http.MethodPatch}, "/match", handler))
 
 	assert.Equal(t, r, r.StaticFile("/file", "."))
 	assert.Equal(t, r, r.StaticFileFS("/static2", ".", Dir(".", false)))