Repository: go-kratos/kratos
Pull request #2172 (2022-07-09 03:18:56)

test(transport): add unit test for transport coverage: 91.7% of state…
…ments

<!--
🎉 Thanks for sending a pull request to Kratos! Here are some tips for you:

1. If this is your first time contributing to Kratos, please read our contribution guide: https://go-kratos.dev/en/docs/community/contribution/
2. Ensure you have added or ran the appropriate tests and lint for your PR, please use `make lint` and `make test` before filing your PR, use `make clean` to tidy your go mod.
3. If the PR is unfinished, you may need to mark it as a WIP(Work In Progress) PR or draft PR
4. Please use a semantic commits format title, such as `<type>[optional scope]: <description>`, see: https://go-kratos.dev/docs/community/contribution#type
5. at the same time, please note that similar work should be submitted in one PR as far as possible to reduce the workload of reviewers. Do not split a work into multiple PR unless it should.
-->

<!--
🎉 感谢您向 Kratos 发送 PR 请求！以下是一些提示：
如果这是你第一次为 Kratos 贡献，请阅读我们的贡献指南：https://go-kratos.dev/en/docs/community/contribution/
2、确保您已经为您的 PR 添加或运行了适当的测试和lint，请在提交PR之前使用“make lint”和“make test”，使用“make clean”整理您的 go.mod。
3、如果请购单未完成，您可能需要将其标记为 WIP（在制品）PR 或 draft PR
4、请使用语义提交格式标题，如“<类型>[可选范围]：<说明>`，请参阅：https://go-kratos.dev/docs/community/contribution#type
5. 同时请注意，同类的工作请尽量在一个PR中提交，以减轻 review 者的工作负担，不要把一项工作拆分成很多个PR，除非它应该这样做。
-->

<!--
-->

#### Description (what this PR does / why we need it):
<!--
* The description should include the motivation for this PR or contrast this with previous behavior
-->
add unit test for transport coverage: 91.7% of statements

#### Which issue(s) this PR fixes (resolves / be part of):
<!--
* Automatically closes linked issue when PR is merged.
* If your PR is not fully resolved the issue, please use `part of #<issue number>` instead.

Usage: `fixes/resolves #<issue number>`, or `fixes/resolves (paste link of issue)`.
-->
part of #2147 

#### Other special notes for the reviewers:
<!--
* Somethings that need extra attention for the reviewers
* Some additional notes, TODO list, etc.
-->


codecov-commenter: # [Codecov](https://codecov.io/gh/go-kratos/kratos/pull/2172?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-kratos) Report
> Merging [#2172](https://codecov.io/gh/go-kratos/kratos/pull/2172?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-kratos) (b6be8e9) into [main](https://codecov.io/gh/go-kratos/kratos/commit/8d0d7838ca282e70fa03213aeb80fe52257db35c?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-kratos) (8d0d783) will **increase** coverage by `5.36%`.
> The diff coverage is `93.33%`.

```diff
@@            Coverage Diff             @@
##             main    #2172      +/-   ##
==========================================
+ Coverage   79.22%   84.58%   +5.36%     
==========================================
  Files          84       86       +2     
  Lines        3701     3743      +42     
==========================================
+ Hits         2932     3166     +234     
+ Misses        559      409     -150     
+ Partials      210      168      -42     
```


| [Impacted Files](https://codecov.io/gh/go-kratos/kratos/pull/2172?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-kratos) | Coverage Δ | |
|---|---|---|
| [transport/http/binding/encode.go](https://codecov.io/gh/go-kratos/kratos/pull/2172/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-kratos#diff-dHJhbnNwb3J0L2h0dHAvYmluZGluZy9lbmNvZGUuZ28=) | `100.00% <ø> (+18.60%)` | :arrow_up: |
| [transport/grpc/resolver/discovery/resolver.go](https://codecov.io/gh/go-kratos/kratos/pull/2172/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-kratos#diff-dHJhbnNwb3J0L2dycGMvcmVzb2x2ZXIvZGlzY292ZXJ5L3Jlc29sdmVyLmdv) | `87.50% <80.00%> (+10.71%)` | :arrow_up: |
| [transport/grpc/resolver/discovery/builder.go](https://codecov.io/gh/go-kratos/kratos/pull/2172/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-kratos#diff-dHJhbnNwb3J0L2dycGMvcmVzb2x2ZXIvZGlzY292ZXJ5L2J1aWxkZXIuZ28=) | `100.00% <100.00%> (+21.27%)` | :arrow_up: |
| [transport/http/resolver.go](https://codecov.io/gh/go-kratos/kratos/pull/2172/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-kratos#diff-dHJhbnNwb3J0L2h0dHAvcmVzb2x2ZXIuZ28=) | `89.33% <100.00%> (+29.87%)` | :arrow_up: |
| [transport/http/server.go](https://codecov.io/gh/go-kratos/kratos/pull/2172/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-kratos#diff-dHJhbnNwb3J0L2h0dHAvc2VydmVyLmdv) | `84.37% <100.00%> (+6.14%)` | :arrow_up: |
| [internal/context/context.go](https://codecov.io/gh/go-kratos/kratos/pull/2172/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-kratos#diff-aW50ZXJuYWwvY29udGV4dC9jb250ZXh0Lmdv) | `97.01% <0.00%> (-2.99%)` | :arrow_down: |
| [middleware/circuitbreaker/circuitbreaker.go](https://codecov.io/gh/go-kratos/kratos/pull/2172/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-kratos#diff-bWlkZGxld2FyZS9jaXJjdWl0YnJlYWtlci9jaXJjdWl0YnJlYWtlci5nbw==) | `100.00% <0.00%> (ø)` | |
| [middleware/ratelimit/ratelimit.go](https://codecov.io/gh/go-kratos/kratos/pull/2172/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-kratos#diff-bWlkZGxld2FyZS9yYXRlbGltaXQvcmF0ZWxpbWl0Lmdv) | `100.00% <0.00%> (ø)` | |
| [transport/http/context.go](https://codecov.io/gh/go-kratos/kratos/pull/2172/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-kratos#diff-dHJhbnNwb3J0L2h0dHAvY29udGV4dC5nbw==) | `94.04% <0.00%> (+2.38%)` | :arrow_up: |
| [middleware/logging/logging.go](https://codecov.io/gh/go-kratos/kratos/pull/2172/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-kratos#diff-bWlkZGxld2FyZS9sb2dnaW5nL2xvZ2dpbmcuZ28=) | `100.00% <0.00%> (+3.03%)` | :arrow_up: |
| ... and [19 more](https://codecov.io/gh/go-kratos/kratos/pull/2172/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-kratos) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/go-kratos/kratos/pull/2172?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-kratos).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-kratos)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/go-kratos/kratos/pull/2172?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-kratos). Last update [8d0d783...b6be8e9](https://codecov.io/gh/go-kratos/kratos/pull/2172?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-kratos). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-kratos).


Patch:
diff --git a/api/metadata/server.go b/api/metadata/server.go
index 445ddeb6..3438d132 100644
--- a/api/metadata/server.go
+++ b/api/metadata/server.go
@@ -4,6 +4,7 @@ import (
 	"bytes"
 	"compress/gzip"
 	"context"
+	"errors"
 	"fmt"
 	"io"
 	"sync"
@@ -76,6 +77,10 @@ func (s *Server) load() error {
 			}
 			fdps, e := allDependency(fdp)
 			if e != nil {
+				if errors.Is(e, protoregistry.NotFound) {
+					// Skip this service if one of its dependencies is not found.
+					continue
+				}
 				err = e
 				return false
 			}
diff --git a/contrib/metrics/prometheus/go.mod b/contrib/metrics/prometheus/go.mod
index 8c6f24aa..fff4b0ab 100644
--- a/contrib/metrics/prometheus/go.mod
+++ b/contrib/metrics/prometheus/go.mod
@@ -5,6 +5,7 @@ go 1.16
 require (
 	github.com/go-kratos/kratos/v2 v2.3.1
 	github.com/prometheus/client_golang v1.12.2
+	github.com/prometheus/common v0.32.1
 )
 
 replace github.com/go-kratos/kratos/v2 => ../../../
diff --git a/contrib/registry/kubernetes/go.mod b/contrib/registry/kubernetes/go.mod
index 8d77a4af..2d01cbaf 100644
--- a/contrib/registry/kubernetes/go.mod
+++ b/contrib/registry/kubernetes/go.mod
@@ -7,7 +7,7 @@ require (
 	github.com/json-iterator/go v1.1.12
 	k8s.io/api v0.24.2
 	k8s.io/apimachinery v0.24.2
-	k8s.io/client-go v0.24.1
+	k8s.io/client-go v0.24.2
 )
 
 replace github.com/go-kratos/kratos/v2 => ../../../
diff --git a/contrib/registry/kubernetes/go.sum b/contrib/registry/kubernetes/go.sum
index 559f0670..368dd5d5 100644
--- a/contrib/registry/kubernetes/go.sum
+++ b/contrib/registry/kubernetes/go.sum
@@ -653,14 +653,12 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
 honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
 honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
 honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
-k8s.io/api v0.24.1/go.mod h1:JhoOvNiLXKTPQ60zh2g0ewpA+bnEYf5q44Flhquh4vQ=
 k8s.io/api v0.24.2 h1:g518dPU/L7VRLxWfcadQn2OnsiGWVOadTLpdnqgY2OI=
 k8s.io/api v0.24.2/go.mod h1:AHqbSkTm6YrQ0ObxjO3Pmp/ubFF/KuM7jU+3khoBsOg=
-k8s.io/apimachinery v0.24.1/go.mod h1:82Bi4sCzVBdpYjyI4jY6aHX+YCUchUIrZrXKedjd2UM=
 k8s.io/apimachinery v0.24.2 h1:5QlH9SL2C8KMcrNJPor+LbXVTaZRReml7svPEh4OKDM=
 k8s.io/apimachinery v0.24.2/go.mod h1:82Bi4sCzVBdpYjyI4jY6aHX+YCUchUIrZrXKedjd2UM=
-k8s.io/client-go v0.24.1 h1:w1hNdI9PFrzu3OlovVeTnf4oHDt+FJLd9Ndluvnb42E=
-k8s.io/client-go v0.24.1/go.mod h1:f1kIDqcEYmwXS/vTbbhopMUbhKp2JhOeVTfxgaCIlF8=
+k8s.io/client-go v0.24.2 h1:CoXFSf8if+bLEbinDqN9ePIDGzcLtqhfd6jpfnwGOFA=
+k8s.io/client-go v0.24.2/go.mod h1:zg4Xaoo+umDsfCWr4fCnmLEtQXyCNXCvJuSsglNcV30=
 k8s.io/gengo v0.0.0-20210813121822-485abfe95c7c/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E=
 k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE=
 k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y=
diff --git a/errors/errors.go b/errors/errors.go
index a18f5512..e6bebbf8 100644
--- a/errors/errors.go
+++ b/errors/errors.go
@@ -106,6 +106,9 @@ func Reason(err error) string {
 
 // Clone deep clone error to a new error.
 func Clone(err *Error) *Error {
+	if err == nil {
+		return nil
+	}
 	metadata := make(map[string]string, len(err.Metadata))
 	for k, v := range err.Metadata {
 		metadata[k] = v
diff --git a/transport/grpc/interceptor.go b/transport/grpc/interceptor.go
index 3f3827f6..26daedfb 100644
--- a/transport/grpc/interceptor.go
+++ b/transport/grpc/interceptor.go
@@ -17,12 +17,15 @@ func (s *Server) unaryServerInterceptor() grpc.UnaryServerInterceptor {
 		defer cancel()
 		md, _ := grpcmd.FromIncomingContext(ctx)
 		replyHeader := grpcmd.MD{}
-		ctx = transport.NewServerContext(ctx, &Transport{
-			endpoint:    s.endpoint.String(),
+		tr := &Transport{
 			operation:   info.FullMethod,
 			reqHeader:   headerCarrier(md),
 			replyHeader: headerCarrier(replyHeader),
-		})
+		}
+		if s.endpoint != nil {
+			tr.endpoint = s.endpoint.String()
+		}
+		ctx = transport.NewServerContext(ctx, tr)
 		if s.timeout > 0 {
 			ctx, cancel = context.WithTimeout(ctx, s.timeout)
 			defer cancel()
diff --git a/transport/grpc/resolver/discovery/builder.go b/transport/grpc/resolver/discovery/builder.go
index 7ce77717..a4425a51 100644
--- a/transport/grpc/resolver/discovery/builder.go
+++ b/transport/grpc/resolver/discovery/builder.go
@@ -59,18 +59,24 @@ func NewBuilder(d registry.Discovery, opts ...Option) resolver.Builder {
 }
 
 func (b *builder) Build(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOptions) (resolver.Resolver, error) {
-	var (
+	watchRes := &struct {
 		err error
 		w   registry.Watcher
-	)
+	}{}
+
 	done := make(chan struct{}, 1)
 	ctx, cancel := context.WithCancel(context.Background())
 	go func() {
-		w, err = b.discoverer.Watch(ctx, strings.TrimPrefix(target.URL.Path, "/"))
+		w, err := b.discoverer.Watch(ctx, strings.TrimPrefix(target.URL.Path, "/"))
+		watchRes.w = w
+		watchRes.err = err
 		close(done)
 	}()
+
+	var err error
 	select {
 	case <-done:
+		err = watchRes.err
 	case <-time.After(b.timeout):
 		err = errors.New("discovery create watcher overtime")
 	}
@@ -79,7 +85,7 @@ func (b *builder) Build(target resolver.Target, cc resolver.ClientConn, opts res
 		return nil, err
 	}
 	r := &discoveryResolver{
-		w:                w,
+		w:                watchRes.w,
 		cc:               cc,
 		ctx:              ctx,
 		cancel:           cancel,
diff --git a/transport/grpc/resolver/discovery/resolver.go b/transport/grpc/resolver/discovery/resolver.go
index 6a7f6e7e..0dc115b6 100644
--- a/transport/grpc/resolver/discovery/resolver.go
+++ b/transport/grpc/resolver/discovery/resolver.go
@@ -49,23 +49,23 @@ func (r *discoveryResolver) update(ins []*registry.ServiceInstance) {
 	addrs := make([]resolver.Address, 0)
 	endpoints := make(map[string]struct{})
 	for _, in := range ins {
-		endpoint, err := endpoint.ParseEndpoint(in.Endpoints, endpoint.Scheme("grpc", !r.insecure))
+		ept, err := endpoint.ParseEndpoint(in.Endpoints, endpoint.Scheme("grpc", !r.insecure))
 		if err != nil {
 			log.Errorf("[resolver] Failed to parse discovery endpoint: %v", err)
 			continue
 		}
-		if endpoint == "" {
+		if ept == "" {
 			continue
 		}
 		// filter redundant endpoints
-		if _, ok := endpoints[endpoint]; ok {
+		if _, ok := endpoints[ept]; ok {
 			continue
 		}
-		endpoints[endpoint] = struct{}{}
+		endpoints[ept] = struct{}{}
 		addr := resolver.Address{
 			ServerName: in.Name,
 			Attributes: parseAttributes(in.Metadata),
-			Addr:       endpoint,
+			Addr:       ept,
 		}
 		addr.Attributes = addr.Attributes.WithValue("rawServiceInstance", in)
 		addrs = append(addrs, addr)
diff --git a/transport/grpc/server.go b/transport/grpc/server.go
index 31c03f75..0131692e 100644
--- a/transport/grpc/server.go
+++ b/transport/grpc/server.go
@@ -156,8 +156,6 @@ func NewServer(opts ...ServerOption) *Server {
 	}
 	srv.Server = grpc.NewServer(grpcOpts...)
 	srv.metadata = apimd.NewServer(srv.Server)
-	// listen and endpoint
-	srv.err = srv.listenAndEndpoint()
 	// internal register
 	grpc_health_v1.RegisterHealthServer(srv.Server, srv.health)
 	apimd.RegisterMetadataServer(srv.Server, srv.metadata)
@@ -169,7 +167,7 @@ func NewServer(opts ...ServerOption) *Server {
 // examples:
 //   grpc://127.0.0.1:9000?isSecure=false
 func (s *Server) Endpoint() (*url.URL, error) {
-	if s.err != nil {
+	if err := s.listenAndEndpoint(); err != nil {
 		return nil, s.err
 	}
 	return s.endpoint, nil
@@ -177,7 +175,7 @@ func (s *Server) Endpoint() (*url.URL, error) {
 
 // Start start the gRPC server.
 func (s *Server) Start(ctx context.Context) error {
-	if s.err != nil {
+	if err := s.listenAndEndpoint(); err != nil {
 		return s.err
 	}
 	s.baseCtx = ctx
@@ -198,15 +196,17 @@ func (s *Server) listenAndEndpoint() error {
 	if s.lis == nil {
 		lis, err := net.Listen(s.network, s.address)
 		if err != nil {
+			s.err = err
+			return err
+		}
+		addr, err := host.Extract(s.address, lis)
+		if err != nil {
+			_ = s.lis.Close()
+			s.err = err
 			return err
 		}
 		s.lis = lis
+		s.endpoint = endpoint.NewEndpoint(endpoint.Scheme("grpc", s.tlsConf != nil), addr)
 	}
-	addr, err := host.Extract(s.address, s.lis)
-	if err != nil {
-		_ = s.lis.Close()
-		return err
-	}
-	s.endpoint = endpoint.NewEndpoint(endpoint.Scheme("grpc", s.tlsConf != nil), addr)
-	return nil
+	return s.err
 }
diff --git a/transport/http/binding/encode.go b/transport/http/binding/encode.go
index 14faf2e9..3129bbe5 100644
--- a/transport/http/binding/encode.go
+++ b/transport/http/binding/encode.go
@@ -12,20 +12,19 @@ import (
 	protoreflect "google.golang.org/protobuf/reflect/protoreflect"
 )
 
+var reg = regexp.MustCompile(`/{[\\.\w]+}`)
+
 // EncodeURL encode proto message to url path.
 func EncodeURL(pathTemplate string, msg proto.Message, needQuery bool) string {
 	if msg == nil || (reflect.ValueOf(msg).Kind() == reflect.Ptr && reflect.ValueOf(msg).IsNil()) {
 		return pathTemplate
 	}
-	reg := regexp.MustCompile(`/{[.\w]+}`)
-	if reg == nil {
-		return pathTemplate
-	}
 	pathParams := make(map[string]struct{})
 	path := reg.ReplaceAllStringFunc(pathTemplate, func(in string) string {
-		if len(in) < 4 { //nolint:gomnd // **  explain the 4 number here :-) **
-			return in
-		}
+		// it's unreachable because the reg means that must have more than one char in {}
+		//if len(in) < 4 { //nolint:gomnd // **  explain the 4 number here :-) **
+		//	return in
+		//}
 		key := in[2 : len(in)-1]
 		vars := strings.Split(key, ".")
 		value, err := getValueByField(msg.ProtoReflect(), vars)
diff --git a/transport/http/server.go b/transport/http/server.go
index c45a7269..a0d4f14f 100644
--- a/transport/http/server.go
+++ b/transport/http/server.go
@@ -22,6 +22,7 @@ import (
 var (
 	_ transport.Server     = (*Server)(nil)
 	_ transport.Endpointer = (*Server)(nil)
+	_ http.Handler         = (*Server)(nil)
 )
 
 // ServerOption is an HTTP server option.
@@ -153,7 +154,6 @@ func NewServer(opts ...ServerOption) *Server {
 		Handler:   FilterChain(srv.filters...)(srv.router),
 		TLSConfig: srv.tlsConf,
 	}
-	srv.err = srv.listenAndEndpoint()
 	return srv
 }
 
@@ -208,14 +208,15 @@ func (s *Server) filter() mux.MiddlewareFunc {
 			}
 
 			tr := &Transport{
-				endpoint:     s.endpoint.String(),
 				operation:    pathTemplate,
 				reqHeader:    headerCarrier(req.Header),
 				replyHeader:  headerCarrier(w.Header()),
 				request:      req,
 				pathTemplate: pathTemplate,
 			}
-
+			if s.endpoint != nil {
+				tr.endpoint = s.endpoint.String()
+			}
 			tr.request = req.WithContext(transport.NewServerContext(ctx, tr))
 			next.ServeHTTP(w, tr.request)
 		})
@@ -227,16 +228,16 @@ func (s *Server) filter() mux.MiddlewareFunc {
 //   https://127.0.0.1:8000
 //   Legacy: http://127.0.0.1:8000?isSecure=false
 func (s *Server) Endpoint() (*url.URL, error) {
-	if s.err != nil {
-		return nil, s.err
+	if err := s.listenAndEndpoint(); err != nil {
+		return nil, err
 	}
 	return s.endpoint, nil
 }
 
 // Start start the HTTP server.
 func (s *Server) Start(ctx context.Context) error {
-	if s.err != nil {
-		return s.err
+	if err := s.listenAndEndpoint(); err != nil {
+		return err
 	}
 	s.BaseContext = func(net.Listener) context.Context {
 		return ctx
@@ -264,15 +265,17 @@ func (s *Server) listenAndEndpoint() error {
 	if s.lis == nil {
 		lis, err := net.Listen(s.network, s.address)
 		if err != nil {
+			s.err = err
+			return err
+		}
+		addr, err := host.Extract(s.address, lis)
+		if err != nil {
+			_ = s.lis.Close()
+			s.err = err
 			return err
 		}
 		s.lis = lis
+		s.endpoint = endpoint.NewEndpoint(endpoint.Scheme("http", s.tlsConf != nil), addr)
 	}
-	addr, err := host.Extract(s.address, s.lis)
-	if err != nil {
-		_ = s.lis.Close()
-		return err
-	}
-	s.endpoint = endpoint.NewEndpoint(endpoint.Scheme("http", s.tlsConf != nil), addr)
-	return nil
+	return s.err
 }


Test patch:
diff --git a/contrib/config/consul/config_test.go b/contrib/config/consul/config_test.go
index 8e952736..a82a2036 100644
--- a/contrib/config/consul/config_test.go
+++ b/contrib/config/consul/config_test.go
@@ -3,7 +3,9 @@ package consul
 import (
 	"reflect"
 	"testing"
+	"time"
 
+	"github.com/go-kratos/kratos/v2/config"
 	"github.com/hashicorp/consul/api"
 )
 
@@ -99,3 +101,158 @@ func TestExtToFormat(t *testing.T) {
 		t.Errorf("kvs[0].Format is %s", kvs[0].Format)
 	}
 }
+
+func Test_source_Watch(t *testing.T) {
+	client, err := api.NewClient(&api.Config{
+		Address: "127.0.0.1:8500",
+	})
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	source, err := New(client, WithPath(testPath))
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	type fields struct {
+		source config.Source
+	}
+
+	type args struct {
+		key   string
+		value string
+	}
+
+	tests := []struct {
+		name      string
+		fields    fields
+		args      args
+		want      string
+		wantErr   bool
+		deferFunc func(t *testing.T)
+	}{
+		{
+			name:   "normal",
+			fields: fields{source: source},
+			args: args{
+				key:   testKey,
+				value: "test value",
+			},
+			want:    "test value",
+			wantErr: false,
+			deferFunc: func(t *testing.T) {
+				_, err := client.KV().Delete(testKey, nil)
+				if err != nil {
+					t.Error(err)
+				}
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if tt.deferFunc != nil {
+				defer tt.deferFunc(t)
+			}
+
+			got, err := tt.fields.source.Watch()
+			if (err != nil) != tt.wantErr {
+				t.Errorf("Watch() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+
+			time.Sleep(100 * time.Millisecond)
+			_, err = client.KV().Put(&api.KVPair{Key: tt.args.key, Value: []byte(tt.args.value)}, nil)
+			if err != nil {
+				t.Error(err)
+			}
+
+			next, err := got.Next()
+			if (err != nil) != tt.wantErr {
+				t.Errorf("Watch() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+
+			if len(next) != 1 {
+				t.Error("watch is error")
+			}
+
+			if !reflect.DeepEqual(string(next[0].Value), tt.want) {
+				t.Errorf("Watch got = %v, want %v", string(next[0].Value), tt.want)
+			}
+		})
+	}
+}
+
+func Test_source_Load(t *testing.T) {
+	client, err := api.NewClient(&api.Config{
+		Address: "127.0.0.1:8500",
+	})
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	source, err := New(client, WithPath(testPath))
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	type args struct {
+		key   string
+		value string
+	}
+	type fields struct {
+		source config.Source
+	}
+	tests := []struct {
+		name      string
+		args      args
+		fields    fields
+		want      []*config.KeyValue
+		wantErr   bool
+		deferFunc func(t *testing.T)
+	}{
+		{
+			name: "normal",
+			args: args{
+				key:   testKey,
+				value: "test value",
+			},
+			fields: fields{
+				source: source,
+			},
+			want: []*config.KeyValue{
+				{
+					Key:   "key",
+					Value: []byte("test value"),
+				},
+			},
+			deferFunc: func(t *testing.T) {
+				_, err1 := client.KV().Delete(testKey, nil)
+				if err1 != nil {
+					t.Error(err)
+				}
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if tt.deferFunc != nil {
+				defer tt.deferFunc(t)
+			}
+			_, err = client.KV().Put(&api.KVPair{Key: tt.args.key, Value: []byte(tt.args.value)}, nil)
+			if err != nil {
+				t.Error(err)
+			}
+			got, err := tt.fields.source.Load()
+			if (err != nil) != tt.wantErr {
+				t.Errorf("Load() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+
+			if !reflect.DeepEqual(got[0], tt.want[0]) {
+				t.Errorf("Load() got = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
diff --git a/contrib/metrics/prometheus/counter_test.go b/contrib/metrics/prometheus/counter_test.go
new file mode 100644
index 00000000..f84b5253
--- /dev/null
+++ b/contrib/metrics/prometheus/counter_test.go
@@ -0,0 +1,65 @@
+package prometheus
+
+import (
+	"bytes"
+	"fmt"
+	"testing"
+
+	"github.com/prometheus/client_golang/prometheus"
+	"github.com/prometheus/common/expfmt"
+)
+
+func gatherLatest(reg *prometheus.Registry) (result string, err error) {
+	mfs, err := reg.Gather()
+	if err != nil {
+		return "", err
+	}
+	buf := &bytes.Buffer{}
+
+	enc := expfmt.NewEncoder(buf, expfmt.FmtText)
+	for _, mf := range mfs {
+		if err = enc.Encode(mf); err != nil {
+			return "", err
+		}
+	}
+	return buf.String(), nil
+}
+
+func TestCounter(t *testing.T) {
+	expect := `# HELP test_request_test_metric test
+# TYPE test_request_test_metric counter
+test_request_test_metric{code="test",kind="test",operation="test",reason="test"} %d
+`
+
+	counterVec := prometheus.NewCounterVec(prometheus.CounterOpts{
+		Namespace: "test",
+		Name:      "test_metric",
+		Subsystem: "request",
+		Help:      "test",
+	}, []string{"kind", "operation", "code", "reason"})
+
+	counter := NewCounter(counterVec)
+	counter.With("test", "test", "test", "test").Inc()
+
+	reg := prometheus.NewRegistry()
+	reg.MustRegister(counterVec)
+
+	result, err := gatherLatest(reg)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if result != fmt.Sprintf(expect, 1) {
+		t.Fatal("metrics error")
+	}
+
+	counter.With("test", "test", "test", "test").Add(10)
+	result, err = gatherLatest(reg)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if result != fmt.Sprintf(expect, 11) {
+		t.Fatal("metrics error")
+	}
+}
diff --git a/contrib/metrics/prometheus/gauge_test.go b/contrib/metrics/prometheus/gauge_test.go
new file mode 100644
index 00000000..31744a5a
--- /dev/null
+++ b/contrib/metrics/prometheus/gauge_test.go
@@ -0,0 +1,55 @@
+package prometheus
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/prometheus/client_golang/prometheus"
+)
+
+func TestGuage(t *testing.T) {
+	expect := `# HELP test_request_test_guage_metric test
+# TYPE test_request_test_guage_metric gauge
+test_request_test_guage_metric{code="test",kind="test",operation="test",reason="test"} %d
+`
+
+	guageVec := prometheus.NewGaugeVec(prometheus.GaugeOpts{
+		Namespace: "test",
+		Name:      "test_guage_metric",
+		Subsystem: "request",
+		Help:      "test",
+	}, []string{"kind", "operation", "code", "reason"})
+
+	guage := NewGauge(guageVec)
+	guage.With("test", "test", "test", "test").Set(1)
+
+	reg := prometheus.NewRegistry()
+	reg.MustRegister(guageVec)
+
+	result, err := gatherLatest(reg)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if result != fmt.Sprintf(expect, 1) {
+		t.Fatal("metrics error")
+	}
+
+	guage.With("test", "test", "test", "test").Add(1)
+	result, err = gatherLatest(reg)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if result != fmt.Sprintf(expect, 2) {
+		t.Fatal("metrics error")
+	}
+
+	guage.With("test", "test", "test", "test").Sub(1)
+	result, err = gatherLatest(reg)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if result != fmt.Sprintf(expect, 1) {
+		t.Fatal("metrics error")
+	}
+}
diff --git a/contrib/metrics/prometheus/histogram_test.go b/contrib/metrics/prometheus/histogram_test.go
new file mode 100644
index 00000000..7f2a511d
--- /dev/null
+++ b/contrib/metrics/prometheus/histogram_test.go
@@ -0,0 +1,77 @@
+package prometheus
+
+import (
+	"fmt"
+	"strconv"
+	"testing"
+
+	"github.com/prometheus/client_golang/prometheus"
+)
+
+func intToFloatString(in int) string {
+	return strconv.FormatFloat(float64(in), 'f', -1, 64)
+}
+
+func TestHistogram(t *testing.T) {
+	expect := `# HELP test_request_test_metrics test
+# TYPE test_request_test_metrics histogram
+test_request_test_metrics_bucket{code="test",kind="test",operation="test",reason="test",le="0.05"} %s
+test_request_test_metrics_bucket{code="test",kind="test",operation="test",reason="test",le="0.1"} %s
+test_request_test_metrics_bucket{code="test",kind="test",operation="test",reason="test",le="0.25"} %s
+test_request_test_metrics_bucket{code="test",kind="test",operation="test",reason="test",le="0.5"} %s
+test_request_test_metrics_bucket{code="test",kind="test",operation="test",reason="test",le="1"} %s
+test_request_test_metrics_bucket{code="test",kind="test",operation="test",reason="test",le="+Inf"} %s
+test_request_test_metrics_sum{code="test",kind="test",operation="test",reason="test"} %s
+test_request_test_metrics_count{code="test",kind="test",operation="test",reason="test"} %s
+`
+
+	histogramVec := prometheus.NewHistogramVec(prometheus.HistogramOpts{
+		Namespace: "test",
+		Name:      "test_metrics",
+		Subsystem: "request",
+		Help:      "test",
+		Buckets:   []float64{0.05, 0.1, 0.250, 0.5, 1},
+	}, []string{"kind", "operation", "code", "reason"})
+
+	histogram := NewHistogram(histogramVec)
+	histogram.With("test", "test", "test", "test").Observe(0.5)
+
+	reg := prometheus.NewRegistry()
+	reg.MustRegister(histogramVec)
+
+	result, err := gatherLatest(reg)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if result != fmt.Sprintf(expect,
+		intToFloatString(0),
+		intToFloatString(0),
+		intToFloatString(0),
+		intToFloatString(1),
+		intToFloatString(1),
+		intToFloatString(1),
+		"0.5",
+		intToFloatString(1)) {
+		t.Fatal("metrics error")
+	}
+
+	histogram.With("test", "test", "test", "test").Observe(0.1)
+	result, err = gatherLatest(reg)
+
+	if err != nil {
+		t.Fatal(err)
+	}
+	if result != fmt.Sprintf(expect,
+		intToFloatString(0),
+		intToFloatString(1),
+		intToFloatString(1),
+		intToFloatString(2),
+		intToFloatString(2),
+		intToFloatString(2),
+		"0.6",
+		intToFloatString(2),
+	) {
+		t.Fatal("metrics error")
+	}
+}
diff --git a/contrib/metrics/prometheus/summary_test.go b/contrib/metrics/prometheus/summary_test.go
new file mode 100644
index 00000000..3c93ae94
--- /dev/null
+++ b/contrib/metrics/prometheus/summary_test.go
@@ -0,0 +1,47 @@
+package prometheus
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/prometheus/client_golang/prometheus"
+)
+
+func TestSummary(t *testing.T) {
+	expect := `# HELP test_request_test_metric test
+# TYPE test_request_test_metric summary
+test_request_test_metric_sum{code="test",kind="test",operation="test",reason="test"} %s
+test_request_test_metric_count{code="test",kind="test",operation="test",reason="test"} %s
+`
+
+	summaryVec := prometheus.NewSummaryVec(prometheus.SummaryOpts{
+		Namespace: "test",
+		Name:      "test_metric",
+		Subsystem: "request",
+		Help:      "test",
+	}, []string{"kind", "operation", "code", "reason"})
+
+	summary := NewSummary(summaryVec)
+	summary.With("test", "test", "test", "test").Observe(1)
+
+	reg := prometheus.NewRegistry()
+	reg.MustRegister(summaryVec)
+	result, err := gatherLatest(reg)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if result != fmt.Sprintf(expect, intToFloatString(1), intToFloatString(1)) {
+		t.Fatal("metrics error")
+	}
+	summary.With("test", "test", "test", "test").Observe(10)
+
+	result, err = gatherLatest(reg)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if result != fmt.Sprintf(expect, intToFloatString(11), intToFloatString(2)) {
+		t.Fatal("metrics error")
+	}
+}
diff --git a/errors/errors_test.go b/errors/errors_test.go
index f76d5151..5196d0b6 100644
--- a/errors/errors_test.go
+++ b/errors/errors_test.go
@@ -114,20 +114,37 @@ func TestCause(t *testing.T) {
 }
 
 func TestOther(t *testing.T) {
+	err := Errorf(10001, "test code 10001", "message")
+	// Code
 	if !reflect.DeepEqual(Code(nil), 200) {
 		t.Errorf("Code(nil) = %v, want %v", Code(nil), 200)
 	}
 	if !reflect.DeepEqual(Code(errors.New("test")), UnknownCode) {
 		t.Errorf(`Code(errors.New("test")) = %v, want %v`, Code(nil), 200)
 	}
-	if !reflect.DeepEqual(Reason(errors.New("test")), UnknownReason) {
-		t.Errorf(`Reason(errors.New("test")) = %v, want %v`, Reason(nil), UnknownReason)
-	}
-	err := Errorf(10001, "test code 10001", "message")
 	if !reflect.DeepEqual(Code(err), 10001) {
 		t.Errorf(`Code(err) = %v, want %v`, Code(err), 10001)
 	}
+	// Reason
+	if !reflect.DeepEqual(Reason(nil), UnknownReason) {
+		t.Errorf(`Reason(nil) = %v, want %v`, Reason(nil), UnknownReason)
+	}
+	if !reflect.DeepEqual(Reason(errors.New("test")), UnknownReason) {
+		t.Errorf(`Reason(errors.New("test")) = %v, want %v`, Reason(nil), UnknownReason)
+	}
 	if !reflect.DeepEqual(Reason(err), "test code 10001") {
 		t.Errorf(`Reason(err) = %v, want %v`, Reason(err), "test code 10001")
 	}
+	// Clone
+	err400 := Newf(http.StatusBadRequest, "BAD_REQUEST", "param invalid")
+	err400.Metadata = map[string]string{
+		"key1": "val1",
+		"key2": "val2",
+	}
+	if cerr := Clone(err400); cerr == nil || cerr.Error() != err400.Error() {
+		t.Errorf("Clone(err) = %v, want %v", Clone(err400), err400)
+	}
+	if cerr := Clone(nil); cerr != nil {
+		t.Errorf("Clone(nil) = %v, want %v", Clone(err400), err400)
+	}
 }
diff --git a/internal/testdata/binding/generate.go b/internal/testdata/binding/generate.go
new file mode 100644
index 00000000..7abe42f8
--- /dev/null
+++ b/internal/testdata/binding/generate.go
@@ -0,0 +1,3 @@
+package binding
+
+//go:generate protoc -I . --go_out=paths=source_relative:. ./test.proto
diff --git a/internal/testdata/binding/test.pb.go b/internal/testdata/binding/test.pb.go
index 3a99e6d9..a0f90919 100644
--- a/internal/testdata/binding/test.pb.go
+++ b/internal/testdata/binding/test.pb.go
@@ -1,7 +1,7 @@
 // Code generated by protoc-gen-go. DO NOT EDIT.
 // versions:
 // 	protoc-gen-go v1.28.0
-// 	protoc        v3.20.0
+// 	protoc        v3.17.3
 // source: test.proto
 
 package binding
@@ -27,13 +27,14 @@ type HelloRequest struct {
 	sizeCache     protoimpl.SizeCache
 	unknownFields protoimpl.UnknownFields
 
-	Name       string                 `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
-	Sub        *Sub                   `protobuf:"bytes,2,opt,name=sub,proto3" json:"sub,omitempty"`
-	UpdateMask *fieldmaskpb.FieldMask `protobuf:"bytes,3,opt,name=update_mask,json=updateMask,proto3" json:"update_mask,omitempty"`
-	OptInt32   *int32                 `protobuf:"varint,4,opt,name=opt_int32,json=optInt32,proto3,oneof" json:"opt_int32,omitempty"`
-	OptInt64   *int64                 `protobuf:"varint,5,opt,name=opt_int64,json=optInt64,proto3,oneof" json:"opt_int64,omitempty"`
-	OptString  *string                `protobuf:"bytes,6,opt,name=opt_string,json=optString,proto3,oneof" json:"opt_string,omitempty"`
-	SubField   *Sub                   `protobuf:"bytes,7,opt,name=subField,proto3" json:"subField,omitempty"`
+	Name         string                 `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
+	Sub          *Sub                   `protobuf:"bytes,2,opt,name=sub,proto3" json:"sub,omitempty"`
+	UpdateMask   *fieldmaskpb.FieldMask `protobuf:"bytes,3,opt,name=update_mask,json=updateMask,proto3" json:"update_mask,omitempty"`
+	OptInt32     *int32                 `protobuf:"varint,4,opt,name=opt_int32,json=optInt32,proto3,oneof" json:"opt_int32,omitempty"`
+	OptInt64     *int64                 `protobuf:"varint,5,opt,name=opt_int64,json=optInt64,proto3,oneof" json:"opt_int64,omitempty"`
+	OptString    *string                `protobuf:"bytes,6,opt,name=opt_string,json=optString,proto3,oneof" json:"opt_string,omitempty"`
+	SubField     *Sub                   `protobuf:"bytes,7,opt,name=subField,proto3" json:"subField,omitempty"`
+	TestRepeated []string               `protobuf:"bytes,8,rep,name=test_repeated,proto3" json:"test_repeated,omitempty"`
 }
 
 func (x *HelloRequest) Reset() {
@@ -117,6 +118,13 @@ func (x *HelloRequest) GetSubField() *Sub {
 	return nil
 }
 
+func (x *HelloRequest) GetTestRepeated() []string {
+	if x != nil {
+		return x.TestRepeated
+	}
+	return nil
+}
+
 type Sub struct {
 	state         protoimpl.MessageState
 	sizeCache     protoimpl.SizeCache
@@ -170,7 +178,7 @@ var file_test_proto_rawDesc = []byte{
 	0x0a, 0x0a, 0x74, 0x65, 0x73, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x07, 0x62, 0x69,
 	0x6e, 0x64, 0x69, 0x6e, 0x67, 0x1a, 0x20, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72,
 	0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x5f, 0x6d, 0x61, 0x73,
-	0x6b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xbc, 0x02, 0x0a, 0x0c, 0x48, 0x65, 0x6c, 0x6c,
+	0x6b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xe2, 0x02, 0x0a, 0x0c, 0x48, 0x65, 0x6c, 0x6c,
 	0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65,
 	0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1e, 0x0a, 0x03,
 	0x73, 0x75, 0x62, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x62, 0x69, 0x6e, 0x64,
@@ -187,15 +195,18 @@ var file_test_proto_rawDesc = []byte{
 	0x09, 0x48, 0x02, 0x52, 0x09, 0x6f, 0x70, 0x74, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x88, 0x01,
 	0x01, 0x12, 0x28, 0x0a, 0x08, 0x73, 0x75, 0x62, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x18, 0x07, 0x20,
 	0x01, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x62, 0x69, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x2e, 0x53, 0x75,
-	0x62, 0x52, 0x08, 0x73, 0x75, 0x62, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x42, 0x0c, 0x0a, 0x0a, 0x5f,
-	0x6f, 0x70, 0x74, 0x5f, 0x69, 0x6e, 0x74, 0x33, 0x32, 0x42, 0x0c, 0x0a, 0x0a, 0x5f, 0x6f, 0x70,
-	0x74, 0x5f, 0x69, 0x6e, 0x74, 0x36, 0x34, 0x42, 0x0d, 0x0a, 0x0b, 0x5f, 0x6f, 0x70, 0x74, 0x5f,
-	0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x22, 0x1b, 0x0a, 0x03, 0x53, 0x75, 0x62, 0x12, 0x14, 0x0a,
-	0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6e, 0x61, 0x6d,
-	0x69, 0x6e, 0x67, 0x42, 0x2f, 0x5a, 0x2d, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f,
-	0x6d, 0x2f, 0x67, 0x6f, 0x2d, 0x6b, 0x72, 0x61, 0x74, 0x6f, 0x73, 0x2f, 0x6b, 0x72, 0x61, 0x74,
-	0x6f, 0x73, 0x2f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x62, 0x69, 0x6e,
-	0x64, 0x69, 0x6e, 0x67, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+	0x62, 0x52, 0x08, 0x73, 0x75, 0x62, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x12, 0x24, 0x0a, 0x0d, 0x74,
+	0x65, 0x73, 0x74, 0x5f, 0x72, 0x65, 0x70, 0x65, 0x61, 0x74, 0x65, 0x64, 0x18, 0x08, 0x20, 0x03,
+	0x28, 0x09, 0x52, 0x0d, 0x74, 0x65, 0x73, 0x74, 0x5f, 0x72, 0x65, 0x70, 0x65, 0x61, 0x74, 0x65,
+	0x64, 0x42, 0x0c, 0x0a, 0x0a, 0x5f, 0x6f, 0x70, 0x74, 0x5f, 0x69, 0x6e, 0x74, 0x33, 0x32, 0x42,
+	0x0c, 0x0a, 0x0a, 0x5f, 0x6f, 0x70, 0x74, 0x5f, 0x69, 0x6e, 0x74, 0x36, 0x34, 0x42, 0x0d, 0x0a,
+	0x0b, 0x5f, 0x6f, 0x70, 0x74, 0x5f, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x22, 0x1b, 0x0a, 0x03,
+	0x53, 0x75, 0x62, 0x12, 0x14, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28,
+	0x09, 0x52, 0x06, 0x6e, 0x61, 0x6d, 0x69, 0x6e, 0x67, 0x42, 0x2f, 0x5a, 0x2d, 0x67, 0x69, 0x74,
+	0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x2d, 0x6b, 0x72, 0x61, 0x74, 0x6f,
+	0x73, 0x2f, 0x6b, 0x72, 0x61, 0x74, 0x6f, 0x73, 0x2f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f,
+	0x72, 0x74, 0x2f, 0x62, 0x69, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74,
+	0x6f, 0x33,
 }
 
 var (
diff --git a/internal/testdata/binding/test.proto b/internal/testdata/binding/test.proto
index 613bd19d..f6df42fc 100644
--- a/internal/testdata/binding/test.proto
+++ b/internal/testdata/binding/test.proto
@@ -15,6 +15,7 @@ message HelloRequest {
   optional int64 opt_int64 = 5;
   optional string opt_string = 6;
   Sub subField = 7;
+  repeated string test_repeated = 8 [json_name = "test_repeated"];
 }
 
 message Sub{
diff --git a/internal/testdata/helloworld/generate.go b/internal/testdata/helloworld/generate.go
new file mode 100644
index 00000000..05de6fcb
--- /dev/null
+++ b/internal/testdata/helloworld/generate.go
@@ -0,0 +1,3 @@
+package helloworld
+
+//go:generate protoc -I . -I ../../../third_party --go_out=paths=source_relative:. --go-grpc_out=paths=source_relative:. --go-http_out=paths=source_relative:. ./helloworld.proto
diff --git a/internal/testdata/helloworld/helloworld.pb.go b/internal/testdata/helloworld/helloworld.pb.go
index 656819d4..48d44179 100644
--- a/internal/testdata/helloworld/helloworld.pb.go
+++ b/internal/testdata/helloworld/helloworld.pb.go
@@ -1,8 +1,8 @@
 // Code generated by protoc-gen-go. DO NOT EDIT.
 // versions:
-// 	protoc-gen-go v1.27.1
+// 	protoc-gen-go v1.28.0
 // 	protoc        v3.17.3
-// source: helloworld/helloworld.proto
+// source: helloworld.proto
 
 package helloworld
 
@@ -33,7 +33,7 @@ type HelloRequest struct {
 func (x *HelloRequest) Reset() {
 	*x = HelloRequest{}
 	if protoimpl.UnsafeEnabled {
-		mi := &file_helloworld_helloworld_proto_msgTypes[0]
+		mi := &file_helloworld_proto_msgTypes[0]
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		ms.StoreMessageInfo(mi)
 	}
@@ -46,7 +46,7 @@ func (x *HelloRequest) String() string {
 func (*HelloRequest) ProtoMessage() {}
 
 func (x *HelloRequest) ProtoReflect() protoreflect.Message {
-	mi := &file_helloworld_helloworld_proto_msgTypes[0]
+	mi := &file_helloworld_proto_msgTypes[0]
 	if protoimpl.UnsafeEnabled && x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -59,7 +59,7 @@ func (x *HelloRequest) ProtoReflect() protoreflect.Message {
 
 // Deprecated: Use HelloRequest.ProtoReflect.Descriptor instead.
 func (*HelloRequest) Descriptor() ([]byte, []int) {
-	return file_helloworld_helloworld_proto_rawDescGZIP(), []int{0}
+	return file_helloworld_proto_rawDescGZIP(), []int{0}
 }
 
 func (x *HelloRequest) GetName() string {
@@ -81,7 +81,7 @@ type HelloReply struct {
 func (x *HelloReply) Reset() {
 	*x = HelloReply{}
 	if protoimpl.UnsafeEnabled {
-		mi := &file_helloworld_helloworld_proto_msgTypes[1]
+		mi := &file_helloworld_proto_msgTypes[1]
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		ms.StoreMessageInfo(mi)
 	}
@@ -94,7 +94,7 @@ func (x *HelloReply) String() string {
 func (*HelloReply) ProtoMessage() {}
 
 func (x *HelloReply) ProtoReflect() protoreflect.Message {
-	mi := &file_helloworld_helloworld_proto_msgTypes[1]
+	mi := &file_helloworld_proto_msgTypes[1]
 	if protoimpl.UnsafeEnabled && x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -107,7 +107,7 @@ func (x *HelloReply) ProtoReflect() protoreflect.Message {
 
 // Deprecated: Use HelloReply.ProtoReflect.Descriptor instead.
 func (*HelloReply) Descriptor() ([]byte, []int) {
-	return file_helloworld_helloworld_proto_rawDescGZIP(), []int{1}
+	return file_helloworld_proto_rawDescGZIP(), []int{1}
 }
 
 func (x *HelloReply) GetMessage() string {
@@ -117,65 +117,71 @@ func (x *HelloReply) GetMessage() string {
 	return ""
 }
 
-var File_helloworld_helloworld_proto protoreflect.FileDescriptor
-
-var file_helloworld_helloworld_proto_rawDesc = []byte{
-	0x0a, 0x1b, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x2f, 0x68, 0x65, 0x6c,
-	0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0a, 0x68,
-	0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x1a, 0x1c, 0x67, 0x6f, 0x6f, 0x67, 0x6c,
-	0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x61, 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e,
-	0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x22, 0x0a, 0x0c, 0x48, 0x65, 0x6c, 0x6c, 0x6f,
-	0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18,
-	0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x26, 0x0a, 0x0a, 0x48,
-	0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73,
-	0x73, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73,
-	0x61, 0x67, 0x65, 0x32, 0x63, 0x0a, 0x07, 0x47, 0x72, 0x65, 0x65, 0x74, 0x65, 0x72, 0x12, 0x58,
-	0x0a, 0x08, 0x53, 0x61, 0x79, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x12, 0x18, 0x2e, 0x68, 0x65, 0x6c,
-	0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x2e, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x71,
-	0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c,
-	0x64, 0x2e, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x22, 0x1a, 0x82, 0xd3,
-	0xe4, 0x93, 0x02, 0x14, 0x12, 0x12, 0x2f, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c,
-	0x64, 0x2f, 0x7b, 0x6e, 0x61, 0x6d, 0x65, 0x7d, 0x42, 0x3d, 0x5a, 0x3b, 0x67, 0x69, 0x74, 0x68,
-	0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x2d, 0x6b, 0x72, 0x61, 0x74, 0x6f, 0x73,
-	0x2f, 0x6b, 0x72, 0x61, 0x74, 0x6f, 0x73, 0x2f, 0x76, 0x32, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72,
-	0x6e, 0x61, 0x6c, 0x2f, 0x74, 0x65, 0x73, 0x74, 0x64, 0x61, 0x74, 0x61, 0x2f, 0x68, 0x65, 0x6c,
-	0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+var File_helloworld_proto protoreflect.FileDescriptor
+
+var file_helloworld_proto_rawDesc = []byte{
+	0x0a, 0x10, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x2e, 0x70, 0x72, 0x6f,
+	0x74, 0x6f, 0x12, 0x0a, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x1a, 0x1c,
+	0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x61, 0x6e, 0x6e, 0x6f, 0x74,
+	0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x22, 0x0a, 0x0c,
+	0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04,
+	0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65,
+	0x22, 0x26, 0x0a, 0x0a, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x18,
+	0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
+	0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x32, 0xab, 0x01, 0x0a, 0x07, 0x47, 0x72, 0x65,
+	0x65, 0x74, 0x65, 0x72, 0x12, 0x58, 0x0a, 0x08, 0x53, 0x61, 0x79, 0x48, 0x65, 0x6c, 0x6c, 0x6f,
+	0x12, 0x18, 0x2e, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x2e, 0x48, 0x65,
+	0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x68, 0x65, 0x6c,
+	0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x2e, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x70,
+	0x6c, 0x79, 0x22, 0x1a, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x14, 0x12, 0x12, 0x2f, 0x68, 0x65, 0x6c,
+	0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x2f, 0x7b, 0x6e, 0x61, 0x6d, 0x65, 0x7d, 0x12, 0x46,
+	0x0a, 0x0e, 0x53, 0x61, 0x79, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d,
+	0x12, 0x18, 0x2e, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x2e, 0x48, 0x65,
+	0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x68, 0x65, 0x6c,
+	0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x2e, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x70,
+	0x6c, 0x79, 0x28, 0x01, 0x30, 0x01, 0x42, 0x3d, 0x5a, 0x3b, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62,
+	0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x2d, 0x6b, 0x72, 0x61, 0x74, 0x6f, 0x73, 0x2f, 0x6b,
+	0x72, 0x61, 0x74, 0x6f, 0x73, 0x2f, 0x76, 0x32, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61,
+	0x6c, 0x2f, 0x74, 0x65, 0x73, 0x74, 0x64, 0x61, 0x74, 0x61, 0x2f, 0x68, 0x65, 0x6c, 0x6c, 0x6f,
+	0x77, 0x6f, 0x72, 0x6c, 0x64, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
 }
 
 var (
-	file_helloworld_helloworld_proto_rawDescOnce sync.Once
-	file_helloworld_helloworld_proto_rawDescData = file_helloworld_helloworld_proto_rawDesc
+	file_helloworld_proto_rawDescOnce sync.Once
+	file_helloworld_proto_rawDescData = file_helloworld_proto_rawDesc
 )
 
-func file_helloworld_helloworld_proto_rawDescGZIP() []byte {
-	file_helloworld_helloworld_proto_rawDescOnce.Do(func() {
-		file_helloworld_helloworld_proto_rawDescData = protoimpl.X.CompressGZIP(file_helloworld_helloworld_proto_rawDescData)
+func file_helloworld_proto_rawDescGZIP() []byte {
+	file_helloworld_proto_rawDescOnce.Do(func() {
+		file_helloworld_proto_rawDescData = protoimpl.X.CompressGZIP(file_helloworld_proto_rawDescData)
 	})
-	return file_helloworld_helloworld_proto_rawDescData
+	return file_helloworld_proto_rawDescData
 }
 
-var file_helloworld_helloworld_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
-var file_helloworld_helloworld_proto_goTypes = []interface{}{
+var file_helloworld_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
+var file_helloworld_proto_goTypes = []interface{}{
 	(*HelloRequest)(nil), // 0: helloworld.HelloRequest
 	(*HelloReply)(nil),   // 1: helloworld.HelloReply
 }
-var file_helloworld_helloworld_proto_depIdxs = []int32{
+var file_helloworld_proto_depIdxs = []int32{
 	0, // 0: helloworld.Greeter.SayHello:input_type -> helloworld.HelloRequest
-	1, // 1: helloworld.Greeter.SayHello:output_type -> helloworld.HelloReply
-	1, // [1:2] is the sub-list for method output_type
-	0, // [0:1] is the sub-list for method input_type
+	0, // 1: helloworld.Greeter.SayHelloStream:input_type -> helloworld.HelloRequest
+	1, // 2: helloworld.Greeter.SayHello:output_type -> helloworld.HelloReply
+	1, // 3: helloworld.Greeter.SayHelloStream:output_type -> helloworld.HelloReply
+	2, // [2:4] is the sub-list for method output_type
+	0, // [0:2] is the sub-list for method input_type
 	0, // [0:0] is the sub-list for extension type_name
 	0, // [0:0] is the sub-list for extension extendee
 	0, // [0:0] is the sub-list for field type_name
 }
 
-func init() { file_helloworld_helloworld_proto_init() }
-func file_helloworld_helloworld_proto_init() {
-	if File_helloworld_helloworld_proto != nil {
+func init() { file_helloworld_proto_init() }
+func file_helloworld_proto_init() {
+	if File_helloworld_proto != nil {
 		return
 	}
 	if !protoimpl.UnsafeEnabled {
-		file_helloworld_helloworld_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
+		file_helloworld_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
 			switch v := v.(*HelloRequest); i {
 			case 0:
 				return &v.state
@@ -187,7 +193,7 @@ func file_helloworld_helloworld_proto_init() {
 				return nil
 			}
 		}
-		file_helloworld_helloworld_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
+		file_helloworld_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
 			switch v := v.(*HelloReply); i {
 			case 0:
 				return &v.state
@@ -204,18 +210,18 @@ func file_helloworld_helloworld_proto_init() {
 	out := protoimpl.TypeBuilder{
 		File: protoimpl.DescBuilder{
 			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
-			RawDescriptor: file_helloworld_helloworld_proto_rawDesc,
+			RawDescriptor: file_helloworld_proto_rawDesc,
 			NumEnums:      0,
 			NumMessages:   2,
 			NumExtensions: 0,
 			NumServices:   1,
 		},
-		GoTypes:           file_helloworld_helloworld_proto_goTypes,
-		DependencyIndexes: file_helloworld_helloworld_proto_depIdxs,
-		MessageInfos:      file_helloworld_helloworld_proto_msgTypes,
+		GoTypes:           file_helloworld_proto_goTypes,
+		DependencyIndexes: file_helloworld_proto_depIdxs,
+		MessageInfos:      file_helloworld_proto_msgTypes,
 	}.Build()
-	File_helloworld_helloworld_proto = out.File
-	file_helloworld_helloworld_proto_rawDesc = nil
-	file_helloworld_helloworld_proto_goTypes = nil
-	file_helloworld_helloworld_proto_depIdxs = nil
+	File_helloworld_proto = out.File
+	file_helloworld_proto_rawDesc = nil
+	file_helloworld_proto_goTypes = nil
+	file_helloworld_proto_depIdxs = nil
 }
diff --git a/internal/testdata/helloworld/helloworld.proto b/internal/testdata/helloworld/helloworld.proto
index bdac62c1..ed068a38 100644
--- a/internal/testdata/helloworld/helloworld.proto
+++ b/internal/testdata/helloworld/helloworld.proto
@@ -14,6 +14,8 @@ service Greeter {
             get: "/helloworld/{name}",
         };
   }
+  // Sends a greeting
+  rpc SayHelloStream (stream HelloRequest) returns (stream HelloReply);
 }
 
 // The request message containing the user's name.
diff --git a/internal/testdata/helloworld/helloworld_grpc.pb.go b/internal/testdata/helloworld/helloworld_grpc.pb.go
index 4e1c32b3..3558cbe4 100644
--- a/internal/testdata/helloworld/helloworld_grpc.pb.go
+++ b/internal/testdata/helloworld/helloworld_grpc.pb.go
@@ -1,4 +1,8 @@
 // Code generated by protoc-gen-go-grpc. DO NOT EDIT.
+// versions:
+// - protoc-gen-go-grpc v1.2.0
+// - protoc             v3.17.3
+// source: helloworld.proto
 
 package helloworld
 
@@ -20,6 +24,8 @@ const _ = grpc.SupportPackageIsVersion7
 type GreeterClient interface {
 	// Sends a greeting
 	SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error)
+	// Sends a greeting
+	SayHelloStream(ctx context.Context, opts ...grpc.CallOption) (Greeter_SayHelloStreamClient, error)
 }
 
 type greeterClient struct {
@@ -39,12 +45,45 @@ func (c *greeterClient) SayHello(ctx context.Context, in *HelloRequest, opts ...
 	return out, nil
 }
 
+func (c *greeterClient) SayHelloStream(ctx context.Context, opts ...grpc.CallOption) (Greeter_SayHelloStreamClient, error) {
+	stream, err := c.cc.NewStream(ctx, &Greeter_ServiceDesc.Streams[0], "/helloworld.Greeter/SayHelloStream", opts...)
+	if err != nil {
+		return nil, err
+	}
+	x := &greeterSayHelloStreamClient{stream}
+	return x, nil
+}
+
+type Greeter_SayHelloStreamClient interface {
+	Send(*HelloRequest) error
+	Recv() (*HelloReply, error)
+	grpc.ClientStream
+}
+
+type greeterSayHelloStreamClient struct {
+	grpc.ClientStream
+}
+
+func (x *greeterSayHelloStreamClient) Send(m *HelloRequest) error {
+	return x.ClientStream.SendMsg(m)
+}
+
+func (x *greeterSayHelloStreamClient) Recv() (*HelloReply, error) {
+	m := new(HelloReply)
+	if err := x.ClientStream.RecvMsg(m); err != nil {
+		return nil, err
+	}
+	return m, nil
+}
+
 // GreeterServer is the server API for Greeter service.
 // All implementations must embed UnimplementedGreeterServer
 // for forward compatibility
 type GreeterServer interface {
 	// Sends a greeting
 	SayHello(context.Context, *HelloRequest) (*HelloReply, error)
+	// Sends a greeting
+	SayHelloStream(Greeter_SayHelloStreamServer) error
 	mustEmbedUnimplementedGreeterServer()
 }
 
@@ -55,6 +94,9 @@ type UnimplementedGreeterServer struct {
 func (UnimplementedGreeterServer) SayHello(context.Context, *HelloRequest) (*HelloReply, error) {
 	return nil, status.Errorf(codes.Unimplemented, "method SayHello not implemented")
 }
+func (UnimplementedGreeterServer) SayHelloStream(Greeter_SayHelloStreamServer) error {
+	return status.Errorf(codes.Unimplemented, "method SayHelloStream not implemented")
+}
 func (UnimplementedGreeterServer) mustEmbedUnimplementedGreeterServer() {}
 
 // UnsafeGreeterServer may be embedded to opt out of forward compatibility for this service.
@@ -86,6 +128,32 @@ func _Greeter_SayHello_Handler(srv interface{}, ctx context.Context, dec func(in
 	return interceptor(ctx, in, info, handler)
 }
 
+func _Greeter_SayHelloStream_Handler(srv interface{}, stream grpc.ServerStream) error {
+	return srv.(GreeterServer).SayHelloStream(&greeterSayHelloStreamServer{stream})
+}
+
+type Greeter_SayHelloStreamServer interface {
+	Send(*HelloReply) error
+	Recv() (*HelloRequest, error)
+	grpc.ServerStream
+}
+
+type greeterSayHelloStreamServer struct {
+	grpc.ServerStream
+}
+
+func (x *greeterSayHelloStreamServer) Send(m *HelloReply) error {
+	return x.ServerStream.SendMsg(m)
+}
+
+func (x *greeterSayHelloStreamServer) Recv() (*HelloRequest, error) {
+	m := new(HelloRequest)
+	if err := x.ServerStream.RecvMsg(m); err != nil {
+		return nil, err
+	}
+	return m, nil
+}
+
 // Greeter_ServiceDesc is the grpc.ServiceDesc for Greeter service.
 // It's only intended for direct use with grpc.RegisterService,
 // and not to be introspected or modified (even as a copy)
@@ -98,6 +166,13 @@ var Greeter_ServiceDesc = grpc.ServiceDesc{
 			Handler:    _Greeter_SayHello_Handler,
 		},
 	},
-	Streams:  []grpc.StreamDesc{},
-	Metadata: "helloworld/helloworld.proto",
+	Streams: []grpc.StreamDesc{
+		{
+			StreamName:    "SayHelloStream",
+			Handler:       _Greeter_SayHelloStream_Handler,
+			ServerStreams: true,
+			ClientStreams: true,
+		},
+	},
+	Metadata: "helloworld.proto",
 }
diff --git a/internal/testdata/helloworld/helloworld_http.pb.go b/internal/testdata/helloworld/helloworld_http.pb.go
index 368b5718..b77f02ec 100644
--- a/internal/testdata/helloworld/helloworld_http.pb.go
+++ b/internal/testdata/helloworld/helloworld_http.pb.go
@@ -1,6 +1,6 @@
 // Code generated by protoc-gen-go-http. DO NOT EDIT.
 // versions:
-// protoc-gen-go-http v2.1.0
+// protoc-gen-go-http v2.3.1
 
 package helloworld
 
@@ -17,6 +17,8 @@ var _ = binding.EncodeURL
 
 const _ = http.SupportPackageIsVersion1
 
+const OperationGreeterSayHello = "/helloworld.Greeter/SayHello"
+
 type GreeterHTTPServer interface {
 	SayHello(context.Context, *HelloRequest) (*HelloReply, error)
 }
@@ -35,7 +37,7 @@ func _Greeter_SayHello0_HTTP_Handler(srv GreeterHTTPServer) func(ctx http.Contex
 		if err := ctx.BindVars(&in); err != nil {
 			return err
 		}
-		http.SetOperation(ctx, "/helloworld.Greeter/SayHello")
+		http.SetOperation(ctx, OperationGreeterSayHello)
 		h := ctx.Middleware(func(ctx context.Context, req interface{}) (interface{}, error) {
 			return srv.SayHello(ctx, req.(*HelloRequest))
 		})
@@ -64,7 +66,7 @@ func (c *GreeterHTTPClientImpl) SayHello(ctx context.Context, in *HelloRequest,
 	var out HelloReply
 	pattern := "/helloworld/{name}"
 	path := binding.EncodeURL(pattern, in, true)
-	opts = append(opts, http.Operation("/helloworld.Greeter/SayHello"))
+	opts = append(opts, http.Operation(OperationGreeterSayHello))
 	opts = append(opts, http.PathTemplate(pattern))
 	err := c.cc.Invoke(ctx, "GET", path, nil, &out, opts...)
 	if err != nil {
diff --git a/transport/grpc/resolver/direct/builder_test.go b/transport/grpc/resolver/direct/builder_test.go
index a0d1d31a..1cd6bf40 100644
--- a/transport/grpc/resolver/direct/builder_test.go
+++ b/transport/grpc/resolver/direct/builder_test.go
@@ -1,6 +1,7 @@
 package direct
 
 import (
+	"fmt"
 	"reflect"
 	"testing"
 
@@ -15,9 +16,14 @@ func TestDirectBuilder_Scheme(t *testing.T) {
 	}
 }
 
-type mockConn struct{}
+type mockConn struct {
+	needUpdateStateErr bool
+}
 
 func (m *mockConn) UpdateState(resolver.State) error {
+	if m.needUpdateStateErr {
+		return fmt.Errorf("mock test needUpdateStateErr")
+	}
 	return nil
 }
 
@@ -38,4 +44,11 @@ func TestDirectBuilder_Build(t *testing.T) {
 		t.Errorf("expect no error, got %v", err)
 	}
 	r.ResolveNow(resolver.ResolveNowOptions{})
+	r.Close()
+
+	// need update state err
+	_, err = b.Build(resolver.Target{}, &mockConn{needUpdateStateErr: true}, resolver.BuildOptions{})
+	if err == nil {
+		t.Errorf("expect needUpdateStateErr, got nil")
+	}
 }
diff --git a/transport/grpc/resolver/discovery/builder_test.go b/transport/grpc/resolver/discovery/builder_test.go
index 7a06abfc..11541cb7 100644
--- a/transport/grpc/resolver/discovery/builder_test.go
+++ b/transport/grpc/resolver/discovery/builder_test.go
@@ -28,6 +28,14 @@ func TestWithTimeout(t *testing.T) {
 	}
 }
 
+func TestDisableDebugLog(t *testing.T) {
+	o := &builder{}
+	DisableDebugLog()(o)
+	if !o.debugLogDisabled {
+		t.Errorf("expected debugLogDisabled true, got %v", o.debugLogDisabled)
+	}
+}
+
 type mockDiscovery struct{}
 
 func (m *mockDiscovery) GetService(ctx context.Context, serviceName string) ([]*registry.ServiceInstance, error) {
@@ -35,6 +43,7 @@ func (m *mockDiscovery) GetService(ctx context.Context, serviceName string) ([]*
 }
 
 func (m *mockDiscovery) Watch(ctx context.Context, serviceName string) (registry.Watcher, error) {
+	time.Sleep(time.Microsecond * 500)
 	return &testWatch{}, nil
 }
 
@@ -62,9 +71,29 @@ func (m *mockConn) ParseServiceConfig(serviceConfigJSON string) *serviceconfig.P
 }
 
 func TestBuilder_Build(t *testing.T) {
-	b := NewBuilder(&mockDiscovery{})
-	_, err := b.Build(resolver.Target{Scheme: resolver.GetDefaultScheme(), Endpoint: "gprc://authority/endpoint"}, &mockConn{}, resolver.BuildOptions{})
+	b := NewBuilder(&mockDiscovery{}, DisableDebugLog())
+	_, err := b.Build(
+		resolver.Target{
+			Scheme:   resolver.GetDefaultScheme(),
+			Endpoint: "gprc://authority/endpoint",
+		},
+		&mockConn{},
+		resolver.BuildOptions{},
+	)
 	if err != nil {
 		t.Errorf("expected no error, got %v", err)
+		return
+	}
+	timeoutBuilder := NewBuilder(&mockDiscovery{}, WithTimeout(0))
+	_, err = timeoutBuilder.Build(
+		resolver.Target{
+			Scheme:   resolver.GetDefaultScheme(),
+			Endpoint: "gprc://authority/endpoint",
+		},
+		&mockConn{},
+		resolver.BuildOptions{},
+	)
+	if err == nil {
+		t.Errorf("expected error, got %v", err)
 	}
 }
diff --git a/transport/grpc/resolver/discovery/resolver_test.go b/transport/grpc/resolver/discovery/resolver_test.go
index 1d196382..33d9fc27 100644
--- a/transport/grpc/resolver/discovery/resolver_test.go
+++ b/transport/grpc/resolver/discovery/resolver_test.go
@@ -23,10 +23,16 @@ func (t *testClientConn) UpdateState(s resolver.State) error {
 
 type testWatch struct {
 	err error
+
+	count uint
 }
 
 func (m *testWatch) Next() ([]*registry.ServiceInstance, error) {
 	time.Sleep(time.Millisecond * 200)
+	if m.count > 1 {
+		return nil, nil
+	}
+	m.count++
 	ins := []*registry.ServiceInstance{
 		{
 			ID:        "mock_ID",
@@ -59,6 +65,7 @@ func TestWatch(t *testing.T) {
 		cancel:   cancel,
 		insecure: false,
 	}
+	r.ResolveNow(resolver.ResolveNowOptions{})
 	go func() {
 		time.Sleep(time.Second * 2)
 		r.Close()
@@ -102,7 +109,10 @@ func TestWatchContextCancel(t *testing.T) {
 }
 
 func TestParseAttributes(t *testing.T) {
-	a := parseAttributes(map[string]string{"a": "b"})
+	a := parseAttributes(map[string]string{
+		"a": "b",
+		"c": "d",
+	})
 	if !reflect.DeepEqual("b", a.Value("a").(string)) {
 		t.Errorf("expect b, got %v", a.Value("a"))
 	}
diff --git a/transport/grpc/server_test.go b/transport/grpc/server_test.go
index 5dd779f3..3a8141c2 100644
--- a/transport/grpc/server_test.go
+++ b/transport/grpc/server_test.go
@@ -15,7 +15,6 @@ import (
 	pb "github.com/go-kratos/kratos/v2/internal/testdata/helloworld"
 	"github.com/go-kratos/kratos/v2/middleware"
 	"github.com/go-kratos/kratos/v2/transport"
-
 	"google.golang.org/grpc"
 )
 
@@ -24,6 +23,36 @@ type server struct {
 	pb.UnimplementedGreeterServer
 }
 
+func (s *server) SayHelloStream(streamServer pb.Greeter_SayHelloStreamServer) error {
+	tctx, ok := transport.FromServerContext(streamServer.Context())
+	if ok {
+		tctx.ReplyHeader().Set("123", "123")
+	}
+	var cnt uint
+	for {
+		in, err := streamServer.Recv()
+		if err != nil {
+			return err
+		}
+		if in.Name == "error" {
+			return errors.BadRequest("custom_error", fmt.Sprintf("invalid argument %s", in.Name))
+		}
+		if in.Name == "panic" {
+			panic("server panic")
+		}
+		err = streamServer.Send(&pb.HelloReply{
+			Message: fmt.Sprintf("hello %s", in.Name),
+		})
+		if err != nil {
+			return err
+		}
+		cnt++
+		if cnt > 1 {
+			return nil
+		}
+	}
+}
+
 // SayHello implements helloworld.GreeterServer
 func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
 	if in.Name == "error" {
@@ -97,6 +126,9 @@ func testClient(t *testing.T, srv *Server) {
 			}
 		}),
 	)
+	defer func() {
+		_ = conn.Close()
+	}()
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -109,7 +141,28 @@ func testClient(t *testing.T, srv *Server) {
 	if !reflect.DeepEqual(reply.Message, "Hello kratos") {
 		t.Errorf("expect %s, got %s", "Hello kratos", reply.Message)
 	}
-	_ = conn.Close()
+
+	streamCli, err := client.SayHelloStream(context.Background())
+	if err != nil {
+		t.Error(err)
+		return
+	}
+	defer func() {
+		_ = streamCli.CloseSend()
+	}()
+	err = streamCli.Send(&pb.HelloRequest{Name: "cc"})
+	if err != nil {
+		t.Error(err)
+		return
+	}
+	reply, err = streamCli.Recv()
+	if err != nil {
+		t.Error(err)
+		return
+	}
+	if !reflect.DeepEqual(reply.Message, "hello cc") {
+		t.Errorf("expect %s, got %s", "hello cc", reply.Message)
+	}
 }
 
 func TestNetwork(t *testing.T) {
diff --git a/transport/http/binding/encode_test.go b/transport/http/binding/encode_test.go
index 9898aba1..fdc2bb78 100644
--- a/transport/http/binding/encode_test.go
+++ b/transport/http/binding/encode_test.go
@@ -14,6 +14,34 @@ func TestProtoPath(t *testing.T) {
 	if url != `http://helloworld.Greeter/helloworld/test/sub/2233!!!` {
 		t.Fatalf("proto path not expected!actual: %s ", url)
 	}
+	url = EncodeURL("http://helloworld.Greeter/helloworld/{name}/sub/{sub.name}", nil, false)
+	fmt.Println(url)
+	if url != "http://helloworld.Greeter/helloworld/{name}/sub/{sub.name}" {
+		t.Fatalf("proto path not expected!actual: %s ", url)
+	}
+	url = EncodeURL("http://helloworld.Greeter/helloworld/{}/sub/{sub.name}", &binding.HelloRequest{Name: "test", Sub: &binding.Sub{Name: "hello"}}, false)
+	fmt.Println(url)
+	if url != "http://helloworld.Greeter/helloworld/{}/sub/hello" {
+		t.Fatalf("proto path not expected!actual: %s ", url)
+	}
+	url = EncodeURL("http://helloworld.Greeter/helloworld/{}/sub/{sub.name.cc}", &binding.HelloRequest{Name: "test", Sub: &binding.Sub{Name: "hello"}}, false)
+	fmt.Println(url)
+	if url != "http://helloworld.Greeter/helloworld/{}/sub/{sub.name.cc}" {
+		t.Fatalf("proto path not expected!actual: %s ", url)
+	}
+
+	url = EncodeURL(
+		"http://helloworld.Greeter/helloworld/{}/sub/{test_repeated.1}",
+		&binding.HelloRequest{
+			Name: "test", Sub: &binding.Sub{Name: "hello"},
+			TestRepeated: []string{"123", "456"},
+		},
+		false,
+	)
+	fmt.Println(url)
+	if url != "http://helloworld.Greeter/helloworld/{}/sub/{test_repeated.1}" {
+		t.Fatalf("proto path not expected!actual: %s ", url)
+	}
 
 	url = EncodeURL("http://helloworld.Greeter/helloworld/{name}/sub/{sub.naming}", &binding.HelloRequest{Name: "test", Sub: &binding.Sub{Name: "5566!!!"}}, false)
 	fmt.Println(url)
diff --git a/transport/http/client_test.go b/transport/http/client_test.go
index 731d111d..c768b364 100644
--- a/transport/http/client_test.go
+++ b/transport/http/client_test.go
@@ -8,6 +8,7 @@ import (
 	"errors"
 	"fmt"
 	"io"
+	"log"
 	nethttp "net/http"
 	"reflect"
 	"strconv"
@@ -17,6 +18,7 @@ import (
 	kratosErrors "github.com/go-kratos/kratos/v2/errors"
 	"github.com/go-kratos/kratos/v2/middleware"
 	"github.com/go-kratos/kratos/v2/registry"
+	"github.com/go-kratos/kratos/v2/selector"
 )
 
 type mockRoundTripper struct{}
@@ -25,6 +27,21 @@ func (rt *mockRoundTripper) RoundTrip(req *nethttp.Request) (resp *nethttp.Respo
 	return
 }
 
+type mockCallOption struct {
+	needErr bool
+}
+
+func (x *mockCallOption) before(info *callInfo) error {
+	if x.needErr {
+		return fmt.Errorf("option need return err")
+	}
+	return nil
+}
+
+func (x *mockCallOption) after(info *callInfo, attempt *csAttempt) {
+	log.Println("run in mockCallOption.after")
+}
+
 func TestWithTransport(t *testing.T) {
 	ov := &mockRoundTripper{}
 	o := WithTransport(ov)
@@ -165,6 +182,16 @@ func TestWithDiscovery(t *testing.T) {
 	}
 }
 
+func TestWithSelector(t *testing.T) {
+	ov := &selector.Default{}
+	o := WithSelector(ov)
+	co := &clientOptions{}
+	o(co)
+	if !reflect.DeepEqual(co.selector, ov) {
+		t.Errorf("expected selector to be %v, got %v", ov, co.selector)
+	}
+}
+
 func TestDefaultRequestEncoder(t *testing.T) {
 	req1 := &nethttp.Request{
 		Header: make(nethttp.Header),
@@ -284,10 +311,6 @@ func TestNewClient(t *testing.T) {
 	if err != nil {
 		t.Error(err)
 	}
-	client, err := NewClient(context.Background(), WithDiscovery(&mockDiscovery{}), WithEndpoint("discovery:///go-kratos"))
-	if err != nil {
-		t.Error(err)
-	}
 	_, err = NewClient(context.Background(), WithDiscovery(&mockDiscovery{}), WithEndpoint("discovery:///go-kratos"))
 	if err != nil {
 		t.Error(err)
@@ -296,13 +319,43 @@ func TestNewClient(t *testing.T) {
 	if err != nil {
 		t.Error(err)
 	}
+	_, err = NewClient(context.Background(), WithEndpoint("127.0.0.1:8888:xxxxa"))
+	if err == nil {
+		t.Error("except a parseTarget error")
+	}
 	_, err = NewClient(context.Background(), WithDiscovery(&mockDiscovery{}), WithEndpoint("https://go-kratos.dev/"))
 	if err == nil {
 		t.Error("err should not be equal to nil")
 	}
 
-	err = client.Invoke(context.Background(), "POST", "/go", map[string]string{"name": "kratos"}, nil, EmptyCallOption{})
+	client, err := NewClient(
+		context.Background(),
+		WithDiscovery(&mockDiscovery{}),
+		WithEndpoint("discovery:///go-kratos"),
+		WithMiddleware(func(handler middleware.Handler) middleware.Handler {
+			t.Logf("handle in middleware")
+			return func(ctx context.Context, req interface{}) (interface{}, error) {
+				return handler(ctx, req)
+			}
+		}),
+	)
+	if err != nil {
+		t.Error(err)
+	}
+
+	err = client.Invoke(context.Background(), "POST", "/go", map[string]string{"name": "kratos"}, nil, EmptyCallOption{}, &mockCallOption{})
 	if err == nil {
 		t.Error("err should not be equal to nil")
 	}
+	err = client.Invoke(context.Background(), "POST", "/go", map[string]string{"name": "kratos"}, nil, EmptyCallOption{}, &mockCallOption{needErr: true})
+	if err == nil {
+		t.Error("err should be equal to callOption err")
+	}
+	client.opts.encoder = func(ctx context.Context, contentType string, in interface{}) (body []byte, err error) {
+		return nil, fmt.Errorf("mock test encoder error")
+	}
+	err = client.Invoke(context.Background(), "POST", "/go", map[string]string{"name": "kratos"}, nil, EmptyCallOption{})
+	if err == nil {
+		t.Error("err should be equal to encoder error")
+	}
 }
diff --git a/transport/http/context_test.go b/transport/http/context_test.go
index 85a7aaaa..3b7466bf 100644
--- a/transport/http/context_test.go
+++ b/transport/http/context_test.go
@@ -3,6 +3,8 @@ package http
 import (
 	"bytes"
 	"context"
+	"errors"
+	"fmt"
 	"net/http"
 	"net/http/httptest"
 	"net/url"
@@ -90,6 +92,11 @@ func TestContextResponse(t *testing.T) {
 	if err != nil {
 		t.Errorf("expected %v, got %v", nil, err)
 	}
+	needErr := fmt.Errorf("some error")
+	err = w.Returns(map[string]string{}, needErr)
+	if !errors.Is(err, needErr) {
+		t.Errorf("expected %v, got %v", needErr, err)
+	}
 }
 
 func TestContextBindQuery(t *testing.T) {
diff --git a/transport/http/resolver_test.go b/transport/http/resolver_test.go
index 419bae44..27473d12 100644
--- a/transport/http/resolver_test.go
+++ b/transport/http/resolver_test.go
@@ -61,22 +61,42 @@ func (m *mockRebalancer) Apply(nodes []selector.Node) {}
 
 type mockDiscoveries struct {
 	isSecure bool
+	nextErr  bool
+	stopErr  bool
 }
 
 func (d *mockDiscoveries) GetService(ctx context.Context, serviceName string) ([]*registry.ServiceInstance, error) {
 	return nil, nil
 }
 
+const errServiceName = "needErr"
+
 func (d *mockDiscoveries) Watch(ctx context.Context, serviceName string) (registry.Watcher, error) {
-	return &mockWatch{isSecure: d.isSecure}, nil
+	if serviceName == errServiceName {
+		return nil, fmt.Errorf("mock test service name watch err")
+	}
+	return &mockWatch{ctx: ctx, isSecure: d.isSecure, nextErr: d.nextErr, stopErr: d.stopErr}, nil
 }
 
 type mockWatch struct {
+	ctx context.Context
+
 	isSecure bool
 	count    int
+
+	nextErr bool
+	stopErr bool
 }
 
 func (m *mockWatch) Next() ([]*registry.ServiceInstance, error) {
+	select {
+	case <-m.ctx.Done():
+		return nil, m.ctx.Err()
+	default:
+	}
+	if m.nextErr {
+		return nil, errors.New("mock test error")
+	}
 	if m.count == 1 {
 		return nil, errors.New("mock test error")
 	}
@@ -95,21 +115,61 @@ func (m *mockWatch) Next() ([]*registry.ServiceInstance, error) {
 }
 
 func (m *mockWatch) Stop() error {
+	if m.stopErr {
+		return fmt.Errorf("mock test error")
+	}
+	// 标记 next 需要报错
+	m.nextErr = true
 	return nil
 }
 
 func TestResolver(t *testing.T) {
-	ta := &Target{
-		Scheme:    "http",
-		Authority: "",
-		Endpoint:  "discovery://helloworld",
+	ta, err := parseTarget("discovery://helloworld", true)
+	if err != nil {
+		t.Errorf("parse err %v", err)
+		return
+	}
+
+	// 异步 无需报错
+	_, err = newResolver(context.Background(), &mockDiscoveries{true, false, false}, ta, &mockRebalancer{}, false, false)
+	if err != nil {
+		t.Errorf("expect %v, got %v", nil, err)
 	}
-	_, err := newResolver(context.Background(), &mockDiscoveries{true}, ta, &mockRebalancer{}, false, false)
+
+	// 同步 一切正常运行
+	_, err = newResolver(context.Background(), &mockDiscoveries{false, false, false}, ta, &mockRebalancer{}, true, true)
 	if err != nil {
 		t.Errorf("expect %v, got %v", nil, err)
 	}
-	_, err = newResolver(context.Background(), &mockDiscoveries{false}, ta, &mockRebalancer{}, true, true)
+
+	// 同步 但是 next 出错 以及 stop 出错
+	_, err = newResolver(context.Background(), &mockDiscoveries{false, true, true}, ta, &mockRebalancer{}, true, true)
+	if err == nil {
+		t.Errorf("expect err, got nil")
+	}
+
+	// 同步 service name watch 失败
+	_, err = newResolver(context.Background(), &mockDiscoveries{false, true, true}, &Target{
+		Scheme:   "discovery",
+		Endpoint: errServiceName,
+	}, &mockRebalancer{}, true, true)
+	if err == nil {
+		t.Errorf("expect err, got nil")
+	}
+
+	cancelCtx, cancel := context.WithCancel(context.Background())
+	cancel()
+
+	// 此处应该打印出来 context.Canceled
+	r, err := newResolver(cancelCtx, &mockDiscoveries{false, false, false}, ta, &mockRebalancer{}, false, false)
 	if err != nil {
 		t.Errorf("expect %v, got %v", nil, err)
 	}
+	_ = r.Close()
+
+	// 同步 但是服务取消，此时需要报错
+	_, err = newResolver(cancelCtx, &mockDiscoveries{false, false, true}, ta, &mockRebalancer{}, true, true)
+	if err == nil {
+		t.Errorf("expect ctx cancel err, got nil")
+	}
 }
diff --git a/transport/http/server_test.go b/transport/http/server_test.go
index 34f2b855..ea60d0cf 100644
--- a/transport/http/server_test.go
+++ b/transport/http/server_test.go
@@ -25,14 +25,28 @@ type testData struct {
 	Path string `json:"path"`
 }
 
+// handleFuncWrapper is a wrapper for http.HandlerFunc to implement http.Handler
+type handleFuncWrapper struct {
+	fn http.HandlerFunc
+}
+
+func (x *handleFuncWrapper) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
+	x.fn.ServeHTTP(writer, request)
+}
+
+func newHandleFuncWrapper(fn http.HandlerFunc) http.Handler {
+	return &handleFuncWrapper{fn: fn}
+}
+
 func TestServer(t *testing.T) {
 	fn := func(w http.ResponseWriter, r *http.Request) {
 		_ = json.NewEncoder(w).Encode(testData{Path: r.RequestURI})
 	}
 	ctx := context.Background()
 	srv := NewServer()
-	srv.HandleFunc("/index", fn)
+	srv.Handle("/index", newHandleFuncWrapper(fn))
 	srv.HandleFunc("/index/{id:[0-9]+}", fn)
+	srv.HandlePrefix("/test/prefix", newHandleFuncWrapper(fn))
 	srv.HandleHeader("content-type", "application/grpc-web+json", func(w http.ResponseWriter, r *http.Request) {
 		_ = json.NewEncoder(w).Encode(testData{Path: r.RequestURI})
 	})
@@ -55,6 +69,7 @@ func TestServer(t *testing.T) {
 	testHeader(t, srv)
 	testClient(t, srv)
 	testAccept(t, srv)
+	time.Sleep(time.Second)
 	if srv.Stop(ctx) != nil {
 		t.Errorf("expected nil got %v", srv.Stop(ctx))
 	}
@@ -121,20 +136,21 @@ func testClient(t *testing.T, srv *Server) {
 		path   string
 		code   int
 	}{
-		{"GET", "/index", 200},
-		{"PUT", "/index", 200},
-		{"POST", "/index", 200},
-		{"PATCH", "/index", 200},
-		{"DELETE", "/index", 200},
+		{"GET", "/index", http.StatusOK},
+		{"PUT", "/index", http.StatusOK},
+		{"POST", "/index", http.StatusOK},
+		{"PATCH", "/index", http.StatusOK},
+		{"DELETE", "/index", http.StatusOK},
 
-		{"GET", "/index/1", 200},
-		{"PUT", "/index/1", 200},
-		{"POST", "/index/1", 200},
-		{"PATCH", "/index/1", 200},
-		{"DELETE", "/index/1", 200},
+		{"GET", "/index/1", http.StatusOK},
+		{"PUT", "/index/1", http.StatusOK},
+		{"POST", "/index/1", http.StatusOK},
+		{"PATCH", "/index/1", http.StatusOK},
+		{"DELETE", "/index/1", http.StatusOK},
 
-		{"GET", "/index/notfound", 404},
-		{"GET", "/errors/cause", 400},
+		{"GET", "/index/notfound", http.StatusNotFound},
+		{"GET", "/errors/cause", http.StatusBadRequest},
+		{"GET", "/test/prefix/123111", http.StatusOK},
 	}
 	e, err := srv.Endpoint()
 	if err != nil {
@@ -307,6 +323,15 @@ func TestTLSConfig(t *testing.T) {
 	}
 }
 
+func TestStrictSlash(t *testing.T) {
+	o := &Server{}
+	v := true
+	StrictSlash(v)(o)
+	if !reflect.DeepEqual(v, o.strictSlash) {
+		t.Errorf("expected %v got %v", v, o.tlsConf)
+	}
+}
+
 func TestListener(t *testing.T) {
 	lis := &net.TCPListener{}
 	s := &Server{}
diff --git a/transport/transport_test.go b/transport/transport_test.go
index c29c51e4..b83c8ee3 100644
--- a/transport/transport_test.go
+++ b/transport/transport_test.go
@@ -55,6 +55,9 @@ func TestServerTransport(t *testing.T) {
 	if mtr == nil {
 		t.Errorf("expected:%v got:%v", nil, mtr)
 	}
+	if mtr.Kind().String() != KindGRPC.String() {
+		t.Errorf("expected:%v got:%v", KindGRPC.String(), mtr.Kind().String())
+	}
 	if !reflect.DeepEqual(mtr.endpoint, "test_endpoint") {
 		t.Errorf("expected:%v got:%v", "test_endpoint", mtr.endpoint)
 	}