Repository: zeromicro/go-zero
Pull request #2152 (2022-07-15 02:56:57)

fix: goctl genhandler duplicate rest/httpx & template import custom
fix: goctl genhandler duplicate rest/httpx & goctl genhandler template support custom import httpx package

Reason:
1、在 handler 层，我对 httpx 包进行了再次封装
2、在某些情况下，我需要控制 httpx 包的导入
3、goctl 1.3.9 会出现两个 rest/httpx 包

Example:
```
# handler.tpl
package {{.PkgName}}

import (
	"net/http"

	"appservice/pkg/resultx"
	{{.ImportPackages}}
        {{if .HasRequest}}
	{{.ImportHttpxPackage}}
        {{end}}
)

func {{.HandlerName}}(svcCtx *svc.ServiceContext) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		{{if .HasRequest}}var req types.{{.RequestType}}
		if err := httpx.Parse(r, &req); err != nil {
			resultx.HttpParamErrorResult(r, w, err)
			return
		}

		{{end}}l := {{.LogicName}}.New{{.LogicType}}(r.Context(), svcCtx)
		{{if .HasResp}}resp, {{end}}err := l.{{.Call}}({{if .HasRequest}}&req{{end}})

		{{if .HasResp}}resultx.HttpResult(r, w, resp, err){{else}}resultx.HttpResult(r, w, nil, err){{end}}

	}
}

```


smithyj: @anqiansong 
Issues-translate-bot:  
> Bot detected the issue body's language is not English, translate it automatically. 👯👭🏻🧑‍🤝‍🧑👫🧑🏿‍🤝‍🧑🏻👩🏾‍🤝‍👨🏿👬🏿         
----    
@An Qian send  
      
smithyj: @kevwan 
Issues-translate-bot:  
> Bot detected the issue body's language is not English, translate it automatically. 👯👭🏻🧑‍🤝‍🧑👫🧑🏿‍🤝‍🧑🏻👩🏾‍🤝‍👨🏿👬🏿         
----    
@kevwan  
      

Patch:
diff --git a/gateway/config.go b/gateway/config.go
new file mode 100644
index 00000000..a547d2ae
--- /dev/null
+++ b/gateway/config.go
@@ -0,0 +1,38 @@
+package gateway
+
+import (
+	"time"
+
+	"github.com/zeromicro/go-zero/rest"
+	"github.com/zeromicro/go-zero/zrpc"
+)
+
+type (
+	// GatewayConf is the configuration for gateway.
+	GatewayConf struct {
+		rest.RestConf
+		Upstreams []Upstream
+		Timeout   time.Duration `json:",default=5s"`
+	}
+
+	// RouteMapping is a mapping between a gateway route and an upstream rpc method.
+	RouteMapping struct {
+		// Method is the HTTP method, like GET, POST, PUT, DELETE.
+		Method string
+		// Path is the HTTP path.
+		Path string
+		// RpcPath is the gRPC rpc method, with format of package.service/method
+		RpcPath string
+	}
+
+	// Upstream is the configuration for an upstream.
+	Upstream struct {
+		// Grpc is the target of the upstream.
+		Grpc zrpc.RpcClientConf
+		// ProtoSet is the file of proto set, like hello.pb
+		ProtoSet string `json:",optional"`
+		// Mapping is the mapping between gateway routes and Upstream rpc methods.
+		// Keep it blank if annotations are added in rpc methods.
+		Mapping []RouteMapping `json:",optional"`
+	}
+)
diff --git a/gateway/internal/descriptorsource.go b/gateway/internal/descriptorsource.go
new file mode 100644
index 00000000..8e1895bc
--- /dev/null
+++ b/gateway/internal/descriptorsource.go
@@ -0,0 +1,102 @@
+package internal
+
+import (
+	"fmt"
+	"net/http"
+	"strings"
+
+	"github.com/fullstorydev/grpcurl"
+	"github.com/jhump/protoreflect/desc"
+	"google.golang.org/genproto/googleapis/api/annotations"
+	"google.golang.org/protobuf/proto"
+)
+
+type Method struct {
+	HttpMethod string
+	HttpPath   string
+	RpcPath    string
+}
+
+// GetMethods returns all methods of the given grpcurl.DescriptorSource.
+func GetMethods(source grpcurl.DescriptorSource) ([]Method, error) {
+	svcs, err := source.ListServices()
+	if err != nil {
+		return nil, err
+	}
+
+	var methods []Method
+	for _, svc := range svcs {
+		d, err := source.FindSymbol(svc)
+		if err != nil {
+			return nil, err
+		}
+
+		switch val := d.(type) {
+		case *desc.ServiceDescriptor:
+			svcMethods := val.GetMethods()
+			for _, method := range svcMethods {
+				rpcPath := fmt.Sprintf("%s/%s", svc, method.GetName())
+				ext := proto.GetExtension(method.GetMethodOptions(), annotations.E_Http)
+				if ext == nil {
+					methods = append(methods, Method{
+						RpcPath: rpcPath,
+					})
+					continue
+				}
+
+				httpExt, ok := ext.(*annotations.HttpRule)
+				if !ok {
+					methods = append(methods, Method{
+						RpcPath: rpcPath,
+					})
+					continue
+				}
+
+				switch rule := httpExt.GetPattern().(type) {
+				case *annotations.HttpRule_Get:
+					methods = append(methods, Method{
+						HttpMethod: http.MethodGet,
+						HttpPath:   adjustHttpPath(rule.Get),
+						RpcPath:    rpcPath,
+					})
+				case *annotations.HttpRule_Post:
+					methods = append(methods, Method{
+						HttpMethod: http.MethodPost,
+						HttpPath:   adjustHttpPath(rule.Post),
+						RpcPath:    rpcPath,
+					})
+				case *annotations.HttpRule_Put:
+					methods = append(methods, Method{
+						HttpMethod: http.MethodPut,
+						HttpPath:   adjustHttpPath(rule.Put),
+						RpcPath:    rpcPath,
+					})
+				case *annotations.HttpRule_Delete:
+					methods = append(methods, Method{
+						HttpMethod: http.MethodDelete,
+						HttpPath:   adjustHttpPath(rule.Delete),
+						RpcPath:    rpcPath,
+					})
+				case *annotations.HttpRule_Patch:
+					methods = append(methods, Method{
+						HttpMethod: http.MethodPatch,
+						HttpPath:   adjustHttpPath(rule.Patch),
+						RpcPath:    rpcPath,
+					})
+				default:
+					methods = append(methods, Method{
+						RpcPath: rpcPath,
+					})
+				}
+			}
+		}
+	}
+
+	return methods, nil
+}
+
+func adjustHttpPath(path string) string {
+	path = strings.ReplaceAll(path, "{", ":")
+	path = strings.ReplaceAll(path, "}", "")
+	return path
+}
diff --git a/gateway/internal/headerprocessor.go b/gateway/internal/headerprocessor.go
new file mode 100644
index 00000000..11181f78
--- /dev/null
+++ b/gateway/internal/headerprocessor.go
@@ -0,0 +1,30 @@
+package internal
+
+import (
+	"fmt"
+	"net/http"
+	"strings"
+)
+
+const (
+	metadataHeaderPrefix = "Grpc-Metadata-"
+	metadataPrefix       = "gateway-"
+)
+
+// ProcessHeaders builds the headers for the gateway from HTTP headers.
+func ProcessHeaders(header http.Header) []string {
+	var headers []string
+
+	for k, v := range header {
+		if !strings.HasPrefix(k, metadataHeaderPrefix) {
+			continue
+		}
+
+		key := fmt.Sprintf("%s%s", metadataPrefix, strings.TrimPrefix(k, metadataHeaderPrefix))
+		for _, vv := range v {
+			headers = append(headers, key+":"+vv)
+		}
+	}
+
+	return headers
+}
diff --git a/gateway/internal/requestparser.go b/gateway/internal/requestparser.go
new file mode 100644
index 00000000..344931d7
--- /dev/null
+++ b/gateway/internal/requestparser.go
@@ -0,0 +1,53 @@
+package internal
+
+import (
+	"bytes"
+	"encoding/json"
+	"net/http"
+
+	"github.com/fullstorydev/grpcurl"
+	"github.com/golang/protobuf/jsonpb"
+	"github.com/zeromicro/go-zero/rest/httpx"
+	"github.com/zeromicro/go-zero/rest/pathvar"
+)
+
+// NewRequestParser creates a new request parser from the given http.Request and resolver.
+func NewRequestParser(r *http.Request, resolver jsonpb.AnyResolver) (grpcurl.RequestParser, error) {
+	vars := pathvar.Vars(r)
+	params, err := httpx.GetFormValues(r)
+	if err != nil {
+		return nil, err
+	}
+
+	for k, v := range vars {
+		params[k] = v
+	}
+	if len(params) == 0 {
+		return grpcurl.NewJSONRequestParser(r.Body, resolver), nil
+	}
+
+	if r.ContentLength == 0 {
+		return buildJsonRequestParser(params, resolver)
+	}
+
+	m := make(map[string]interface{})
+	if err := json.NewDecoder(r.Body).Decode(&m); err != nil {
+		return nil, err
+	}
+
+	for k, v := range params {
+		m[k] = v
+	}
+
+	return buildJsonRequestParser(m, resolver)
+}
+
+func buildJsonRequestParser(m map[string]interface{}, resolver jsonpb.AnyResolver) (
+	grpcurl.RequestParser, error) {
+	var buf bytes.Buffer
+	if err := json.NewEncoder(&buf).Encode(m); err != nil {
+		return nil, err
+	}
+
+	return grpcurl.NewJSONRequestParser(&buf, resolver), nil
+}
diff --git a/gateway/internal/timeout.go b/gateway/internal/timeout.go
new file mode 100644
index 00000000..afe5ca8b
--- /dev/null
+++ b/gateway/internal/timeout.go
@@ -0,0 +1,19 @@
+package internal
+
+import (
+	"net/http"
+	"time"
+)
+
+const grpcTimeoutHeader = "Grpc-Timeout"
+
+// GetTimeout returns the timeout from the header, if not set, returns the default timeout.
+func GetTimeout(header http.Header, defaultTimeout time.Duration) time.Duration {
+	if timeout := header.Get(grpcTimeoutHeader); len(timeout) > 0 {
+		if t, err := time.ParseDuration(timeout); err == nil {
+			return t
+		}
+	}
+
+	return defaultTimeout
+}
diff --git a/gateway/readme.md b/gateway/readme.md
new file mode 100644
index 00000000..1fef0bec
--- /dev/null
+++ b/gateway/readme.md
@@ -0,0 +1,56 @@
+# Gateway
+
+## Usage
+
+- main.go
+
+```go
+var configFile = flag.String("f", "config.yaml", "config file")
+
+func main() {
+	flag.Parse()
+
+	var c gateway.GatewayConf
+	conf.MustLoad(*configFile, &c)
+	gw := gateway.MustNewServer(c)
+	defer gw.Stop()
+	gw.Start()
+}
+```
+
+- config.yaml
+
+```yaml
+Name: demo-gateway
+Host: localhost
+Port: 8888
+Upstreams:
+  - Grpc:
+      Etcd:
+        Hosts:
+        - localhost:2379
+        Key: hello.rpc
+    # protoset mode
+    ProtoSet: hello.pb
+    Mapping:
+      - Method: get
+        Path: /pingHello/:ping
+        RpcPath: hello.Hello/Ping
+  - Grpc:
+      Endpoints:
+        - localhost:8081
+    # reflection mode, no ProtoSet settings
+    Mapping:
+      - Method: post
+        Path: /pingWorld
+        RpcPath: world.World/Ping
+```
+
+## Generate ProtoSet files
+
+- example command
+
+```shell
+protoc --descriptor_set_out=hello.pb hello.proto
+```
+
diff --git a/gateway/server.go b/gateway/server.go
new file mode 100644
index 00000000..a15b17da
--- /dev/null
+++ b/gateway/server.go
@@ -0,0 +1,174 @@
+package gateway
+
+import (
+	"context"
+	"fmt"
+	"net/http"
+	"strings"
+	"time"
+
+	"github.com/fullstorydev/grpcurl"
+	"github.com/golang/protobuf/jsonpb"
+	"github.com/jhump/protoreflect/grpcreflect"
+	"github.com/zeromicro/go-zero/core/logx"
+	"github.com/zeromicro/go-zero/core/mr"
+	"github.com/zeromicro/go-zero/gateway/internal"
+	"github.com/zeromicro/go-zero/rest"
+	"github.com/zeromicro/go-zero/rest/httpx"
+	"github.com/zeromicro/go-zero/zrpc"
+	"google.golang.org/grpc/reflection/grpc_reflection_v1alpha"
+)
+
+type (
+	// Server is a gateway server.
+	Server struct {
+		*rest.Server
+		upstreams     []Upstream
+		timeout       time.Duration
+		processHeader func(http.Header) []string
+	}
+
+	// Option defines the method to customize Server.
+	Option func(svr *Server)
+)
+
+// MustNewServer creates a new gateway server.
+func MustNewServer(c GatewayConf, opts ...Option) *Server {
+	svr := &Server{
+		Server:    rest.MustNewServer(c.RestConf),
+		upstreams: c.Upstreams,
+		timeout:   c.Timeout,
+	}
+	for _, opt := range opts {
+		opt(svr)
+	}
+
+	return svr
+}
+
+// Start starts the gateway server.
+func (s *Server) Start() {
+	logx.Must(s.build())
+	s.Server.Start()
+}
+
+// Stop stops the gateway server.
+func (s *Server) Stop() {
+	s.Server.Stop()
+}
+
+func (s *Server) build() error {
+	return mr.MapReduceVoid(func(source chan<- interface{}) {
+		for _, up := range s.upstreams {
+			source <- up
+		}
+	}, func(item interface{}, writer mr.Writer, cancel func(error)) {
+		up := item.(Upstream)
+		cli := zrpc.MustNewClient(up.Grpc)
+		source, err := s.createDescriptorSource(cli, up)
+		if err != nil {
+			cancel(err)
+			return
+		}
+
+		methods, err := internal.GetMethods(source)
+		if err != nil {
+			cancel(err)
+			return
+		}
+
+		resolver := grpcurl.AnyResolverFromDescriptorSource(source)
+		for _, m := range methods {
+			if len(m.HttpMethod) > 0 && len(m.HttpPath) > 0 {
+				writer.Write(rest.Route{
+					Method:  m.HttpMethod,
+					Path:    m.HttpPath,
+					Handler: s.buildHandler(source, resolver, cli, m.RpcPath),
+				})
+			}
+		}
+
+		methodSet := make(map[string]struct{})
+		for _, m := range methods {
+			methodSet[m.RpcPath] = struct{}{}
+		}
+		for _, m := range up.Mapping {
+			if _, ok := methodSet[m.RpcPath]; !ok {
+				cancel(fmt.Errorf("rpc method %s not found", m.RpcPath))
+				return
+			}
+
+			writer.Write(rest.Route{
+				Method:  strings.ToUpper(m.Method),
+				Path:    m.Path,
+				Handler: s.buildHandler(source, resolver, cli, m.RpcPath),
+			})
+		}
+	}, func(pipe <-chan interface{}, cancel func(error)) {
+		for item := range pipe {
+			route := item.(rest.Route)
+			s.Server.AddRoute(route)
+		}
+	})
+}
+
+func (s *Server) buildHandler(source grpcurl.DescriptorSource, resolver jsonpb.AnyResolver,
+	cli zrpc.Client, rpcPath string) func(http.ResponseWriter, *http.Request) {
+	return func(w http.ResponseWriter, r *http.Request) {
+		handler := &grpcurl.DefaultEventHandler{
+			Out: w,
+			Formatter: grpcurl.NewJSONFormatter(true,
+				grpcurl.AnyResolverFromDescriptorSource(source)),
+		}
+		parser, err := internal.NewRequestParser(r, resolver)
+		if err != nil {
+			httpx.Error(w, err)
+			return
+		}
+
+		timeout := internal.GetTimeout(r.Header, s.timeout)
+		ctx, can := context.WithTimeout(r.Context(), timeout)
+		defer can()
+
+		w.Header().Set(httpx.ContentType, httpx.JsonContentType)
+		if err := grpcurl.InvokeRPC(ctx, source, cli.Conn(), rpcPath, s.prepareMetadata(r.Header),
+			handler, parser.Next); err != nil {
+			httpx.Error(w, err)
+		}
+	}
+}
+
+func (s *Server) createDescriptorSource(cli zrpc.Client, up Upstream) (grpcurl.DescriptorSource, error) {
+	var source grpcurl.DescriptorSource
+	var err error
+
+	if len(up.ProtoSet) > 0 {
+		source, err = grpcurl.DescriptorSourceFromProtoSets(up.ProtoSet)
+		if err != nil {
+			return nil, err
+		}
+	} else {
+		refCli := grpc_reflection_v1alpha.NewServerReflectionClient(cli.Conn())
+		client := grpcreflect.NewClient(context.Background(), refCli)
+		source = grpcurl.DescriptorSourceFromServer(context.Background(), client)
+	}
+
+	return source, nil
+}
+
+func (s *Server) prepareMetadata(header http.Header) []string {
+	vals := internal.ProcessHeaders(header)
+	if s.processHeader != nil {
+		vals = append(vals, s.processHeader(header)...)
+	}
+
+	return vals
+}
+
+// WithHeaderProcessor sets a processor to process request headers.
+// The returned headers are used as metadata to invoke the RPC.
+func WithHeaderProcessor(processHeader func(http.Header) []string) func(*Server) {
+	return func(s *Server) {
+		s.processHeader = processHeader
+	}
+}
diff --git a/go.mod b/go.mod
index 3053a124..090d3f93 100644
--- a/go.mod
+++ b/go.mod
@@ -7,12 +7,15 @@ require (
 	github.com/DATA-DOG/go-sqlmock v1.5.0
 	github.com/alicebob/miniredis/v2 v2.22.0
 	github.com/fatih/color v1.13.0
+	github.com/fullstorydev/grpcurl v1.8.6
 	github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8
 	github.com/go-redis/redis/v8 v8.11.5
 	github.com/go-sql-driver/mysql v1.6.0
 	github.com/golang-jwt/jwt/v4 v4.4.2
 	github.com/golang/mock v1.6.0
+	github.com/golang/protobuf v1.5.2
 	github.com/google/uuid v1.3.0
+	github.com/jhump/protoreflect v1.12.0
 	github.com/justinas/alice v1.2.0
 	github.com/lib/pq v1.10.6
 	github.com/olekukonko/tablewriter v0.0.5
diff --git a/go.sum b/go.sum
index 6f7000da..6f39ce1b 100644
--- a/go.sum
+++ b/go.sum
@@ -122,6 +122,8 @@ github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM
 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
 github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
 github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
+github.com/fullstorydev/grpcurl v1.8.6 h1:WylAwnPauJIofYSHqqMTC1eEfUIzqzevXyogBxnQquo=
+github.com/fullstorydev/grpcurl v1.8.6/go.mod h1:WhP7fRQdhxz2TkL97u+TCb505sxfH78W1usyoB3tepw=
 github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
 github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8 h1:DujepqpGd1hyOd7aW59XpK7Qymp8iy83xq74fLr21is=
 github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
@@ -235,6 +237,7 @@ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5m
 github.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2cUuW7uA/OeU=
 github.com/googleapis/gnostic v0.5.5 h1:9fHAtK0uDfpveeqqo1hkEZJcFvYXAiCN3UutL8F9xHw=
 github.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97DwqyJO1AENw9kA=
+github.com/gordonklaus/ineffassign v0.0.0-20200309095847-7953dde2c7bf/go.mod h1:cuNKsD1zp2v6XfE/orVX2QE1LC+i254ceGcVeDT3pTU=
 github.com/gorilla/handlers v1.4.2/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ=
 github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
 github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
@@ -259,6 +262,13 @@ github.com/jcmturner/gofork v1.0.0/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/U
 github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=
 github.com/jcmturner/gokrb5/v8 v8.4.2/go.mod h1:sb+Xq/fTY5yktf/VxLsE3wlfPqQjp0aWNYyvBVK62bc=
 github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
+github.com/jhump/gopoet v0.0.0-20190322174617-17282ff210b3/go.mod h1:me9yfT6IJSlOL3FCfrg+L6yzUEZ+5jW6WHt4Sk+UPUI=
+github.com/jhump/gopoet v0.1.0/go.mod h1:me9yfT6IJSlOL3FCfrg+L6yzUEZ+5jW6WHt4Sk+UPUI=
+github.com/jhump/goprotoc v0.5.0/go.mod h1:VrbvcYrQOrTi3i0Vf+m+oqQWk9l72mjkJCYo7UvLHRQ=
+github.com/jhump/protoreflect v1.10.3/go.mod h1:7GcYQDdMU/O/BBrl/cX6PNHpXh6cenjd8pneu5yW7Tg=
+github.com/jhump/protoreflect v1.11.0/go.mod h1:U7aMIjN0NWq9swDP7xDdoMfRHb35uiuTd3Z9nFXJf5E=
+github.com/jhump/protoreflect v1.12.0 h1:1NQ4FpWMgn3by/n1X0fbeKEUxP1wBt7+Oitpv01HR10=
+github.com/jhump/protoreflect v1.12.0/go.mod h1:JytZfP5d0r8pVNLZvai7U/MCuTWITgrI4tTg7puQFKI=
 github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks=
 github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
 github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
@@ -323,6 +333,7 @@ github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+
 github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 h1:W6apQkHrMkS0Muv8G/TipAy/FJl/rCYT0+EuS8+Z0z4=
 github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms=
 github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
+github.com/nishanths/predeclared v0.0.0-20200524104333-86fad755b4d3/go.mod h1:nt3d53pc1VYcphSCIaYAJtnPYnr3Zyn8fMq2wvPGPso=
 github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
 github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
 github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
@@ -711,8 +722,10 @@ golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWc
 golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
 golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
 golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200522201501-cb1345f3a375/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
 golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
 golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200717024301-6ddee64345a6/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
 golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
 golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
 golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
@@ -799,6 +812,7 @@ google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTp
 google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
 google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
 google.golang.org/grpc v1.41.0/go.mod h1:U3l9uK9J0sini8mHphKoXyaqDA/8VyGnDee1zzIUK6k=
+google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
 google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
 google.golang.org/grpc v1.48.0 h1:rQOsyJ/8+ufEDJd/Gdsz7HG220Mh9HAhFHRGnIjda0w=
 google.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
@@ -812,6 +826,7 @@ google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2
 google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
 google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
 google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
+google.golang.org/protobuf v1.25.1-0.20200805231151-a709e31e5d12/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
 google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
 google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
diff --git a/readme-cn.md b/readme-cn.md
index a97cb9f2..13e50426 100644
--- a/readme-cn.md
+++ b/readme-cn.md
@@ -23,7 +23,7 @@
 > 
 > `GOPROXY=https://goproxy.cn/,direct go install github.com/zeromicro/go-zero/tools/goctl@latest`
 > 
-> `goctl migrate —verbose —version v1.3.4`
+> `goctl migrate —verbose —version v1.3.5`
 
 ## 0. go-zero 介绍
 
diff --git a/readme.md b/readme.md
index ac8d3842..b2712756 100644
--- a/readme.md
+++ b/readme.md
@@ -21,7 +21,7 @@ English | [简体中文](readme-cn.md)
 > 
 > `go install github.com/zeromicro/go-zero/tools/goctl@latest`
 > 
-> `goctl migrate —verbose —version v1.3.4`
+> `goctl migrate —verbose —version v1.3.5`
 
 ## 0. what is go-zero
 
diff --git a/rest/httpx/requests.go b/rest/httpx/requests.go
index f8fe7169..e46025b5 100644
--- a/rest/httpx/requests.go
+++ b/rest/httpx/requests.go
@@ -49,24 +49,11 @@ func ParseHeaders(r *http.Request, v interface{}) error {
 
 // ParseForm parses the form request.
 func ParseForm(r *http.Request, v interface{}) error {
-	if err := r.ParseForm(); err != nil {
+	params, err := GetFormValues(r)
+	if err != nil {
 		return err
 	}
 
-	if err := r.ParseMultipartForm(maxMemory); err != nil {
-		if err != http.ErrNotMultipart {
-			return err
-		}
-	}
-
-	params := make(map[string]interface{}, len(r.Form))
-	for name := range r.Form {
-		formValue := r.Form.Get(name)
-		if len(formValue) > 0 {
-			params[name] = formValue
-		}
-	}
-
 	return formUnmarshaler.Unmarshal(params, v)
 }
 
diff --git a/rest/httpx/util.go b/rest/httpx/util.go
index a5f5196f..216abaa0 100644
--- a/rest/httpx/util.go
+++ b/rest/httpx/util.go
@@ -4,6 +4,29 @@ import "net/http"
 
 const xForwardedFor = "X-Forwarded-For"
 
+// GetFormValues returns the form values.
+func GetFormValues(r *http.Request) (map[string]interface{}, error) {
+	if err := r.ParseForm(); err != nil {
+		return nil, err
+	}
+
+	if err := r.ParseMultipartForm(maxMemory); err != nil {
+		if err != http.ErrNotMultipart {
+			return nil, err
+		}
+	}
+
+	params := make(map[string]interface{}, len(r.Form))
+	for name := range r.Form {
+		formValue := r.Form.Get(name)
+		if len(formValue) > 0 {
+			params[name] = formValue
+		}
+	}
+
+	return params, nil
+}
+
 // GetRemoteAddr returns the peer address, supports X-Forward-For.
 func GetRemoteAddr(r *http.Request) string {
 	v := r.Header.Get(xForwardedFor)
diff --git a/tools/goctl/api/gogen/genhandlers.go b/tools/goctl/api/gogen/genhandlers.go
index 5b286228..560ffe94 100644
--- a/tools/goctl/api/gogen/genhandlers.go
+++ b/tools/goctl/api/gogen/genhandlers.go
@@ -21,15 +21,16 @@ const defaultLogicPackage = "logic"
 var handlerTemplate string
 
 type handlerInfo struct {
-	PkgName        string
-	ImportPackages string
-	HandlerName    string
-	RequestType    string
-	LogicName      string
-	LogicType      string
-	Call           string
-	HasResp        bool
-	HasRequest     bool
+	PkgName            string
+	ImportPackages     string
+	ImportHttpxPackage string
+	HandlerName        string
+	RequestType        string
+	LogicName          string
+	LogicType          string
+	Call               string
+	HasResp            bool
+	HasRequest         bool
 }
 
 func genHandler(dir, rootPkg string, cfg *config.Config, group spec.Group, route spec.Route) error {
@@ -47,15 +48,16 @@ func genHandler(dir, rootPkg string, cfg *config.Config, group spec.Group, route
 	}
 
 	return doGenToFile(dir, handler, cfg, group, route, handlerInfo{
-		PkgName:        pkgName,
-		ImportPackages: genHandlerImports(group, route, parentPkg),
-		HandlerName:    handler,
-		RequestType:    util.Title(route.RequestTypeName()),
-		LogicName:      logicName,
-		LogicType:      strings.Title(getLogicName(route)),
-		Call:           strings.Title(strings.TrimSuffix(handler, "Handler")),
-		HasResp:        len(route.ResponseTypeName()) > 0,
-		HasRequest:     len(route.RequestTypeName()) > 0,
+		PkgName:            pkgName,
+		ImportPackages:     genHandlerImports(group, route, parentPkg),
+		ImportHttpxPackage: fmt.Sprintf("\"%s/rest/httpx\"", vars.ProjectOpenSourceURL),
+		HandlerName:        handler,
+		RequestType:        util.Title(route.RequestTypeName()),
+		LogicName:          logicName,
+		LogicType:          strings.Title(getLogicName(route)),
+		Call:               strings.Title(strings.TrimSuffix(handler, "Handler")),
+		HasResp:            len(route.ResponseTypeName()) > 0,
+		HasRequest:         len(route.RequestTypeName()) > 0,
 	})
 }
 
@@ -99,7 +101,6 @@ func genHandlerImports(group spec.Group, route spec.Route, parentPkg string) str
 	if len(route.RequestTypeName()) > 0 {
 		imports = append(imports, fmt.Sprintf("\"%s\"\n", pathx.JoinPackages(parentPkg, typesDir)))
 	}
-	imports = append(imports, fmt.Sprintf("\"%s/rest/httpx\"", vars.ProjectOpenSourceURL))
 
 	return strings.Join(imports, "\n\t")
 }
diff --git a/tools/goctl/api/gogen/handler.tpl b/tools/goctl/api/gogen/handler.tpl
index 033dc22e..3a9f9bac 100644
--- a/tools/goctl/api/gogen/handler.tpl
+++ b/tools/goctl/api/gogen/handler.tpl
@@ -3,8 +3,8 @@ package {{.PkgName}}
 import (
 	"net/http"
 
-	"github.com/zeromicro/go-zero/rest/httpx"
 	{{.ImportPackages}}
+	{{.ImportHttpxPackage}}
 )
 
 func {{.HandlerName}}(svcCtx *svc.ServiceContext) http.HandlerFunc {


Test patch:
diff --git a/gateway/internal/descriptorsource_test.go b/gateway/internal/descriptorsource_test.go
new file mode 100644
index 00000000..0d6e8e37
--- /dev/null
+++ b/gateway/internal/descriptorsource_test.go
@@ -0,0 +1,78 @@
+package internal
+
+import (
+	"encoding/base64"
+	"io/ioutil"
+	"net/http"
+	"os"
+	"testing"
+
+	"github.com/fullstorydev/grpcurl"
+	"github.com/stretchr/testify/assert"
+	"github.com/zeromicro/go-zero/core/hash"
+)
+
+const (
+	b64pb                = `CpgBCgtoZWxsby5wcm90bxIFaGVsbG8iHQoHUmVxdWVzdBISCgRwaW5nGAEgASgJUgRwaW5nIh4KCFJlc3BvbnNlEhIKBHBvbmcYASABKAlSBHBvbmcyMAoFSGVsbG8SJwoEUGluZxIOLmhlbGxvLlJlcXVlc3QaDy5oZWxsby5SZXNwb25zZUIJWgcuL2hlbGxvYgZwcm90bzM=`
+	b64pbWithAnnotations = `Cs4EChVnb29nbGUvYXBpL2h0dHAucHJvdG8SCmdvb2dsZS5hcGkieQoESHR0cBIqCgVydWxlcxgBIAMoCzIULmdvb2dsZS5hcGkuSHR0cFJ1bGVSBXJ1bGVzEkUKH2Z1bGx5X2RlY29kZV9yZXNlcnZlZF9leHBhbnNpb24YAiABKAhSHGZ1bGx5RGVjb2RlUmVzZXJ2ZWRFeHBhbnNpb24i2gIKCEh0dHBSdWxlEhoKCHNlbGVjdG9yGAEgASgJUghzZWxlY3RvchISCgNnZXQYAiABKAlIAFIDZ2V0EhIKA3B1dBgDIAEoCUgAUgNwdXQSFAoEcG9zdBgEIAEoCUgAUgRwb3N0EhgKBmRlbGV0ZRgFIAEoCUgAUgZkZWxldGUSFgoFcGF0Y2gYBiABKAlIAFIFcGF0Y2gSNwoGY3VzdG9tGAggASgLMh0uZ29vZ2xlLmFwaS5DdXN0b21IdHRwUGF0dGVybkgAUgZjdXN0b20SEgoEYm9keRgHIAEoCVIEYm9keRIjCg1yZXNwb25zZV9ib2R5GAwgASgJUgxyZXNwb25zZUJvZHkSRQoTYWRkaXRpb25hbF9iaW5kaW5ncxgLIAMoCzIULmdvb2dsZS5hcGkuSHR0cFJ1bGVSEmFkZGl0aW9uYWxCaW5kaW5nc0IJCgdwYXR0ZXJuIjsKEUN1c3RvbUh0dHBQYXR0ZXJuEhIKBGtpbmQYASABKAlSBGtpbmQSEgoEcGF0aBgCIAEoCVIEcGF0aEIMWgpnb29nbGUvYXBpYgZwcm90bzMK8zsKIGdvb2dsZS9wcm90b2J1Zi9kZXNjcmlwdG9yLnByb3RvEg9nb29nbGUucHJvdG9idWYiTQoRRmlsZURlc2NyaXB0b3JTZXQSOAoEZmlsZRgBIAMoCzIkLmdvb2dsZS5wcm90b2J1Zi5GaWxlRGVzY3JpcHRvclByb3RvUgRmaWxlIuQEChNGaWxlRGVzY3JpcHRvclByb3RvEhIKBG5hbWUYASABKAlSBG5hbWUSGAoHcGFja2FnZRgCIAEoCVIHcGFja2FnZRIeCgpkZXBlbmRlbmN5GAMgAygJUgpkZXBlbmRlbmN5EisKEXB1YmxpY19kZXBlbmRlbmN5GAogAygFUhBwdWJsaWNEZXBlbmRlbmN5EicKD3dlYWtfZGVwZW5kZW5jeRgLIAMoBVIOd2Vha0RlcGVuZGVuY3kSQwoMbWVzc2FnZV90eXBlGAQgAygLMiAuZ29vZ2xlLnByb3RvYnVmLkRlc2NyaXB0b3JQcm90b1ILbWVzc2FnZVR5cGUSQQoJZW51bV90eXBlGAUgAygLMiQuZ29vZ2xlLnByb3RvYnVmLkVudW1EZXNjcmlwdG9yUHJvdG9SCGVudW1UeXBlEkEKB3NlcnZpY2UYBiADKAsyJy5nb29nbGUucHJvdG9idWYuU2VydmljZURlc2NyaXB0b3JQcm90b1IHc2VydmljZRJDCglleHRlbnNpb24YByADKAsyJS5nb29nbGUucHJvdG9idWYuRmllbGREZXNjcmlwdG9yUHJvdG9SCWV4dGVuc2lvbhI2CgdvcHRpb25zGAggASgLMhwuZ29vZ2xlLnByb3RvYnVmLkZpbGVPcHRpb25zUgdvcHRpb25zEkkKEHNvdXJjZV9jb2RlX2luZm8YCSABKAsyHy5nb29nbGUucHJvdG9idWYuU291cmNlQ29kZUluZm9SDnNvdXJjZUNvZGVJbmZvEhYKBnN5bnRheBgMIAEoCVIGc3ludGF4IrkGCg9EZXNjcmlwdG9yUHJvdG8SEgoEbmFtZRgBIAEoCVIEbmFtZRI7CgVmaWVsZBgCIAMoCzIlLmdvb2dsZS5wcm90b2J1Zi5GaWVsZERlc2NyaXB0b3JQcm90b1IFZmllbGQSQwoJZXh0ZW5zaW9uGAYgAygLMiUuZ29vZ2xlLnByb3RvYnVmLkZpZWxkRGVzY3JpcHRvclByb3RvUglleHRlbnNpb24SQQoLbmVzdGVkX3R5cGUYAyADKAsyIC5nb29nbGUucHJvdG9idWYuRGVzY3JpcHRvclByb3RvUgpuZXN0ZWRUeXBlEkEKCWVudW1fdHlwZRgEIAMoCzIkLmdvb2dsZS5wcm90b2J1Zi5FbnVtRGVzY3JpcHRvclByb3RvUghlbnVtVHlwZRJYCg9leHRlbnNpb25fcmFuZ2UYBSADKAsyLy5nb29nbGUucHJvdG9idWYuRGVzY3JpcHRvclByb3RvLkV4dGVuc2lvblJhbmdlUg5leHRlbnNpb25SYW5nZRJECgpvbmVvZl9kZWNsGAggAygLMiUuZ29vZ2xlLnByb3RvYnVmLk9uZW9mRGVzY3JpcHRvclByb3RvUglvbmVvZkRlY2wSOQoHb3B0aW9ucxgHIAEoCzIfLmdvb2dsZS5wcm90b2J1Zi5NZXNzYWdlT3B0aW9uc1IHb3B0aW9ucxJVCg5yZXNlcnZlZF9yYW5nZRgJIAMoCzIuLmdvb2dsZS5wcm90b2J1Zi5EZXNjcmlwdG9yUHJvdG8uUmVzZXJ2ZWRSYW5nZVINcmVzZXJ2ZWRSYW5nZRIjCg1yZXNlcnZlZF9uYW1lGAogAygJUgxyZXNlcnZlZE5hbWUaegoORXh0ZW5zaW9uUmFuZ2USFAoFc3RhcnQYASABKAVSBXN0YXJ0EhAKA2VuZBgCIAEoBVIDZW5kEkAKB29wdGlvbnMYAyABKAsyJi5nb29nbGUucHJvdG9idWYuRXh0ZW5zaW9uUmFuZ2VPcHRpb25zUgdvcHRpb25zGjcKDVJlc2VydmVkUmFuZ2USFAoFc3RhcnQYASABKAVSBXN0YXJ0EhAKA2VuZBgCIAEoBVIDZW5kInwKFUV4dGVuc2lvblJhbmdlT3B0aW9ucxJYChR1bmludGVycHJldGVkX29wdGlvbhjnByADKAsyJC5nb29nbGUucHJvdG9idWYuVW5pbnRlcnByZXRlZE9wdGlvblITdW5pbnRlcnByZXRlZE9wdGlvbioJCOgHEICAgIACIsEGChRGaWVsZERlc2NyaXB0b3JQcm90bxISCgRuYW1lGAEgASgJUgRuYW1lEhYKBm51bWJlchgDIAEoBVIGbnVtYmVyEkEKBWxhYmVsGAQgASgOMisuZ29vZ2xlLnByb3RvYnVmLkZpZWxkRGVzY3JpcHRvclByb3RvLkxhYmVsUgVsYWJlbBI+CgR0eXBlGAUgASgOMiouZ29vZ2xlLnByb3RvYnVmLkZpZWxkRGVzY3JpcHRvclByb3RvLlR5cGVSBHR5cGUSGwoJdHlwZV9uYW1lGAYgASgJUgh0eXBlTmFtZRIaCghleHRlbmRlZRgCIAEoCVIIZXh0ZW5kZWUSIwoNZGVmYXVsdF92YWx1ZRgHIAEoCVIMZGVmYXVsdFZhbHVlEh8KC29uZW9mX2luZGV4GAkgASgFUgpvbmVvZkluZGV4EhsKCWpzb25fbmFtZRgKIAEoCVIIanNvbk5hbWUSNwoHb3B0aW9ucxgIIAEoCzIdLmdvb2dsZS5wcm90b2J1Zi5GaWVsZE9wdGlvbnNSB29wdGlvbnMSJwoPcHJvdG8zX29wdGlvbmFsGBEgASgIUg5wcm90bzNPcHRpb25hbCK2AgoEVHlwZRIPCgtUWVBFX0RPVUJMRRABEg4KClRZUEVfRkxPQVQQAhIOCgpUWVBFX0lOVDY0EAMSDwoLVFlQRV9VSU5UNjQQBBIOCgpUWVBFX0lOVDMyEAUSEAoMVFlQRV9GSVhFRDY0EAYSEAoMVFlQRV9GSVhFRDMyEAcSDQoJVFlQRV9CT09MEAgSDwoLVFlQRV9TVFJJTkcQCRIOCgpUWVBFX0dST1VQEAoSEAoMVFlQRV9NRVNTQUdFEAsSDgoKVFlQRV9CWVRFUxAMEg8KC1RZUEVfVUlOVDMyEA0SDQoJVFlQRV9FTlVNEA4SEQoNVFlQRV9TRklYRUQzMhAPEhEKDVRZUEVfU0ZJWEVENjQQEBIPCgtUWVBFX1NJTlQzMhAREg8KC1RZUEVfU0lOVDY0EBIiQwoFTGFiZWwSEgoOTEFCRUxfT1BUSU9OQUwQARISCg5MQUJFTF9SRVFVSVJFRBACEhIKDkxBQkVMX1JFUEVBVEVEEAMiYwoUT25lb2ZEZXNjcmlwdG9yUHJvdG8SEgoEbmFtZRgBIAEoCVIEbmFtZRI3CgdvcHRpb25zGAIgASgLMh0uZ29vZ2xlLnByb3RvYnVmLk9uZW9mT3B0aW9uc1IHb3B0aW9ucyLjAgoTRW51bURlc2NyaXB0b3JQcm90bxISCgRuYW1lGAEgASgJUgRuYW1lEj8KBXZhbHVlGAIgAygLMikuZ29vZ2xlLnByb3RvYnVmLkVudW1WYWx1ZURlc2NyaXB0b3JQcm90b1IFdmFsdWUSNgoHb3B0aW9ucxgDIAEoCzIcLmdvb2dsZS5wcm90b2J1Zi5FbnVtT3B0aW9uc1IHb3B0aW9ucxJdCg5yZXNlcnZlZF9yYW5nZRgEIAMoCzI2Lmdvb2dsZS5wcm90b2J1Zi5FbnVtRGVzY3JpcHRvclByb3RvLkVudW1SZXNlcnZlZFJhbmdlUg1yZXNlcnZlZFJhbmdlEiMKDXJlc2VydmVkX25hbWUYBSADKAlSDHJlc2VydmVkTmFtZRo7ChFFbnVtUmVzZXJ2ZWRSYW5nZRIUCgVzdGFydBgBIAEoBVIFc3RhcnQSEAoDZW5kGAIgASgFUgNlbmQigwEKGEVudW1WYWx1ZURlc2NyaXB0b3JQcm90bxISCgRuYW1lGAEgASgJUgRuYW1lEhYKBm51bWJlchgCIAEoBVIGbnVtYmVyEjsKB29wdGlvbnMYAyABKAsyIS5nb29nbGUucHJvdG9idWYuRW51bVZhbHVlT3B0aW9uc1IHb3B0aW9ucyKnAQoWU2VydmljZURlc2NyaXB0b3JQcm90bxISCgRuYW1lGAEgASgJUgRuYW1lEj4KBm1ldGhvZBgCIAMoCzImLmdvb2dsZS5wcm90b2J1Zi5NZXRob2REZXNjcmlwdG9yUHJvdG9SBm1ldGhvZBI5CgdvcHRpb25zGAMgASgLMh8uZ29vZ2xlLnByb3RvYnVmLlNlcnZpY2VPcHRpb25zUgdvcHRpb25zIokCChVNZXRob2REZXNjcmlwdG9yUHJvdG8SEgoEbmFtZRgBIAEoCVIEbmFtZRIdCgppbnB1dF90eXBlGAIgASgJUglpbnB1dFR5cGUSHwoLb3V0cHV0X3R5cGUYAyABKAlSCm91dHB1dFR5cGUSOAoHb3B0aW9ucxgEIAEoCzIeLmdvb2dsZS5wcm90b2J1Zi5NZXRob2RPcHRpb25zUgdvcHRpb25zEjAKEGNsaWVudF9zdHJlYW1pbmcYBSABKAg6BWZhbHNlUg9jbGllbnRTdHJlYW1pbmcSMAoQc2VydmVyX3N0cmVhbWluZxgGIAEoCDoFZmFsc2VSD3NlcnZlclN0cmVhbWluZyKRCQoLRmlsZU9wdGlvbnMSIQoMamF2YV9wYWNrYWdlGAEgASgJUgtqYXZhUGFja2FnZRIwChRqYXZhX291dGVyX2NsYXNzbmFtZRgIIAEoCVISamF2YU91dGVyQ2xhc3NuYW1lEjUKE2phdmFfbXVsdGlwbGVfZmlsZXMYCiABKAg6BWZhbHNlUhFqYXZhTXVsdGlwbGVGaWxlcxJECh1qYXZhX2dlbmVyYXRlX2VxdWFsc19hbmRfaGFzaBgUIAEoCEICGAFSGWphdmFHZW5lcmF0ZUVxdWFsc0FuZEhhc2gSOgoWamF2YV9zdHJpbmdfY2hlY2tfdXRmOBgbIAEoCDoFZmFsc2VSE2phdmFTdHJpbmdDaGVja1V0ZjgSUwoMb3B0aW1pemVfZm9yGAkgASgOMikuZ29vZ2xlLnByb3RvYnVmLkZpbGVPcHRpb25zLk9wdGltaXplTW9kZToFU1BFRURSC29wdGltaXplRm9yEh0KCmdvX3BhY2thZ2UYCyABKAlSCWdvUGFja2FnZRI1ChNjY19nZW5lcmljX3NlcnZpY2VzGBAgASgIOgVmYWxzZVIRY2NHZW5lcmljU2VydmljZXMSOQoVamF2YV9nZW5lcmljX3NlcnZpY2VzGBEgASgIOgVmYWxzZVITamF2YUdlbmVyaWNTZXJ2aWNlcxI1ChNweV9nZW5lcmljX3NlcnZpY2VzGBIgASgIOgVmYWxzZVIRcHlHZW5lcmljU2VydmljZXMSNwoUcGhwX2dlbmVyaWNfc2VydmljZXMYKiABKAg6BWZhbHNlUhJwaHBHZW5lcmljU2VydmljZXMSJQoKZGVwcmVjYXRlZBgXIAEoCDoFZmFsc2VSCmRlcHJlY2F0ZWQSLgoQY2NfZW5hYmxlX2FyZW5hcxgfIAEoCDoEdHJ1ZVIOY2NFbmFibGVBcmVuYXMSKgoRb2JqY19jbGFzc19wcmVmaXgYJCABKAlSD29iamNDbGFzc1ByZWZpeBIpChBjc2hhcnBfbmFtZXNwYWNlGCUgASgJUg9jc2hhcnBOYW1lc3BhY2USIQoMc3dpZnRfcHJlZml4GCcgASgJUgtzd2lmdFByZWZpeBIoChBwaHBfY2xhc3NfcHJlZml4GCggASgJUg5waHBDbGFzc1ByZWZpeBIjCg1waHBfbmFtZXNwYWNlGCkgASgJUgxwaHBOYW1lc3BhY2USNAoWcGhwX21ldGFkYXRhX25hbWVzcGFjZRgsIAEoCVIUcGhwTWV0YWRhdGFOYW1lc3BhY2USIQoMcnVieV9wYWNrYWdlGC0gASgJUgtydWJ5UGFja2FnZRJYChR1bmludGVycHJldGVkX29wdGlvbhjnByADKAsyJC5nb29nbGUucHJvdG9idWYuVW5pbnRlcnByZXRlZE9wdGlvblITdW5pbnRlcnByZXRlZE9wdGlvbiI6CgxPcHRpbWl6ZU1vZGUSCQoFU1BFRUQQARINCglDT0RFX1NJWkUQAhIQCgxMSVRFX1JVTlRJTUUQAyoJCOgHEICAgIACSgQIJhAnIuMCCg5NZXNzYWdlT3B0aW9ucxI8ChdtZXNzYWdlX3NldF93aXJlX2Zvcm1hdBgBIAEoCDoFZmFsc2VSFG1lc3NhZ2VTZXRXaXJlRm9ybWF0EkwKH25vX3N0YW5kYXJkX2Rlc2NyaXB0b3JfYWNjZXNzb3IYAiABKAg6BWZhbHNlUhxub1N0YW5kYXJkRGVzY3JpcHRvckFjY2Vzc29yEiUKCmRlcHJlY2F0ZWQYAyABKAg6BWZhbHNlUgpkZXByZWNhdGVkEhsKCW1hcF9lbnRyeRgHIAEoCFIIbWFwRW50cnkSWAoUdW5pbnRlcnByZXRlZF9vcHRpb24Y5wcgAygLMiQuZ29vZ2xlLnByb3RvYnVmLlVuaW50ZXJwcmV0ZWRPcHRpb25SE3VuaW50ZXJwcmV0ZWRPcHRpb24qCQjoBxCAgICAAkoECAQQBUoECAUQBkoECAYQB0oECAgQCUoECAkQCiKSBAoMRmllbGRPcHRpb25zEkEKBWN0eXBlGAEgASgOMiMuZ29vZ2xlLnByb3RvYnVmLkZpZWxkT3B0aW9ucy5DVHlwZToGU1RSSU5HUgVjdHlwZRIWCgZwYWNrZWQYAiABKAhSBnBhY2tlZBJHCgZqc3R5cGUYBiABKA4yJC5nb29nbGUucHJvdG9idWYuRmllbGRPcHRpb25zLkpTVHlwZToJSlNfTk9STUFMUgZqc3R5cGUSGQoEbGF6eRgFIAEoCDoFZmFsc2VSBGxhenkSLgoPdW52ZXJpZmllZF9sYXp5GA8gASgIOgVmYWxzZVIOdW52ZXJpZmllZExhenkSJQoKZGVwcmVjYXRlZBgDIAEoCDoFZmFsc2VSCmRlcHJlY2F0ZWQSGQoEd2VhaxgKIAEoCDoFZmFsc2VSBHdlYWsSWAoUdW5pbnRlcnByZXRlZF9vcHRpb24Y5wcgAygLMiQuZ29vZ2xlLnByb3RvYnVmLlVuaW50ZXJwcmV0ZWRPcHRpb25SE3VuaW50ZXJwcmV0ZWRPcHRpb24iLwoFQ1R5cGUSCgoGU1RSSU5HEAASCAoEQ09SRBABEhAKDFNUUklOR19QSUVDRRACIjUKBkpTVHlwZRINCglKU19OT1JNQUwQABINCglKU19TVFJJTkcQARINCglKU19OVU1CRVIQAioJCOgHEICAgIACSgQIBBAFInMKDE9uZW9mT3B0aW9ucxJYChR1bmludGVycHJldGVkX29wdGlvbhjnByADKAsyJC5nb29nbGUucHJvdG9idWYuVW5pbnRlcnByZXRlZE9wdGlvblITdW5pbnRlcnByZXRlZE9wdGlvbioJCOgHEICAgIACIsABCgtFbnVtT3B0aW9ucxIfCgthbGxvd19hbGlhcxgCIAEoCFIKYWxsb3dBbGlhcxIlCgpkZXByZWNhdGVkGAMgASgIOgVmYWxzZVIKZGVwcmVjYXRlZBJYChR1bmludGVycHJldGVkX29wdGlvbhjnByADKAsyJC5nb29nbGUucHJvdG9idWYuVW5pbnRlcnByZXRlZE9wdGlvblITdW5pbnRlcnByZXRlZE9wdGlvbioJCOgHEICAgIACSgQIBRAGIp4BChBFbnVtVmFsdWVPcHRpb25zEiUKCmRlcHJlY2F0ZWQYASABKAg6BWZhbHNlUgpkZXByZWNhdGVkElgKFHVuaW50ZXJwcmV0ZWRfb3B0aW9uGOcHIAMoCzIkLmdvb2dsZS5wcm90b2J1Zi5VbmludGVycHJldGVkT3B0aW9uUhN1bmludGVycHJldGVkT3B0aW9uKgkI6AcQgICAgAIinAEKDlNlcnZpY2VPcHRpb25zEiUKCmRlcHJlY2F0ZWQYISABKAg6BWZhbHNlUgpkZXByZWNhdGVkElgKFHVuaW50ZXJwcmV0ZWRfb3B0aW9uGOcHIAMoCzIkLmdvb2dsZS5wcm90b2J1Zi5VbmludGVycHJldGVkT3B0aW9uUhN1bmludGVycHJldGVkT3B0aW9uKgkI6AcQgICAgAIi4AIKDU1ldGhvZE9wdGlvbnMSJQoKZGVwcmVjYXRlZBghIAEoCDoFZmFsc2VSCmRlcHJlY2F0ZWQScQoRaWRlbXBvdGVuY3lfbGV2ZWwYIiABKA4yLy5nb29nbGUucHJvdG9idWYuTWV0aG9kT3B0aW9ucy5JZGVtcG90ZW5jeUxldmVsOhNJREVNUE9URU5DWV9VTktOT1dOUhBpZGVtcG90ZW5jeUxldmVsElgKFHVuaW50ZXJwcmV0ZWRfb3B0aW9uGOcHIAMoCzIkLmdvb2dsZS5wcm90b2J1Zi5VbmludGVycHJldGVkT3B0aW9uUhN1bmludGVycHJldGVkT3B0aW9uIlAKEElkZW1wb3RlbmN5TGV2ZWwSFwoTSURFTVBPVEVOQ1lfVU5LTk9XThAAEhMKD05PX1NJREVfRUZGRUNUUxABEg4KCklERU1QT1RFTlQQAioJCOgHEICAgIACIpoDChNVbmludGVycHJldGVkT3B0aW9uEkEKBG5hbWUYAiADKAsyLS5nb29nbGUucHJvdG9idWYuVW5pbnRlcnByZXRlZE9wdGlvbi5OYW1lUGFydFIEbmFtZRIpChBpZGVudGlmaWVyX3ZhbHVlGAMgASgJUg9pZGVudGlmaWVyVmFsdWUSLAoScG9zaXRpdmVfaW50X3ZhbHVlGAQgASgEUhBwb3NpdGl2ZUludFZhbHVlEiwKEm5lZ2F0aXZlX2ludF92YWx1ZRgFIAEoA1IQbmVnYXRpdmVJbnRWYWx1ZRIhCgxkb3VibGVfdmFsdWUYBiABKAFSC2RvdWJsZVZhbHVlEiEKDHN0cmluZ192YWx1ZRgHIAEoDFILc3RyaW5nVmFsdWUSJwoPYWdncmVnYXRlX3ZhbHVlGAggASgJUg5hZ2dyZWdhdGVWYWx1ZRpKCghOYW1lUGFydBIbCgluYW1lX3BhcnQYASACKAlSCG5hbWVQYXJ0EiEKDGlzX2V4dGVuc2lvbhgCIAIoCFILaXNFeHRlbnNpb24ipwIKDlNvdXJjZUNvZGVJbmZvEkQKCGxvY2F0aW9uGAEgAygLMiguZ29vZ2xlLnByb3RvYnVmLlNvdXJjZUNvZGVJbmZvLkxvY2F0aW9uUghsb2NhdGlvbhrOAQoITG9jYXRpb24SFgoEcGF0aBgBIAMoBUICEAFSBHBhdGgSFgoEc3BhbhgCIAMoBUICEAFSBHNwYW4SKQoQbGVhZGluZ19jb21tZW50cxgDIAEoCVIPbGVhZGluZ0NvbW1lbnRzEisKEXRyYWlsaW5nX2NvbW1lbnRzGAQgASgJUhB0cmFpbGluZ0NvbW1lbnRzEjoKGWxlYWRpbmdfZGV0YWNoZWRfY29tbWVudHMYBiADKAlSF2xlYWRpbmdEZXRhY2hlZENvbW1lbnRzItEBChFHZW5lcmF0ZWRDb2RlSW5mbxJNCgphbm5vdGF0aW9uGAEgAygLMi0uZ29vZ2xlLnByb3RvYnVmLkdlbmVyYXRlZENvZGVJbmZvLkFubm90YXRpb25SCmFubm90YXRpb24abQoKQW5ub3RhdGlvbhIWCgRwYXRoGAEgAygFQgIQAVIEcGF0aBIfCgtzb3VyY2VfZmlsZRgCIAEoCVIKc291cmNlRmlsZRIUCgViZWdpbhgDIAEoBVIFYmVnaW4SEAoDZW5kGAQgASgFUgNlbmRCfgoTY29tLmdvb2dsZS5wcm90b2J1ZkIQRGVzY3JpcHRvclByb3Rvc0gBWi1nb29nbGUuZ29sYW5nLm9yZy9wcm90b2J1Zi90eXBlcy9kZXNjcmlwdG9ycGL4AQGiAgNHUEKqAhpHb29nbGUuUHJvdG9idWYuUmVmbGVjdGlvbgrYAQocZ29vZ2xlL2FwaS9hbm5vdGF0aW9ucy5wcm90bxIKZ29vZ2xlLmFwaRoVZ29vZ2xlL2FwaS9odHRwLnByb3RvGiBnb29nbGUvcHJvdG9idWYvZGVzY3JpcHRvci5wcm90bzpLCgRodHRwEh4uZ29vZ2xlLnByb3RvYnVmLk1ldGhvZE9wdGlvbnMYsMq8IiABKAsyFC5nb29nbGUuYXBpLkh0dHBSdWxlUgRodHRwQh5aHHByb3RvL3RoaXJkX3BhcnR5L2dvb2dsZS9hcGliBnByb3RvMwrcAwoHYS5wcm90bxIFaGVsbG8aHGdvb2dsZS9hcGkvYW5ub3RhdGlvbnMucHJvdG8iHQoHUmVxdWVzdBISCgRwaW5nGAEgASgJUgRwaW5nIh4KCFJlc3BvbnNlEhIKBHBvbmcYASABKAlSBHBvbmcy2QIKBUhlbGxvEkQKB1BpbmdHZXQSDi5oZWxsby5SZXF1ZXN0Gg8uaGVsbG8uUmVzcG9uc2UiGILT5JMCEhINL3YxL2dldC97Zm9vfToBKhJACghQaW5nUG9zdBIOLmhlbGxvLlJlcXVlc3QaDy5oZWxsby5SZXNwb25zZSITgtPkkwINIggvdjEvcG9zdDoBKhI+CgdQaW5nUHV0Eg4uaGVsbG8uUmVxdWVzdBoPLmhlbGxvLlJlc3BvbnNlIhKC0+STAgwaBy92MS9wdXQ6ASoSRAoKUGluZ0RlbGV0ZRIOLmhlbGxvLlJlcXVlc3QaDy5oZWxsby5SZXNwb25zZSIVgtPkkwIPKgovdjEvZGVsZXRlOgEqEkIKCVBpbmdQYXRjaBIOLmhlbGxvLlJlcXVlc3QaDy5oZWxsby5SZXNwb25zZSIUgtPkkwIOMgkvdjEvcGF0Y2g6ASpCCVoHLi9oZWxsb2IGcHJvdG8z`
+)
+
+func TestGetMethods(t *testing.T) {
+	tmpfile, err := ioutil.TempFile(os.TempDir(), hash.Md5Hex([]byte(b64pb)))
+	assert.Nil(t, err)
+	b, err := base64.StdEncoding.DecodeString(b64pb)
+	assert.Nil(t, err)
+	assert.Nil(t, ioutil.WriteFile(tmpfile.Name(), b, os.ModeTemporary))
+	defer os.Remove(tmpfile.Name())
+
+	source, err := grpcurl.DescriptorSourceFromProtoSets(tmpfile.Name())
+	assert.Nil(t, err)
+	methods, err := GetMethods(source)
+	assert.Nil(t, err)
+	assert.EqualValues(t, []Method{
+		{
+			RpcPath: "hello.Hello/Ping",
+		},
+	}, methods)
+}
+
+func TestGetMethodsWithAnnotations(t *testing.T) {
+	tmpfile, err := ioutil.TempFile(os.TempDir(), hash.Md5Hex([]byte(b64pb)))
+	assert.Nil(t, err)
+	b, err := base64.StdEncoding.DecodeString(b64pbWithAnnotations)
+	assert.Nil(t, err)
+	assert.Nil(t, ioutil.WriteFile(tmpfile.Name(), b, os.ModeTemporary))
+	defer os.Remove(tmpfile.Name())
+
+	source, err := grpcurl.DescriptorSourceFromProtoSets(tmpfile.Name())
+	assert.Nil(t, err)
+	methods, err := GetMethods(source)
+	assert.Nil(t, err)
+	assert.EqualValues(t, []Method{
+		{
+			HttpMethod: http.MethodGet,
+			HttpPath:   "/v1/get/:foo",
+			RpcPath:    "hello.Hello/PingGet",
+		},
+		{
+			HttpMethod: http.MethodPost,
+			HttpPath:   "/v1/post",
+			RpcPath:    "hello.Hello/PingPost",
+		},
+		{
+			HttpMethod: http.MethodPut,
+			HttpPath:   "/v1/put",
+			RpcPath:    "hello.Hello/PingPut",
+		},
+		{
+			HttpMethod: http.MethodDelete,
+			HttpPath:   "/v1/delete",
+			RpcPath:    "hello.Hello/PingDelete",
+		},
+		{
+			HttpMethod: http.MethodPatch,
+			HttpPath:   "/v1/patch",
+			RpcPath:    "hello.Hello/PingPatch",
+		},
+	}, methods)
+}
diff --git a/gateway/internal/headerprocessor_test.go b/gateway/internal/headerprocessor_test.go
new file mode 100644
index 00000000..c22f59f6
--- /dev/null
+++ b/gateway/internal/headerprocessor_test.go
@@ -0,0 +1,21 @@
+package internal
+
+import (
+	"net/http/httptest"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestBuildHeadersNoValue(t *testing.T) {
+	req := httptest.NewRequest("GET", "/", nil)
+	req.Header.Add("a", "b")
+	assert.Nil(t, ProcessHeaders(req.Header))
+}
+
+func TestBuildHeadersWithValues(t *testing.T) {
+	req := httptest.NewRequest("GET", "/", nil)
+	req.Header.Add("grpc-metadata-a", "b")
+	req.Header.Add("grpc-metadata-b", "b")
+	assert.ElementsMatch(t, []string{"gateway-A:b", "gateway-B:b"}, ProcessHeaders(req.Header))
+}
diff --git a/gateway/internal/requestparser_test.go b/gateway/internal/requestparser_test.go
new file mode 100644
index 00000000..ede871f0
--- /dev/null
+++ b/gateway/internal/requestparser_test.go
@@ -0,0 +1,55 @@
+package internal
+
+import (
+	"net/http/httptest"
+	"strings"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/zeromicro/go-zero/rest/pathvar"
+)
+
+func TestNewRequestParserNoVar(t *testing.T) {
+	req := httptest.NewRequest("GET", "/", nil)
+	parser, err := NewRequestParser(req, nil)
+	assert.Nil(t, err)
+	assert.NotNil(t, parser)
+}
+
+func TestNewRequestParserWithVars(t *testing.T) {
+	req := httptest.NewRequest("GET", "/", nil)
+	req = pathvar.WithVars(req, map[string]string{"a": "b"})
+	parser, err := NewRequestParser(req, nil)
+	assert.Nil(t, err)
+	assert.NotNil(t, parser)
+}
+
+func TestNewRequestParserNoVarWithBody(t *testing.T) {
+	req := httptest.NewRequest("GET", "/", strings.NewReader(`{"a": "b"}`))
+	parser, err := NewRequestParser(req, nil)
+	assert.Nil(t, err)
+	assert.NotNil(t, parser)
+}
+
+func TestNewRequestParserWithVarsWithBody(t *testing.T) {
+	req := httptest.NewRequest("GET", "/", strings.NewReader(`{"a": "b"}`))
+	req = pathvar.WithVars(req, map[string]string{"c": "d"})
+	parser, err := NewRequestParser(req, nil)
+	assert.Nil(t, err)
+	assert.NotNil(t, parser)
+}
+
+func TestNewRequestParserWithVarsWithWrongBody(t *testing.T) {
+	req := httptest.NewRequest("GET", "/", strings.NewReader(`{"a": "b"`))
+	req = pathvar.WithVars(req, map[string]string{"c": "d"})
+	parser, err := NewRequestParser(req, nil)
+	assert.NotNil(t, err)
+	assert.Nil(t, parser)
+}
+
+func TestNewRequestParserWithForm(t *testing.T) {
+	req := httptest.NewRequest("GET", "/val?a=b", nil)
+	parser, err := NewRequestParser(req, nil)
+	assert.Nil(t, err)
+	assert.NotNil(t, parser)
+}
diff --git a/gateway/internal/timeout_test.go b/gateway/internal/timeout_test.go
new file mode 100644
index 00000000..bf681e9d
--- /dev/null
+++ b/gateway/internal/timeout_test.go
@@ -0,0 +1,22 @@
+package internal
+
+import (
+	"net/http/httptest"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestGetTimeout(t *testing.T) {
+	req := httptest.NewRequest("GET", "/", nil)
+	req.Header.Set(grpcTimeoutHeader, "1s")
+	timeout := GetTimeout(req.Header, time.Second*5)
+	assert.Equal(t, time.Second, timeout)
+}
+
+func TestGetTimeoutDefault(t *testing.T) {
+	req := httptest.NewRequest("GET", "/", nil)
+	timeout := GetTimeout(req.Header, time.Second*5)
+	assert.Equal(t, time.Second*5, timeout)
+}