Repository: labstack/echo
Pull request #2574 (2024-01-12 16:49:13)

binder: allow binding to a nil map
Given the following code:

```
var params map[string]interface{} // <- this is nil
c.Bind(&params)
```

Currently, if the context does not define any path or query params, this might work when parsing a JSON body.
If there are path parameters involved, this will panic with:
```
echo: http: panic serving 127.0.0.1:41198: assignment to entry in nil map
goroutine 6 [running]:
net/http.(*conn).serve.func1()
	/usr/lib/golang/src/net/http/server.go:1868 +0x13d
panic({0x845340?, 0x8deaf0?})
	/usr/lib/golang/src/runtime/panic.go:920 +0x290
reflect.mapassign_faststr0(0x842580, 0x100c0000f8b18?, {0x8dc8c0?, 0x0?}, 0x842580?)
	/usr/lib/golang/src/runtime/map.go:1376 +0x25
reflect.mapassign_faststr(0x842580, 0x0, {0x8dc8c0, 0x1}, 0xc000017520)
	/usr/lib/golang/src/reflect/value.go:3837 +0x65
reflect.Value.SetMapIndex({0x842580, 0xc00006a088, 0x195}, {0x8327e0, 0xc000017510, 0x98}, {0x83d3a0, 0xc000017520, 0x94})
	/usr/lib/golang/src/reflect/value.go:2402 +0x2e5
github.com/labstack/echo/v4.(*DefaultBinder).bindData(0xad1780, {0x82ed80, 0xc00006a088}, 0xc0000f91b8, {0x89016d, 0x5})
	/home/georg/go/pkg/mod/github.com/labstack/echo/v4@v4.11.5-0.20231220133251-60fc2fb1b76f/bind.go:152 +0x150d
github.com/labstack/echo/v4.(*DefaultBinder).BindPathParams(0xad1780, {0x8e42b8, 0xc0000aaa00}, {0x82ed80, 0xc00006a088})
	/home/georg/go/pkg/mod/github.com/labstack/echo/v4@v4.11.5-0.20231220133251-60fc2fb1b76f/bind.go:40 +0x33d
github.com/labstack/echo/v4.(*DefaultBinder).Bind(0xad1780, {0x82ed80, 0xc00006a088}, {0x8e42b8, 0xc0000aaa00})
	/home/georg/go/pkg/mod/github.com/labstack/echo/v4@v4.11.5-0.20231220133251-60fc2fb1b76f/bind.go:111 +0x6d
github.com/labstack/echo/v4.(*context).Bind(0xc0000aaa00, {0x82ed80, 0xc00006a088})
	/home/georg/go/pkg/mod/github.com/labstack/echo/v4@v4.11.5-0.20231220133251-60fc2fb1b76f/context.go:439 +0x5d
```

With this patch applied, there are no panics anymore.


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/labstack/echo/pull/2574?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=labstack) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Comparison is base [(`60fc2fb`)](https://app.codecov.io/gh/labstack/echo/commit/60fc2fb1b76f5613fc41aa9315cad6e8c96c6859?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=labstack) 93.02% compared to head [(`f638c84`)](https://app.codecov.io/gh/labstack/echo/pull/2574?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=labstack) 93.02%.
> Report is 4 commits behind head on master.


<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff           @@
##           master    #2574   +/-   ##
=======================================
  Coverage   93.02%   93.02%           
=======================================
  Files          39       39           
  Lines        4673     4676    +3     
=======================================
+ Hits         4347     4350    +3     
  Misses        236      236           
  Partials       90       90           
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/labstack/echo/pull/2574?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=labstack).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=labstack).


Patch:
diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml
index fbd6d95..9ae5dbd 100644
--- a/.github/workflows/checks.yml
+++ b/.github/workflows/checks.yml
@@ -14,7 +14,7 @@ permissions:
 
 env:
   # run static analysis only with the latest Go version
-  LATEST_GO_VERSION: "1.21"
+  LATEST_GO_VERSION: "1.22"
 
 jobs:
   check:
diff --git a/.github/workflows/echo.yml b/.github/workflows/echo.yml
index 5722dcb..cb3dc44 100644
--- a/.github/workflows/echo.yml
+++ b/.github/workflows/echo.yml
@@ -14,7 +14,7 @@ permissions:
 
 env:
   # run coverage and benchmarks only with the latest Go version
-  LATEST_GO_VERSION: "1.21"
+  LATEST_GO_VERSION: "1.22"
 
 jobs:
   test:
@@ -25,7 +25,7 @@ jobs:
         # Echo tests with last four major releases (unless there are pressing vulnerabilities)
         # As we depend on `golang.org/x/` libraries which only support last 2 Go releases we could have situations when
         # we derive from last four major releases promise.
-        go: ["1.18", "1.19", "1.20", "1.21"]
+        go: ["1.19", "1.20", "1.21", "1.22"]
     name: ${{ matrix.os }} @ Go ${{ matrix.go }}
     runs-on: ${{ matrix.os }}
     steps:
diff --git a/bind.go b/bind.go
index 6f41ce5..51f4689 100644
--- a/bind.go
+++ b/bind.go
@@ -145,6 +145,9 @@ func (b *DefaultBinder) bindData(destination interface{}, data map[string][]stri
 		if !(isElemSliceOfStrings || isElemString || isElemInterface) {
 			return nil
 		}
+		if val.IsNil() {
+			val.Set(reflect.MakeMap(typ))
+		}
 		for k, v := range data {
 			if isElemString {
 				val.SetMapIndex(reflect.ValueOf(k), reflect.ValueOf(v[0]))


Test patch:
diff --git a/bind_test.go b/bind_test.go
index c117233..cffccfb 100644
--- a/bind_test.go
+++ b/bind_test.go
@@ -447,6 +447,18 @@ func TestDefaultBinder_bindDataToMap(t *testing.T) {
 		)
 	})
 
+	t.Run("ok, bind to map[string]string with nil map", func(t *testing.T) {
+		var dest map[string]string
+		assert.NoError(t, new(DefaultBinder).bindData(&dest, exampleData, "param"))
+		assert.Equal(t,
+			map[string]string{
+				"multiple": "1",
+				"single":   "3",
+			},
+			dest,
+		)
+	})
+
 	t.Run("ok, bind to map[string][]string", func(t *testing.T) {
 		dest := map[string][]string{}
 		assert.NoError(t, new(DefaultBinder).bindData(&dest, exampleData, "param"))
@@ -459,6 +471,18 @@ func TestDefaultBinder_bindDataToMap(t *testing.T) {
 		)
 	})
 
+	t.Run("ok, bind to map[string][]string with nil map", func(t *testing.T) {
+		var dest map[string][]string
+		assert.NoError(t, new(DefaultBinder).bindData(&dest, exampleData, "param"))
+		assert.Equal(t,
+			map[string][]string{
+				"multiple": {"1", "2"},
+				"single":   {"3"},
+			},
+			dest,
+		)
+	})
+
 	t.Run("ok, bind to map[string]interface", func(t *testing.T) {
 		dest := map[string]interface{}{}
 		assert.NoError(t, new(DefaultBinder).bindData(&dest, exampleData, "param"))
@@ -471,18 +495,41 @@ func TestDefaultBinder_bindDataToMap(t *testing.T) {
 		)
 	})
 
+	t.Run("ok, bind to map[string]interface with nil map", func(t *testing.T) {
+		var dest map[string]interface{}
+		assert.NoError(t, new(DefaultBinder).bindData(&dest, exampleData, "param"))
+		assert.Equal(t,
+			map[string]interface{}{
+				"multiple": []string{"1", "2"},
+				"single":   []string{"3"},
+			},
+			dest,
+		)
+	})
+
 	t.Run("ok, bind to map[string]int skips", func(t *testing.T) {
 		dest := map[string]int{}
 		assert.NoError(t, new(DefaultBinder).bindData(&dest, exampleData, "param"))
 		assert.Equal(t, map[string]int{}, dest)
 	})
 
+	t.Run("ok, bind to map[string]int skips with nil map", func(t *testing.T) {
+		var dest map[string]int
+		assert.NoError(t, new(DefaultBinder).bindData(&dest, exampleData, "param"))
+		assert.Equal(t, map[string]int(nil), dest)
+	})
+
 	t.Run("ok, bind to map[string][]int skips", func(t *testing.T) {
 		dest := map[string][]int{}
 		assert.NoError(t, new(DefaultBinder).bindData(&dest, exampleData, "param"))
 		assert.Equal(t, map[string][]int{}, dest)
 	})
 
+	t.Run("ok, bind to map[string][]int skips with nil map", func(t *testing.T) {
+		var dest map[string][]int
+		assert.NoError(t, new(DefaultBinder).bindData(&dest, exampleData, "param"))
+		assert.Equal(t, map[string][]int(nil), dest)
+	})
 }
 
 func TestBindbindData(t *testing.T) {
diff --git a/echo_test.go b/echo_test.go
index a352e40..4164791 100644
--- a/echo_test.go
+++ b/echo_test.go
@@ -1572,7 +1572,7 @@ func TestEcho_OnAddRouteHandler(t *testing.T) {
 		})
 	}
 
-	e.GET("/static", NotFoundHandler)
+	e.GET("/static", dummyHandler)
 	e.Host("domain.site").GET("/static/*", dummyHandler, func(next HandlerFunc) HandlerFunc {
 		return func(c Context) error {
 			return next(c)
@@ -1582,7 +1582,7 @@ func TestEcho_OnAddRouteHandler(t *testing.T) {
 	assert.Len(t, added, 2)
 
 	assert.Equal(t, "", added[0].host)
-	assert.Equal(t, Route{Method: http.MethodGet, Path: "/static", Name: "github.com/labstack/echo/v4.glob..func1"}, added[0].route)
+	assert.Equal(t, Route{Method: http.MethodGet, Path: "/static", Name: "github.com/labstack/echo/v4.TestEcho_OnAddRouteHandler.func1"}, added[0].route)
 	assert.Len(t, added[0].middleware, 0)
 
 	assert.Equal(t, "domain.site", added[1].host)