Repository: zeromicro/go-zero
Pull request #1672 (2022-03-20 14:46:55)

fix -cache=true insert no clean cache
在生成model代码指定-cache=true时，如果表中只有主键没有其他唯一索引时候，insert是不删除缓存的，但是findone会查缓存，这样会有问题在findone不存在的数据时候会缓存* ，那先findone在insert就会有问题

When the generated model code specifies - cache = true, if there is only a primary key in the table and there is no other unique index, the insert will not delete the cache, but findone will check the cache, which will cause problems. If there is no data in findone, it will cache *, then findone will have problems in the insert first


Patch:
diff --git a/LICENSE b/LICENSE
index 1a2288a4..7f9cdc90 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
 MIT License
 
-Copyright (c) 2020 xiaoheiban_server_go
+Copyright (c) 2022 zeromicro
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
diff --git a/core/prof/runtime.go b/core/prof/runtime.go
index 085440ea..17ede442 100644
--- a/core/prof/runtime.go
+++ b/core/prof/runtime.go
@@ -11,6 +11,7 @@ const (
 	mega            = 1024 * 1024
 )
 
+// DisplayStats prints the goroutine, memory, GC stats with given interval, default to 5 seconds.
 func DisplayStats(interval ...time.Duration) {
 	duration := defaultInterval
 	for _, val := range interval {
diff --git a/core/stores/kv/store.go b/core/stores/kv/store.go
index 603f14a6..d39334cc 100644
--- a/core/stores/kv/store.go
+++ b/core/stores/kv/store.go
@@ -24,6 +24,7 @@ type (
 		Expire(key string, seconds int) error
 		Expireat(key string, expireTime int64) error
 		Get(key string) (string, error)
+		GetSet(key, value string) (string, error)
 		Hdel(key, field string) (bool, error)
 		Hexists(key, field string) (bool, error)
 		Hget(key, field string) (string, error)
@@ -459,6 +460,15 @@ func (cs clusterStore) SetnxEx(key, value string, seconds int) (bool, error) {
 	return node.SetnxEx(key, value, seconds)
 }
 
+func (cs clusterStore) GetSet(key, value string) (string, error) {
+	node, err := cs.getRedis(key)
+	if err != nil {
+		return "", err
+	}
+
+	return node.GetSet(key, value)
+}
+
 func (cs clusterStore) Sismember(key string, value interface{}) (bool, error) {
 	node, err := cs.getRedis(key)
 	if err != nil {
diff --git a/core/stores/redis/redis.go b/core/stores/redis/redis.go
index aff71591..cb6cbcbd 100644
--- a/core/stores/redis/redis.go
+++ b/core/stores/redis/redis.go
@@ -640,6 +640,29 @@ func (s *Redis) GetBitCtx(ctx context.Context, key string, offset int64) (val in
 	return
 }
 
+// GetSet is the implementation of redis getset command.
+func (s *Redis) GetSet(key, value string) (string, error) {
+	return s.GetSetCtx(context.Background(), key, value)
+}
+
+// GetSetCtx is the implementation of redis getset command.
+func (s *Redis) GetSetCtx(ctx context.Context, key, value string) (val string, err error) {
+	err = s.brk.DoWithAcceptable(func() error {
+		conn, err := getRedis(s)
+		if err != nil {
+			return err
+		}
+
+		if val, err = conn.GetSet(ctx, key, value).Result(); err == red.Nil {
+			return nil
+		}
+
+		return err
+	}, acceptable)
+
+	return
+}
+
 // Hdel is the implementation of redis hdel command.
 func (s *Redis) Hdel(key string, fields ...string) (bool, error) {
 	return s.HdelCtx(context.Background(), key, fields...)
diff --git a/rest/httpc/responses.go b/rest/httpc/responses.go
new file mode 100644
index 00000000..7147ad64
--- /dev/null
+++ b/rest/httpc/responses.go
@@ -0,0 +1,36 @@
+package httpc
+
+import (
+	"net/http"
+	"strings"
+
+	"github.com/zeromicro/go-zero/core/mapping"
+	"github.com/zeromicro/go-zero/rest/internal/encoding"
+)
+
+// Parse parses the response.
+func Parse(resp *http.Response, val interface{}) error {
+	if err := ParseHeaders(resp, val); err != nil {
+		return err
+	}
+
+	return ParseJsonBody(resp, val)
+}
+
+// ParseHeaders parses the rsponse headers.
+func ParseHeaders(resp *http.Response, val interface{}) error {
+	return encoding.ParseHeaders(resp.Header, val)
+}
+
+// ParseJsonBody parses the rsponse body, which should be in json content type.
+func ParseJsonBody(resp *http.Response, val interface{}) error {
+	if withJsonBody(resp) {
+		return mapping.UnmarshalJsonReader(resp.Body, val)
+	}
+
+	return mapping.UnmarshalJsonMap(nil, val)
+}
+
+func withJsonBody(r *http.Response) bool {
+	return r.ContentLength > 0 && strings.Contains(r.Header.Get(contentType), applicationJson)
+}
diff --git a/rest/httpc/service.go b/rest/httpc/service.go
index db17b280..8297a3df 100644
--- a/rest/httpc/service.go
+++ b/rest/httpc/service.go
@@ -9,9 +9,6 @@ import (
 	"github.com/zeromicro/go-zero/rest/httpc/internal"
 )
 
-// ContentType means Content-Type.
-const ContentType = "Content-Type"
-
 var interceptors = []internal.Interceptor{
 	internal.LogInterceptor,
 }
@@ -86,13 +83,13 @@ func (s namedService) Get(url string) (*http.Response, error) {
 }
 
 // Post sends an HTTP POST request to the service.
-func (s namedService) Post(url, contentType string, body io.Reader) (*http.Response, error) {
+func (s namedService) Post(url, ctype string, body io.Reader) (*http.Response, error) {
 	r, err := http.NewRequest(http.MethodPost, url, body)
 	if err != nil {
 		return nil, err
 	}
 
-	r.Header.Set(ContentType, contentType)
+	r.Header.Set(contentType, ctype)
 	return s.Do(r)
 }
 
diff --git a/rest/httpc/vars.go b/rest/httpc/vars.go
new file mode 100644
index 00000000..32d68da9
--- /dev/null
+++ b/rest/httpc/vars.go
@@ -0,0 +1,6 @@
+package httpc
+
+const (
+	contentType     = "Content-Type"
+	applicationJson = "application/json"
+)
diff --git a/rest/httpx/requests.go b/rest/httpx/requests.go
index 46c879bb..4b3da146 100644
--- a/rest/httpx/requests.go
+++ b/rest/httpx/requests.go
@@ -3,17 +3,16 @@ package httpx
 import (
 	"io"
 	"net/http"
-	"net/textproto"
 	"strings"
 
 	"github.com/zeromicro/go-zero/core/mapping"
+	"github.com/zeromicro/go-zero/rest/internal/encoding"
 	"github.com/zeromicro/go-zero/rest/pathvar"
 )
 
 const (
 	formKey           = "form"
 	pathKey           = "path"
-	headerKey         = "header"
 	maxMemory         = 32 << 20 // 32MB
 	maxBodyLen        = 8 << 20  // 8MB
 	separator         = ";"
@@ -21,10 +20,8 @@ const (
 )
 
 var (
-	formUnmarshaler   = mapping.NewUnmarshaler(formKey, mapping.WithStringValues())
-	pathUnmarshaler   = mapping.NewUnmarshaler(pathKey, mapping.WithStringValues())
-	headerUnmarshaler = mapping.NewUnmarshaler(headerKey, mapping.WithStringValues(),
-		mapping.WithCanonicalKeyFunc(textproto.CanonicalMIMEHeaderKey))
+	formUnmarshaler = mapping.NewUnmarshaler(formKey, mapping.WithStringValues())
+	pathUnmarshaler = mapping.NewUnmarshaler(pathKey, mapping.WithStringValues())
 )
 
 // Parse parses the request.
@@ -46,16 +43,7 @@ func Parse(r *http.Request, v interface{}) error {
 
 // ParseHeaders parses the headers request.
 func ParseHeaders(r *http.Request, v interface{}) error {
-	m := map[string]interface{}{}
-	for k, v := range r.Header {
-		if len(v) == 1 {
-			m[k] = v[0]
-		} else {
-			m[k] = v
-		}
-	}
-
-	return headerUnmarshaler.Unmarshal(m, v)
+	return encoding.ParseHeaders(r.Header, v)
 }
 
 // ParseForm parses the form request.
diff --git a/rest/internal/encoding/parser.go b/rest/internal/encoding/parser.go
new file mode 100644
index 00000000..b9bfec05
--- /dev/null
+++ b/rest/internal/encoding/parser.go
@@ -0,0 +1,27 @@
+package encoding
+
+import (
+	"net/http"
+	"net/textproto"
+
+	"github.com/zeromicro/go-zero/core/mapping"
+)
+
+const headerKey = "header"
+
+var headerUnmarshaler = mapping.NewUnmarshaler(headerKey, mapping.WithStringValues(),
+	mapping.WithCanonicalKeyFunc(textproto.CanonicalMIMEHeaderKey))
+
+// ParseHeaders parses the headers request.
+func ParseHeaders(header http.Header, v interface{}) error {
+	m := map[string]interface{}{}
+	for k, v := range header {
+		if len(v) == 1 {
+			m[k] = v[0]
+		} else {
+			m[k] = v
+		}
+	}
+
+	return headerUnmarshaler.Unmarshal(m, v)
+}
diff --git a/tools/goctl/env/check.go b/tools/goctl/env/check.go
index 044e39e8..45849f8b 100644
--- a/tools/goctl/env/check.go
+++ b/tools/goctl/env/check.go
@@ -40,21 +40,23 @@ var bins = []bin{
 func Check(ctx *cli.Context) error {
 	install := ctx.Bool("install")
 	force := ctx.Bool("force")
-	return Prepare(install, force)
+	verbose := ctx.Bool("verbose")
+	return Prepare(install, force, verbose)
 }
 
-func Prepare(install, force bool) error {
+func Prepare(install, force, verbose bool) error {
+	log := console.NewColorConsole(verbose)
 	pending := true
-	console.Info("[goctl-env]: preparing to check env")
+	log.Info("[goctl-env]: preparing to check env")
 	defer func() {
 		if p := recover(); p != nil {
-			console.Error("%+v", p)
+			log.Error("%+v", p)
 			return
 		}
 		if pending {
-			console.Success("\n[goctl-env]: congratulations! your goctl environment is ready!")
+			log.Success("\n[goctl-env]: congratulations! your goctl environment is ready!")
 		} else {
-			console.Error(`
+			log.Error(`
 [goctl-env]: check env finish, some dependencies is not found in PATH, you can execute
 command 'goctl env check --install' to install it, for details, please execute command 
 'goctl env check --help'`)
@@ -62,29 +64,29 @@ command 'goctl env check --install' to install it, for details, please execute c
 	}()
 	for _, e := range bins {
 		time.Sleep(200 * time.Millisecond)
-		console.Info("")
-		console.Info("[goctl-env]: looking up %q", e.name)
+		log.Info("")
+		log.Info("[goctl-env]: looking up %q", e.name)
 		if e.exists {
-			console.Success("[goctl-env]: %q is installed", e.name)
+			log.Success("[goctl-env]: %q is installed", e.name)
 			continue
 		}
-		console.Warning("[goctl-env]: %q is not found in PATH", e.name)
+		log.Warning("[goctl-env]: %q is not found in PATH", e.name)
 		if install {
 			install := func() {
-				console.Info("[goctl-env]: preparing to install %q", e.name)
+				log.Info("[goctl-env]: preparing to install %q", e.name)
 				path, err := e.get(env.Get(env.GoctlCache))
 				if err != nil {
-					console.Error("[goctl-env]: an error interrupted the installation: %+v", err)
+					log.Error("[goctl-env]: an error interrupted the installation: %+v", err)
 					pending = false
 				} else {
-					console.Success("[goctl-env]: %q is already installed in %q", e.name, path)
+					log.Success("[goctl-env]: %q is already installed in %q", e.name, path)
 				}
 			}
 			if force {
 				install()
 				continue
 			}
-			console.Info("[goctl-env]: do you want to install %q [y: YES, n: No]", e.name)
+			log.Info("[goctl-env]: do you want to install %q [y: YES, n: No]", e.name)
 			for {
 				var in string
 				fmt.Scanln(&in)
@@ -95,10 +97,10 @@ command 'goctl env check --install' to install it, for details, please execute c
 					brk = true
 				case strings.EqualFold(in, "n"):
 					pending = false
-					console.Info("[goctl-env]: %q installation is ignored", e.name)
+					log.Info("[goctl-env]: %q installation is ignored", e.name)
 					brk = true
 				default:
-					console.Error("[goctl-env]: invalid input, input 'y' for yes, 'n' for no")
+					log.Error("[goctl-env]: invalid input, input 'y' for yes, 'n' for no")
 				}
 				if brk {
 					break
diff --git a/tools/goctl/goctl.go b/tools/goctl/goctl.go
index 308c5aab..e91d7f8f 100644
--- a/tools/goctl/goctl.go
+++ b/tools/goctl/goctl.go
@@ -23,7 +23,6 @@ import (
 	"github.com/zeromicro/go-zero/tools/goctl/completion"
 	"github.com/zeromicro/go-zero/tools/goctl/docker"
 	"github.com/zeromicro/go-zero/tools/goctl/env"
-	"github.com/zeromicro/go-zero/tools/goctl/internal/errorx"
 	"github.com/zeromicro/go-zero/tools/goctl/internal/version"
 	"github.com/zeromicro/go-zero/tools/goctl/kube"
 	"github.com/zeromicro/go-zero/tools/goctl/migrate"
@@ -70,6 +69,10 @@ var commands = []cli.Command{
 						Name:  "force, f",
 						Usage: "silent installation of non-existent dependencies",
 					},
+					cli.BoolFlag{
+						Name:  "verbose, v",
+						Usage: "enable log output",
+					},
 				},
 				Action: env.Check,
 			},
@@ -519,6 +522,10 @@ var commands = []cli.Command{
 						Name:  "branch",
 						Usage: "the branch of the remote repo, it does work with --remote",
 					},
+					cli.BoolFlag{
+						Name:  "verbose, v",
+						Usage: "enable log output",
+					},
 				},
 				Action: rpc.RPCNew,
 			},
@@ -601,6 +608,10 @@ var commands = []cli.Command{
 						Name:  "branch",
 						Usage: "the branch of the remote repo, it does work with --remote",
 					},
+					cli.BoolFlag{
+						Name:  "verbose, v",
+						Usage: "enable log output",
+					},
 				},
 			},
 		},
@@ -894,7 +905,7 @@ func main() {
 
 	// cli already print error messages.
 	if err := app.Run(os.Args); err != nil {
-		fmt.Println(aurora.Red(errorx.Wrap(err).Error()))
+		fmt.Println(aurora.Red(err.Error()))
 		os.Exit(codeFailure)
 	}
 }
diff --git a/tools/goctl/model/sql/gen/insert.go b/tools/goctl/model/sql/gen/insert.go
index 46c28748..1ca7b312 100644
--- a/tools/goctl/model/sql/gen/insert.go
+++ b/tools/goctl/model/sql/gen/insert.go
@@ -55,7 +55,6 @@ func genInsert(table Table, withCache, postgreSql bool) (string, string, error)
 		Parse(text).
 		Execute(map[string]interface{}{
 			"withCache":             withCache,
-			"containsIndexCache":    table.ContainsUniqueCacheKey,
 			"upperStartCamelObject": camel,
 			"lowerStartCamelObject": stringx.From(camel).Untitle(),
 			"expression":            strings.Join(expressions, ", "),
diff --git a/tools/goctl/model/sql/template/insert.go b/tools/goctl/model/sql/template/insert.go
index 115ea7ff..0339808c 100644
--- a/tools/goctl/model/sql/template/insert.go
+++ b/tools/goctl/model/sql/template/insert.go
@@ -3,13 +3,11 @@ package template
 // Insert defines a template for insert code in model
 var Insert = `
 func (m *default{{.upperStartCamelObject}}Model) Insert(ctx context.Context, data *{{.upperStartCamelObject}}) (sql.Result,error) {
-	{{if .withCache}}{{if .containsIndexCache}}{{.keys}}
+	{{if .withCache}}{{.keys}}
     ret, err := m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) {
 		query := fmt.Sprintf("insert into %s (%s) values ({{.expression}})", m.table, {{.lowerStartCamelObject}}RowsExpectAutoSet)
 		return conn.ExecCtx(ctx, query, {{.expressionValues}})
 	}, {{.keyValues}}){{else}}query := fmt.Sprintf("insert into %s (%s) values ({{.expression}})", m.table, {{.lowerStartCamelObject}}RowsExpectAutoSet)
-    ret,err:=m.ExecNoCacheCtx(ctx, query, {{.expressionValues}})
-	{{end}}{{else}}query := fmt.Sprintf("insert into %s (%s) values ({{.expression}})", m.table, {{.lowerStartCamelObject}}RowsExpectAutoSet)
     ret,err:=m.conn.ExecCtx(ctx, query, {{.expressionValues}}){{end}}
 	return ret,err
 }
diff --git a/tools/goctl/rpc/cli/cli.go b/tools/goctl/rpc/cli/cli.go
index b9716ac6..be98b315 100644
--- a/tools/goctl/rpc/cli/cli.go
+++ b/tools/goctl/rpc/cli/cli.go
@@ -23,6 +23,7 @@ func RPCNew(c *cli.Context) error {
 	home := c.String("home")
 	remote := c.String("remote")
 	branch := c.String("branch")
+	verbose := c.Bool("verbose")
 	if len(remote) > 0 {
 		repo, _ := util.CloneIntoGitHome(remote, branch)
 		if len(repo) > 0 {
@@ -52,7 +53,7 @@ func RPCNew(c *cli.Context) error {
 	ctx.IsGooglePlugin = true
 	ctx.Output = filepath.Dir(src)
 	ctx.ProtocCmd = fmt.Sprintf("protoc -I=%s %s --go_out=%s --go-grpc_out=%s", filepath.Dir(src), filepath.Base(src), filepath.Dir(src), filepath.Dir(src))
-	g := generator.NewGenerator(style)
+	g := generator.NewGenerator(style, verbose)
 	return g.Generate(&ctx)
 }
 
diff --git a/tools/goctl/rpc/cli/zrpc.go b/tools/goctl/rpc/cli/zrpc.go
index e0ab934b..fa84d8ea 100644
--- a/tools/goctl/rpc/cli/zrpc.go
+++ b/tools/goctl/rpc/cli/zrpc.go
@@ -42,6 +42,7 @@ func ZRPC(c *cli.Context) error {
 	home := c.String("home")
 	remote := c.String("remote")
 	branch := c.String("branch")
+	verbose := c.Bool("verbose")
 	if len(grpcOutList) == 0 {
 		return errInvalidGrpcOutput
 	}
@@ -107,7 +108,7 @@ func ZRPC(c *cli.Context) error {
 	ctx.IsGooglePlugin = isGooglePlugin
 	ctx.Output = zrpcOut
 	ctx.ProtocCmd = strings.Join(protocArgs, " ")
-	g := generator.NewGenerator(style)
+	g := generator.NewGenerator(style, verbose)
 	return g.Generate(&ctx)
 }
 
@@ -117,11 +118,13 @@ func removeGoctlFlag(args []string) []string {
 	for step < len(args) {
 		arg := args[step]
 		switch {
-		case arg == "--style", arg == "--home", arg == "--zrpc_out":
+		case arg == "--style", arg == "--home", arg == "--zrpc_out", arg == "--verbose", arg == "-v":
 			step += 2
 			continue
 		case strings.HasPrefix(arg, "--style="),
 			strings.HasPrefix(arg, "--home="),
+			strings.HasPrefix(arg, "--verbose="),
+			strings.HasPrefix(arg, "-v="),
 			strings.HasPrefix(arg, "--zrpc_out="):
 			step += 1
 			continue
diff --git a/tools/goctl/rpc/generator/generator.go b/tools/goctl/rpc/generator/generator.go
index c66627cc..3786e3f8 100644
--- a/tools/goctl/rpc/generator/generator.go
+++ b/tools/goctl/rpc/generator/generator.go
@@ -10,25 +10,27 @@ import (
 
 // Generator defines the environment needs of rpc service generation
 type Generator struct {
-	log console.Console
-	cfg *conf.Config
+	log     console.Console
+	cfg     *conf.Config
+	verbose bool
 }
 
 // NewGenerator returns an instance of Generator
-func NewGenerator(style string) *Generator {
+func NewGenerator(style string, verbose bool) *Generator {
 	cfg, err := conf.NewConfig(style)
 	if err != nil {
 		log.Fatalln(err)
 	}
-	log := console.NewColorConsole()
+	log := console.NewColorConsole(verbose)
 	return &Generator{
-		log: log,
-		cfg: cfg,
+		log:     log,
+		cfg:     cfg,
+		verbose: verbose,
 	}
 }
 
 // Prepare provides environment detection generated by rpc service,
 // including go environment, protoc, whether protoc-gen-go is installed or not
 func (g *Generator) Prepare() error {
-	return env.Prepare(true, true)
+	return env.Prepare(true, true, g.verbose)
 }
diff --git a/tools/goctl/util/console/console.go b/tools/goctl/util/console/console.go
index 7f160d9c..726a3f71 100644
--- a/tools/goctl/util/console/console.go
+++ b/tools/goctl/util/console/console.go
@@ -24,7 +24,9 @@ type (
 		Must(err error)
 	}
 
-	colorConsole struct{}
+	colorConsole struct {
+		enable bool
+	}
 
 	// for idea log
 	ideaConsole struct{}
@@ -39,45 +41,75 @@ func NewConsole(idea bool) Console {
 }
 
 // NewColorConsole returns an instance of colorConsole
-func NewColorConsole() Console {
-	return &colorConsole{}
+func NewColorConsole(enable ...bool) Console {
+	logEnable := true
+	for _, e := range enable {
+		logEnable = e
+	}
+	return &colorConsole{
+		enable: logEnable,
+	}
 }
 
 func (c *colorConsole) Info(format string, a ...interface{}) {
+	if !c.enable {
+		return
+	}
 	msg := fmt.Sprintf(format, a...)
 	fmt.Println(msg)
 }
 
 func (c *colorConsole) Debug(format string, a ...interface{}) {
+	if !c.enable {
+		return
+	}
 	msg := fmt.Sprintf(format, a...)
 	println(aurora.BrightCyan(msg))
 }
 
 func (c *colorConsole) Success(format string, a ...interface{}) {
+	if !c.enable {
+		return
+	}
 	msg := fmt.Sprintf(format, a...)
 	println(aurora.BrightGreen(msg))
 }
 
 func (c *colorConsole) Warning(format string, a ...interface{}) {
+	if !c.enable {
+		return
+	}
 	msg := fmt.Sprintf(format, a...)
 	println(aurora.BrightYellow(msg))
 }
 
 func (c *colorConsole) Error(format string, a ...interface{}) {
+	if !c.enable {
+		return
+	}
 	msg := fmt.Sprintf(format, a...)
 	println(aurora.BrightRed(msg))
 }
 
 func (c *colorConsole) Fatalln(format string, a ...interface{}) {
+	if !c.enable {
+		return
+	}
 	c.Error(format, a...)
 	os.Exit(1)
 }
 
 func (c *colorConsole) MarkDone() {
+	if !c.enable {
+		return
+	}
 	c.Success("Done.")
 }
 
 func (c *colorConsole) Must(err error) {
+	if !c.enable {
+		return
+	}
 	if err != nil {
 		c.Fatalln("%+v", err)
 	}


Test patch:
diff --git a/core/stores/kv/store_test.go b/core/stores/kv/store_test.go
index 0a8f44d2..1803d015 100644
--- a/core/stores/kv/store_test.go
+++ b/core/stores/kv/store_test.go
@@ -490,6 +490,29 @@ func TestRedis_SetExNx(t *testing.T) {
 	})
 }
 
+func TestRedis_Getset(t *testing.T) {
+	store := clusterStore{dispatcher: hash.NewConsistentHash()}
+	_, err := store.GetSet("hello", "world")
+	assert.NotNil(t, err)
+
+	runOnCluster(t, func(client Store) {
+		val, err := client.GetSet("hello", "world")
+		assert.Nil(t, err)
+		assert.Equal(t, "", val)
+		val, err = client.Get("hello")
+		assert.Nil(t, err)
+		assert.Equal(t, "world", val)
+		val, err = client.GetSet("hello", "newworld")
+		assert.Nil(t, err)
+		assert.Equal(t, "world", val)
+		val, err = client.Get("hello")
+		assert.Nil(t, err)
+		assert.Equal(t, "newworld", val)
+		_, err = client.Del("hello")
+		assert.Nil(t, err)
+	})
+}
+
 func TestRedis_SetGetDelHashField(t *testing.T) {
 	store := clusterStore{dispatcher: hash.NewConsistentHash()}
 	err := store.Hset("key", "field", "value")
diff --git a/core/stores/redis/redis_test.go b/core/stores/redis/redis_test.go
index 1c196e85..fe8aa7ca 100644
--- a/core/stores/redis/redis_test.go
+++ b/core/stores/redis/redis_test.go
@@ -701,6 +701,28 @@ func TestRedis_Set(t *testing.T) {
 	})
 }
 
+func TestRedis_GetSet(t *testing.T) {
+	runOnRedis(t, func(client *Redis) {
+		_, err := New(client.Addr, badType()).GetSet("hello", "world")
+		assert.NotNil(t, err)
+		val, err := client.GetSet("hello", "world")
+		assert.Nil(t, err)
+		assert.Equal(t, "", val)
+		val, err = client.Get("hello")
+		assert.Nil(t, err)
+		assert.Equal(t, "world", val)
+		val, err = client.GetSet("hello", "newworld")
+		assert.Nil(t, err)
+		assert.Equal(t, "world", val)
+		val, err = client.Get("hello")
+		assert.Nil(t, err)
+		assert.Equal(t, "newworld", val)
+		ret, err := client.Del("hello")
+		assert.Nil(t, err)
+		assert.Equal(t, 1, ret)
+	})
+}
+
 func TestRedis_SetGetDel(t *testing.T) {
 	runOnRedis(t, func(client *Redis) {
 		err := New(client.Addr, badType()).Set("hello", "world")
diff --git a/rest/httpc/internal/loginterceptor_test.go b/rest/httpc/internal/loginterceptor_test.go
index 108dfdee..39d445d3 100644
--- a/rest/httpc/internal/loginterceptor_test.go
+++ b/rest/httpc/internal/loginterceptor_test.go
@@ -11,6 +11,7 @@ import (
 func TestLogInterceptor(t *testing.T) {
 	svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 	}))
+	defer svr.Close()
 	req, err := http.NewRequest(http.MethodGet, svr.URL, nil)
 	assert.Nil(t, err)
 	req, handler := LogInterceptor(req)
@@ -24,6 +25,7 @@ func TestLogInterceptorServerError(t *testing.T) {
 	svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 		w.WriteHeader(http.StatusInternalServerError)
 	}))
+	defer svr.Close()
 	req, err := http.NewRequest(http.MethodGet, svr.URL, nil)
 	assert.Nil(t, err)
 	req, handler := LogInterceptor(req)
diff --git a/rest/httpc/requests_test.go b/rest/httpc/requests_test.go
index 6696d20f..0deeff2a 100644
--- a/rest/httpc/requests_test.go
+++ b/rest/httpc/requests_test.go
@@ -11,6 +11,7 @@ import (
 func TestDo(t *testing.T) {
 	svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 	}))
+	defer svr.Close()
 	_, err := Get("foo", "tcp://bad request")
 	assert.NotNil(t, err)
 	resp, err := Get("foo", svr.URL)
@@ -20,6 +21,7 @@ func TestDo(t *testing.T) {
 
 func TestDoNotFound(t *testing.T) {
 	svr := httptest.NewServer(http.NotFoundHandler())
+	defer svr.Close()
 	_, err := Post("foo", "tcp://bad request", "application/json", nil)
 	assert.NotNil(t, err)
 	resp, err := Post("foo", svr.URL, "application/json", nil)
@@ -29,6 +31,7 @@ func TestDoNotFound(t *testing.T) {
 
 func TestDoMoved(t *testing.T) {
 	svr := httptest.NewServer(http.RedirectHandler("/foo", http.StatusMovedPermanently))
+	defer svr.Close()
 	req, err := http.NewRequest(http.MethodGet, svr.URL, nil)
 	assert.Nil(t, err)
 	_, err = Do("foo", req)
diff --git a/rest/httpc/responses_test.go b/rest/httpc/responses_test.go
new file mode 100644
index 00000000..13e4acc6
--- /dev/null
+++ b/rest/httpc/responses_test.go
@@ -0,0 +1,58 @@
+package httpc
+
+import (
+	"net/http"
+	"net/http/httptest"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestParse(t *testing.T) {
+	var val struct {
+		Foo   string `header:"foo"`
+		Name  string `json:"name"`
+		Value int    `json:"value"`
+	}
+	svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		w.Header().Set("foo", "bar")
+		w.Header().Set(contentType, applicationJson)
+		w.Write([]byte(`{"name":"kevin","value":100}`))
+	}))
+	defer svr.Close()
+	resp, err := Get("foo", svr.URL)
+	assert.Nil(t, err)
+	assert.Nil(t, Parse(resp, &val))
+	assert.Equal(t, "bar", val.Foo)
+	assert.Equal(t, "kevin", val.Name)
+	assert.Equal(t, 100, val.Value)
+}
+
+func TestParseHeaderError(t *testing.T) {
+	var val struct {
+		Foo int `header:"foo"`
+	}
+	svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		w.Header().Set("foo", "bar")
+		w.Header().Set(contentType, applicationJson)
+	}))
+	defer svr.Close()
+	resp, err := Get("foo", svr.URL)
+	assert.Nil(t, err)
+	assert.NotNil(t, Parse(resp, &val))
+}
+
+func TestParseNoBody(t *testing.T) {
+	var val struct {
+		Foo string `header:"foo"`
+	}
+	svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		w.Header().Set("foo", "bar")
+		w.Header().Set(contentType, applicationJson)
+	}))
+	defer svr.Close()
+	resp, err := Get("foo", svr.URL)
+	assert.Nil(t, err)
+	assert.Nil(t, Parse(resp, &val))
+	assert.Equal(t, "bar", val.Foo)
+}
diff --git a/rest/httpc/service_test.go b/rest/httpc/service_test.go
index f619fe76..f92177a4 100644
--- a/rest/httpc/service_test.go
+++ b/rest/httpc/service_test.go
@@ -10,6 +10,7 @@ import (
 
 func TestNamedService_Do(t *testing.T) {
 	svr := httptest.NewServer(http.RedirectHandler("/foo", http.StatusMovedPermanently))
+	defer svr.Close()
 	req, err := http.NewRequest(http.MethodGet, svr.URL, nil)
 	assert.Nil(t, err)
 	service := NewService("foo")
@@ -22,6 +23,7 @@ func TestNamedService_Get(t *testing.T) {
 	svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 		w.Header().Set("foo", r.Header.Get("foo"))
 	}))
+	defer svr.Close()
 	service := NewService("foo", func(r *http.Request) *http.Request {
 		r.Header.Set("foo", "bar")
 		return r
@@ -34,6 +36,7 @@ func TestNamedService_Get(t *testing.T) {
 
 func TestNamedService_Post(t *testing.T) {
 	svr := httptest.NewServer(http.NotFoundHandler())
+	defer svr.Close()
 	service := NewService("foo")
 	_, err := service.Post("tcp://bad request", "application/json", nil)
 	assert.NotNil(t, err)
diff --git a/rest/internal/encoding/parser_test.go b/rest/internal/encoding/parser_test.go
new file mode 100644
index 00000000..70e001f4
--- /dev/null
+++ b/rest/internal/encoding/parser_test.go
@@ -0,0 +1,40 @@
+package encoding
+
+import (
+	"net/http"
+	"net/http/httptest"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestParseHeaders(t *testing.T) {
+	var val struct {
+		Foo string `header:"foo"`
+		Baz int    `header:"baz"`
+		Qux bool   `header:"qux,default=true"`
+	}
+	r := httptest.NewRequest(http.MethodGet, "/any", nil)
+	r.Header.Set("foo", "bar")
+	r.Header.Set("baz", "1")
+	assert.Nil(t, ParseHeaders(r.Header, &val))
+	assert.Equal(t, "bar", val.Foo)
+	assert.Equal(t, 1, val.Baz)
+	assert.True(t, val.Qux)
+}
+
+func TestParseHeadersMulti(t *testing.T) {
+	var val struct {
+		Foo []string `header:"foo"`
+		Baz int      `header:"baz"`
+		Qux bool     `header:"qux,default=true"`
+	}
+	r := httptest.NewRequest(http.MethodGet, "/any", nil)
+	r.Header.Set("foo", "bar")
+	r.Header.Add("foo", "bar1")
+	r.Header.Set("baz", "1")
+	assert.Nil(t, ParseHeaders(r.Header, &val))
+	assert.Equal(t, []string{"bar", "bar1"}, val.Foo)
+	assert.Equal(t, 1, val.Baz)
+	assert.True(t, val.Qux)
+}