Repository: go-kratos/kratos
Pull request #1077 (2021-06-17 09:20:37)

add examples/errors


Patch:
diff --git a/api/metadata/metadata_http.pb.go b/api/metadata/metadata_http.pb.go
index 8485c35b..f30262c4 100644
--- a/api/metadata/metadata_http.pb.go
+++ b/api/metadata/metadata_http.pb.go
@@ -1,6 +1,6 @@
 // Code generated by protoc-gen-go-http. DO NOT EDIT.
 // versions:
-// protoc-gen-go-http v2.0.0-rc3
+// protoc-gen-go-http v2.0.0-rc5
 
 package metadata
 
@@ -84,8 +84,10 @@ func NewMetadataHTTPClient(client *http.Client) MetadataHTTPClient {
 
 func (c *MetadataHTTPClientImpl) GetServiceDesc(ctx context.Context, in *GetServiceDescRequest, opts ...http.CallOption) (*GetServiceDescReply, error) {
 	var out GetServiceDescReply
-	path := binding.EncodeURL("/services/{name}", in, true)
+	pattern := "/services/{name}"
+	path := binding.EncodeURL(pattern, in, true)
 	opts = append(opts, http.Operation("/kratos.api.Metadata/GetServiceDesc"))
+	opts = append(opts, http.PathTemplate(pattern))
 	err := c.cc.Invoke(ctx, "GET", path, nil, &out, opts...)
 	if err != nil {
 		return nil, err
@@ -95,8 +97,10 @@ func (c *MetadataHTTPClientImpl) GetServiceDesc(ctx context.Context, in *GetServ
 
 func (c *MetadataHTTPClientImpl) ListServices(ctx context.Context, in *ListServicesRequest, opts ...http.CallOption) (*ListServicesReply, error) {
 	var out ListServicesReply
-	path := binding.EncodeURL("/services", in, true)
+	pattern := "/services"
+	path := binding.EncodeURL(pattern, in, true)
 	opts = append(opts, http.Operation("/kratos.api.Metadata/ListServices"))
+	opts = append(opts, http.PathTemplate(pattern))
 	err := c.cc.Invoke(ctx, "GET", path, nil, &out, opts...)
 	if err != nil {
 		return nil, err
diff --git a/cmd/protoc-gen-go-http/main.go b/cmd/protoc-gen-go-http/main.go
index e8f7455b..36fb4dc5 100644
--- a/cmd/protoc-gen-go-http/main.go
+++ b/cmd/protoc-gen-go-http/main.go
@@ -8,7 +8,7 @@ import (
 	"google.golang.org/protobuf/types/pluginpb"
 )
 
-const version = "v2.0.0-rc3"
+const version = "v2.0.0-rc5"
 
 func main() {
 	showVersion := flag.Bool("version", false, "print the version and exit")
diff --git a/cmd/protoc-gen-go-http/template.go b/cmd/protoc-gen-go-http/template.go
index 4a313099..7bfd85f6 100644
--- a/cmd/protoc-gen-go-http/template.go
+++ b/cmd/protoc-gen-go-http/template.go
@@ -65,8 +65,10 @@ func New{{.ServiceType}}HTTPClient (client *http.Client) {{.ServiceType}}HTTPCli
 {{range .MethodSets}}
 func (c *{{$svrType}}HTTPClientImpl) {{.Name}}(ctx context.Context, in *{{.Request}}, opts ...http.CallOption) (*{{.Reply}}, error) {
 	var out {{.Reply}}
-	path := binding.EncodeURL("{{.Path}}", in, {{.IsQuery}})
+	pattern := "{{.Path}}"
+	path := binding.EncodeURL(pattern, in, {{.IsQuery}})
 	opts = append(opts, http.Operation("/{{$svrName}}/{{.Name}}"))
+	opts = append(opts, http.PathTemplate(pattern))
 	{{if .HasBody -}}
 	err := c.cc.Invoke(ctx, "{{.Method}}", path, in{{.Body}}, &out{{.ResponseBody}}, opts...)
 	{{else -}} 
diff --git a/examples/blog/api/blog/v1/blog_http.pb.go b/examples/blog/api/blog/v1/blog_http.pb.go
index 3fcd414d..bea209a5 100644
--- a/examples/blog/api/blog/v1/blog_http.pb.go
+++ b/examples/blog/api/blog/v1/blog_http.pb.go
@@ -1,6 +1,6 @@
 // Code generated by protoc-gen-go-http. DO NOT EDIT.
 // versions:
-// protoc-gen-go-http v2.0.0-rc3
+// protoc-gen-go-http v2.0.0-rc5
 
 package v1
 
@@ -156,8 +156,10 @@ func NewBlogServiceHTTPClient(client *http.Client) BlogServiceHTTPClient {
 
 func (c *BlogServiceHTTPClientImpl) CreateArticle(ctx context.Context, in *CreateArticleRequest, opts ...http.CallOption) (*CreateArticleReply, error) {
 	var out CreateArticleReply
-	path := binding.EncodeURL("/v1/article/", in, false)
+	pattern := "/v1/article/"
+	path := binding.EncodeURL(pattern, in, false)
 	opts = append(opts, http.Operation("/blog.api.v1.BlogService/CreateArticle"))
+	opts = append(opts, http.PathTemplate(pattern))
 	err := c.cc.Invoke(ctx, "POST", path, in, &out, opts...)
 	if err != nil {
 		return nil, err
@@ -167,8 +169,10 @@ func (c *BlogServiceHTTPClientImpl) CreateArticle(ctx context.Context, in *Creat
 
 func (c *BlogServiceHTTPClientImpl) DeleteArticle(ctx context.Context, in *DeleteArticleRequest, opts ...http.CallOption) (*DeleteArticleReply, error) {
 	var out DeleteArticleReply
-	path := binding.EncodeURL("/v1/article/{id}", in, false)
+	pattern := "/v1/article/{id}"
+	path := binding.EncodeURL(pattern, in, false)
 	opts = append(opts, http.Operation("/blog.api.v1.BlogService/DeleteArticle"))
+	opts = append(opts, http.PathTemplate(pattern))
 	err := c.cc.Invoke(ctx, "DELETE", path, nil, &out, opts...)
 	if err != nil {
 		return nil, err
@@ -178,8 +182,10 @@ func (c *BlogServiceHTTPClientImpl) DeleteArticle(ctx context.Context, in *Delet
 
 func (c *BlogServiceHTTPClientImpl) GetArticle(ctx context.Context, in *GetArticleRequest, opts ...http.CallOption) (*GetArticleReply, error) {
 	var out GetArticleReply
-	path := binding.EncodeURL("/v1/article/{id}", in, true)
+	pattern := "/v1/article/{id}"
+	path := binding.EncodeURL(pattern, in, true)
 	opts = append(opts, http.Operation("/blog.api.v1.BlogService/GetArticle"))
+	opts = append(opts, http.PathTemplate(pattern))
 	err := c.cc.Invoke(ctx, "GET", path, nil, &out, opts...)
 	if err != nil {
 		return nil, err
@@ -189,8 +195,10 @@ func (c *BlogServiceHTTPClientImpl) GetArticle(ctx context.Context, in *GetArtic
 
 func (c *BlogServiceHTTPClientImpl) ListArticle(ctx context.Context, in *ListArticleRequest, opts ...http.CallOption) (*ListArticleReply, error) {
 	var out ListArticleReply
-	path := binding.EncodeURL("/v1/article/", in, true)
+	pattern := "/v1/article/"
+	path := binding.EncodeURL(pattern, in, true)
 	opts = append(opts, http.Operation("/blog.api.v1.BlogService/ListArticle"))
+	opts = append(opts, http.PathTemplate(pattern))
 	err := c.cc.Invoke(ctx, "GET", path, nil, &out, opts...)
 	if err != nil {
 		return nil, err
@@ -200,8 +208,10 @@ func (c *BlogServiceHTTPClientImpl) ListArticle(ctx context.Context, in *ListArt
 
 func (c *BlogServiceHTTPClientImpl) UpdateArticle(ctx context.Context, in *UpdateArticleRequest, opts ...http.CallOption) (*UpdateArticleReply, error) {
 	var out UpdateArticleReply
-	path := binding.EncodeURL("/v1/article/{id}", in, false)
+	pattern := "/v1/article/{id}"
+	path := binding.EncodeURL(pattern, in, false)
 	opts = append(opts, http.Operation("/blog.api.v1.BlogService/UpdateArticle"))
+	opts = append(opts, http.PathTemplate(pattern))
 	err := c.cc.Invoke(ctx, "PUT", path, in, &out, opts...)
 	if err != nil {
 		return nil, err
diff --git a/examples/errors/api/error_reason.pb.go b/examples/errors/api/error_reason.pb.go
new file mode 100644
index 00000000..1f850e47
--- /dev/null
+++ b/examples/errors/api/error_reason.pb.go
@@ -0,0 +1,136 @@
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// 	protoc-gen-go v1.26.0
+// 	protoc        v3.17.1
+// source: error_reason.proto
+
+package api
+
+import (
+	_ "github.com/go-kratos/kratos/v2/errors"
+	protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+	protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+	reflect "reflect"
+	sync "sync"
+)
+
+const (
+	// Verify that this generated code is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+	// Verify that runtime/protoimpl is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+type ErrorReason int32
+
+const (
+	ErrorReason_USER_NOT_FOUND  ErrorReason = 0
+	ErrorReason_CONTENT_MISSING ErrorReason = 1
+)
+
+// Enum value maps for ErrorReason.
+var (
+	ErrorReason_name = map[int32]string{
+		0: "USER_NOT_FOUND",
+		1: "CONTENT_MISSING",
+	}
+	ErrorReason_value = map[string]int32{
+		"USER_NOT_FOUND":  0,
+		"CONTENT_MISSING": 1,
+	}
+)
+
+func (x ErrorReason) Enum() *ErrorReason {
+	p := new(ErrorReason)
+	*p = x
+	return p
+}
+
+func (x ErrorReason) String() string {
+	return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
+}
+
+func (ErrorReason) Descriptor() protoreflect.EnumDescriptor {
+	return file_error_reason_proto_enumTypes[0].Descriptor()
+}
+
+func (ErrorReason) Type() protoreflect.EnumType {
+	return &file_error_reason_proto_enumTypes[0]
+}
+
+func (x ErrorReason) Number() protoreflect.EnumNumber {
+	return protoreflect.EnumNumber(x)
+}
+
+// Deprecated: Use ErrorReason.Descriptor instead.
+func (ErrorReason) EnumDescriptor() ([]byte, []int) {
+	return file_error_reason_proto_rawDescGZIP(), []int{0}
+}
+
+var File_error_reason_proto protoreflect.FileDescriptor
+
+var file_error_reason_proto_rawDesc = []byte{
+	0x0a, 0x12, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x2e, 0x70,
+	0x72, 0x6f, 0x74, 0x6f, 0x12, 0x06, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x1a, 0x0c, 0x65, 0x72,
+	0x72, 0x6f, 0x72, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2a, 0x48, 0x0a, 0x0b, 0x45, 0x72,
+	0x72, 0x6f, 0x72, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x12, 0x18, 0x0a, 0x0e, 0x55, 0x53, 0x45,
+	0x52, 0x5f, 0x4e, 0x4f, 0x54, 0x5f, 0x46, 0x4f, 0x55, 0x4e, 0x44, 0x10, 0x00, 0x1a, 0x04, 0xa8,
+	0x45, 0x94, 0x03, 0x12, 0x19, 0x0a, 0x0f, 0x43, 0x4f, 0x4e, 0x54, 0x45, 0x4e, 0x54, 0x5f, 0x4d,
+	0x49, 0x53, 0x53, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x1a, 0x04, 0xa8, 0x45, 0x90, 0x03, 0x1a, 0x04,
+	0xa0, 0x45, 0xf4, 0x03, 0x42, 0x57, 0x0a, 0x0e, 0x62, 0x6c, 0x6f, 0x67, 0x2e, 0x76, 0x31, 0x2e,
+	0x65, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x50, 0x01, 0x5a, 0x33, 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, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x73, 0x2f, 0x62,
+	0x6c, 0x6f, 0x67, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x3b, 0x76, 0x31, 0xa2, 0x02, 0x0d,
+	0x41, 0x50, 0x49, 0x42, 0x6c, 0x6f, 0x67, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x62, 0x06, 0x70,
+	0x72, 0x6f, 0x74, 0x6f, 0x33,
+}
+
+var (
+	file_error_reason_proto_rawDescOnce sync.Once
+	file_error_reason_proto_rawDescData = file_error_reason_proto_rawDesc
+)
+
+func file_error_reason_proto_rawDescGZIP() []byte {
+	file_error_reason_proto_rawDescOnce.Do(func() {
+		file_error_reason_proto_rawDescData = protoimpl.X.CompressGZIP(file_error_reason_proto_rawDescData)
+	})
+	return file_error_reason_proto_rawDescData
+}
+
+var file_error_reason_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
+var file_error_reason_proto_goTypes = []interface{}{
+	(ErrorReason)(0), // 0: errors.ErrorReason
+}
+var file_error_reason_proto_depIdxs = []int32{
+	0, // [0:0] is the sub-list for method output_type
+	0, // [0:0] 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_error_reason_proto_init() }
+func file_error_reason_proto_init() {
+	if File_error_reason_proto != nil {
+		return
+	}
+	type x struct{}
+	out := protoimpl.TypeBuilder{
+		File: protoimpl.DescBuilder{
+			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+			RawDescriptor: file_error_reason_proto_rawDesc,
+			NumEnums:      1,
+			NumMessages:   0,
+			NumExtensions: 0,
+			NumServices:   0,
+		},
+		GoTypes:           file_error_reason_proto_goTypes,
+		DependencyIndexes: file_error_reason_proto_depIdxs,
+		EnumInfos:         file_error_reason_proto_enumTypes,
+	}.Build()
+	File_error_reason_proto = out.File
+	file_error_reason_proto_rawDesc = nil
+	file_error_reason_proto_goTypes = nil
+	file_error_reason_proto_depIdxs = nil
+}
diff --git a/examples/errors/api/error_reason.proto b/examples/errors/api/error_reason.proto
new file mode 100644
index 00000000..2b1a2c16
--- /dev/null
+++ b/examples/errors/api/error_reason.proto
@@ -0,0 +1,17 @@
+syntax = "proto3";
+
+package errors;
+import "errors.proto";
+
+// 多语言特定包名，用于源代码引用
+option go_package = "github.com/go-kratos/kratos/examples/blog/api/v1;v1";
+option java_multiple_files = true;
+option java_package = "blog.v1.errors";
+option objc_class_prefix = "APIBlogErrors";
+
+enum ErrorReason {
+  option (errors.default_code) = 500;
+
+  USER_NOT_FOUND = 0 [(errors.code) = 404];
+  CONTENT_MISSING = 1 [(errors.code) = 400];;
+}
diff --git a/examples/errors/api/error_reason_errors.pb.go b/examples/errors/api/error_reason_errors.pb.go
new file mode 100644
index 00000000..19190580
--- /dev/null
+++ b/examples/errors/api/error_reason_errors.pb.go
@@ -0,0 +1,30 @@
+// Code generated by protoc-gen-go-errors. DO NOT EDIT.
+
+package api
+
+import (
+	fmt "fmt"
+	errors "github.com/go-kratos/kratos/v2/errors"
+)
+
+// This is a compile-time assertion to ensure that this generated file
+// is compatible with the kratos package it is being compiled against.
+const _ = errors.SupportPackageIsVersion1
+
+func IsUserNotFound(err error) bool {
+	e := errors.FromError(err)
+	return e.Reason == ErrorReason_USER_NOT_FOUND.String() && e.Code == 404
+}
+
+func ErrorUserNotFound(format string, args ...interface{}) *errors.Error {
+	return errors.New(404, ErrorReason_USER_NOT_FOUND.String(), fmt.Sprintf(format, args...))
+}
+
+func IsContentMissing(err error) bool {
+	e := errors.FromError(err)
+	return e.Reason == ErrorReason_CONTENT_MISSING.String() && e.Code == 400
+}
+
+func ErrorContentMissing(format string, args ...interface{}) *errors.Error {
+	return errors.New(400, ErrorReason_CONTENT_MISSING.String(), fmt.Sprintf(format, args...))
+}
diff --git a/examples/errors/api/errors.proto b/examples/errors/api/errors.proto
new file mode 100644
index 00000000..3603729a
--- /dev/null
+++ b/examples/errors/api/errors.proto
@@ -0,0 +1,18 @@
+syntax = "proto3";
+
+package errors;
+
+option go_package = "github.com/go-kratos/kratos/v2/errors;v1";
+option java_multiple_files = true;
+option java_package = "com.github.kratos.errors";
+option objc_class_prefix = "KratosErrors";
+
+import "google/protobuf/descriptor.proto";
+
+extend google.protobuf.EnumOptions {
+  int32 default_code = 1108;
+}
+
+extend google.protobuf.EnumValueOptions {
+  int32 code = 1109;
+}
\ No newline at end of file
diff --git a/examples/errors/client/main.go b/examples/errors/client/main.go
new file mode 100644
index 00000000..7a5d9cdc
--- /dev/null
+++ b/examples/errors/client/main.go
@@ -0,0 +1,62 @@
+package main
+
+import (
+	"context"
+	"github.com/go-kratos/kratos/examples/errors/api"
+	"github.com/go-kratos/kratos/v2/errors"
+	"log"
+
+	pb "github.com/go-kratos/kratos/examples/helloworld/helloworld"
+	transgrpc "github.com/go-kratos/kratos/v2/transport/grpc"
+	transhttp "github.com/go-kratos/kratos/v2/transport/http"
+)
+
+func main() {
+	callHTTP()
+	callGRPC()
+}
+
+func callHTTP() {
+	conn, err := transhttp.NewClient(
+		context.Background(),
+		transhttp.WithEndpoint("127.0.0.1:8000"),
+	)
+	if err != nil {
+		panic(err)
+	}
+	client := pb.NewGreeterHTTPClient(conn)
+	reply, err := client.SayHello(context.Background(), &pb.HelloRequest{Name: "empty"})
+	if err != nil {
+		if errors.Code(err) == 500 {
+			log.Println(err)
+		}
+		if api.IsUserNotFound(err) {
+			log.Println("[http] USER_NOT_FOUND_ERROR", err)
+		}
+	} else {
+		log.Printf("[http] SayHello %s\n", reply.Message)
+	}
+}
+
+func callGRPC() {
+	conn, err := transgrpc.DialInsecure(
+		context.Background(),
+		transgrpc.WithEndpoint("127.0.0.1:9000"),
+	)
+	if err != nil {
+		panic(err)
+	}
+	client := pb.NewGreeterClient(conn)
+	reply, err := client.SayHello(context.Background(), &pb.HelloRequest{Name: "kratos"})
+	if err != nil {
+		e := errors.FromError(err)
+		if e.Reason == "USER_NAME_EMPTY" && e.Code == 500 {
+			log.Println("[grpc] USER_NAME_EMPTY", err)
+		}
+		if api.IsUserNotFound(err) {
+			log.Println("[grpc] USER_NOT_FOUND_ERROR", err)
+		}
+	} else {
+		log.Printf("[grpc] SayHello %+v\n", reply)
+	}
+}
diff --git a/examples/errors/server/main.go b/examples/errors/server/main.go
new file mode 100644
index 00000000..7319554e
--- /dev/null
+++ b/examples/errors/server/main.go
@@ -0,0 +1,65 @@
+package main
+
+import (
+	"context"
+	"fmt"
+	"github.com/go-kratos/kratos/v2/errors"
+	"log"
+
+	"github.com/go-kratos/kratos/examples/errors/api"
+	"github.com/go-kratos/kratos/examples/helloworld/helloworld"
+	"github.com/go-kratos/kratos/v2"
+	"github.com/go-kratos/kratos/v2/transport/grpc"
+	"github.com/go-kratos/kratos/v2/transport/http"
+)
+
+// go build -ldflags "-X main.Version=x.y.z"
+var (
+	// Name is the name of the compiled software.
+	Name = "errors"
+	// Version is the version of the compiled software.
+	Version = "v1.0.0"
+)
+
+// server is used to implement helloworld.GreeterServer.
+type server struct {
+	helloworld.UnimplementedGreeterServer
+}
+
+// SayHello implements helloworld.GreeterServer
+func (s *server) SayHello(ctx context.Context, in *helloworld.HelloRequest) (*helloworld.HelloReply, error) {
+	fmt.Println(in.Name)
+	if in.Name == "empty" {
+		// Respond to errors through errors.New().
+		return nil, errors.New(500, "USER_NAME_EMPTY", "user name is empty")
+	}
+	if in.Name == "kratos" {
+		// Respond to errors with proto generated code.
+		return nil, api.ErrorUserNotFound("user %s not found", "kratos")
+	}
+	return &helloworld.HelloReply{Message: fmt.Sprintf("Hello %+v", in.Name)}, nil
+}
+
+func main() {
+	s := &server{}
+	grpcSrv := grpc.NewServer(
+		grpc.Address(":9000"),
+	)
+	httpSrv := http.NewServer(
+		http.Address(":8000"),
+	)
+	helloworld.RegisterGreeterServer(grpcSrv, s)
+	helloworld.RegisterGreeterHTTPServer(httpSrv, s)
+
+	app := kratos.New(
+		kratos.Name(Name),
+		kratos.Server(
+			httpSrv,
+			grpcSrv,
+		),
+	)
+
+	if err := app.Run(); err != nil {
+		log.Fatal(err)
+	}
+}
diff --git a/examples/helloworld/helloworld/helloworld_http.pb.go b/examples/helloworld/helloworld/helloworld_http.pb.go
index 55517338..81f78683 100644
--- a/examples/helloworld/helloworld/helloworld_http.pb.go
+++ b/examples/helloworld/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.0.0-rc3
+// protoc-gen-go-http v2.0.0-rc5
 
 package helloworld
 
@@ -62,8 +62,10 @@ func NewGreeterHTTPClient(client *http.Client) GreeterHTTPClient {
 
 func (c *GreeterHTTPClientImpl) SayHello(ctx context.Context, in *HelloRequest, opts ...http.CallOption) (*HelloReply, error) {
 	var out HelloReply
-	path := binding.EncodeURL("/helloworld/{name}", in, true)
+	pattern := "/helloworld/{name}"
+	path := binding.EncodeURL(pattern, in, true)
 	opts = append(opts, http.Operation("/helloworld.Greeter/SayHello"))
+	opts = append(opts, http.PathTemplate(pattern))
 	err := c.cc.Invoke(ctx, "GET", path, nil, &out, opts...)
 	if err != nil {
 		return nil, err
diff --git a/examples/traces/api/message/message_http.pb.go b/examples/traces/api/message/message_http.pb.go
index bfa19083..60a4e410 100644
--- a/examples/traces/api/message/message_http.pb.go
+++ b/examples/traces/api/message/message_http.pb.go
@@ -1,6 +1,6 @@
 // Code generated by protoc-gen-go-http. DO NOT EDIT.
 // versions:
-// protoc-gen-go-http v2.0.0-rc3
+// protoc-gen-go-http v2.0.0-rc5
 
 package v1
 
@@ -62,8 +62,10 @@ func NewMessageServiceHTTPClient(client *http.Client) MessageServiceHTTPClient {
 
 func (c *MessageServiceHTTPClientImpl) GetUserMessage(ctx context.Context, in *GetUserMessageRequest, opts ...http.CallOption) (*GetUserMessageReply, error) {
 	var out GetUserMessageReply
-	path := binding.EncodeURL("/v1/message/user/{id}/{count}", in, true)
+	pattern := "/v1/message/user/{id}/{count}"
+	path := binding.EncodeURL(pattern, in, true)
 	opts = append(opts, http.Operation("/api.message.v1.MessageService/GetUserMessage"))
+	opts = append(opts, http.PathTemplate(pattern))
 	err := c.cc.Invoke(ctx, "GET", path, in, &out, opts...)
 	if err != nil {
 		return nil, err
diff --git a/examples/traces/api/user/user_http.pb.go b/examples/traces/api/user/user_http.pb.go
index 758b4560..d5c41663 100644
--- a/examples/traces/api/user/user_http.pb.go
+++ b/examples/traces/api/user/user_http.pb.go
@@ -1,6 +1,6 @@
 // Code generated by protoc-gen-go-http. DO NOT EDIT.
 // versions:
-// protoc-gen-go-http v2.0.0-rc3
+// protoc-gen-go-http v2.0.0-rc5
 
 package v1
 
@@ -62,8 +62,10 @@ func NewUserHTTPClient(client *http.Client) UserHTTPClient {
 
 func (c *UserHTTPClientImpl) GetMyMessages(ctx context.Context, in *GetMyMessagesRequest, opts ...http.CallOption) (*GetMyMessagesReply, error) {
 	var out GetMyMessagesReply
-	path := binding.EncodeURL("/v1/user/get/message/{count}", in, true)
+	pattern := "/v1/user/get/message/{count}"
+	path := binding.EncodeURL(pattern, in, true)
 	opts = append(opts, http.Operation("/api.user.v1.User/GetMyMessages"))
+	opts = append(opts, http.PathTemplate(pattern))
 	err := c.cc.Invoke(ctx, "GET", path, in, &out, opts...)
 	if err != nil {
 		return nil, err
diff --git a/transport/http/binding/encode.go b/transport/http/binding/encode.go
index 521be793..bc16e65e 100644
--- a/transport/http/binding/encode.go
+++ b/transport/http/binding/encode.go
@@ -19,16 +19,16 @@ import (
 )
 
 // EncodeURL encode proto message to url path.
-func EncodeURL(pathPattern string, msg proto.Message, needQuery bool) string {
+func EncodeURL(pathTemplate string, msg proto.Message, needQuery bool) string {
 	if msg == nil || (reflect.ValueOf(msg).Kind() == reflect.Ptr && reflect.ValueOf(msg).IsNil()) {
-		return pathPattern
+		return pathTemplate
 	}
 	reg := regexp.MustCompile(`/{[.\w]+}`)
 	if reg == nil {
-		return pathPattern
+		return pathTemplate
 	}
 	pathParams := make(map[string]struct{}, 0)
-	path := reg.ReplaceAllStringFunc(pathPattern, func(in string) string {
+	path := reg.ReplaceAllStringFunc(pathTemplate, func(in string) string {
 		if len(in) < 4 {
 			return in
 		}
diff --git a/transport/http/calloption.go b/transport/http/calloption.go
index cd2cd72c..db2a75db 100644
--- a/transport/http/calloption.go
+++ b/transport/http/calloption.go
@@ -13,8 +13,9 @@ type CallOption interface {
 }
 
 type callInfo struct {
-	contentType string
-	operation   string
+	contentType  string
+	operation    string
+	pathTemplate string
 }
 
 // EmptyCallOption does not alter the Call configuration.
@@ -45,8 +46,9 @@ func (o ContentTypeCallOption) before(c *callInfo) error {
 
 func defaultCallInfo(path string) callInfo {
 	return callInfo{
-		contentType: "application/json",
-		operation:   path,
+		contentType:  "application/json",
+		operation:    path,
+		pathTemplate: path,
 	}
 }
 
@@ -65,3 +67,19 @@ func (o OperationCallOption) before(c *callInfo) error {
 	c.operation = o.Operation
 	return nil
 }
+
+// PathTemplate is http path template
+func PathTemplate(pattern string) CallOption {
+	return PathTemplateCallOption{Pattern: pattern}
+}
+
+// PathTemplateCallOption is set path template for client call
+type PathTemplateCallOption struct {
+	EmptyCallOption
+	Pattern string
+}
+
+func (o PathTemplateCallOption) before(c *callInfo) error {
+	c.pathTemplate = o.Pattern
+	return nil
+}
diff --git a/transport/http/client.go b/transport/http/client.go
index a21557e9..e5aa07f4 100644
--- a/transport/http/client.go
+++ b/transport/http/client.go
@@ -197,10 +197,11 @@ func (client *Client) Invoke(ctx context.Context, method, path string, args inte
 		req.Header.Set("User-Agent", client.opts.userAgent)
 	}
 	ctx = transport.NewClientContext(ctx, &Transport{
-		endpoint:  client.opts.endpoint,
-		header:    headerCarrier(req.Header),
-		operation: c.operation,
-		request:   req,
+		endpoint:     client.opts.endpoint,
+		header:       headerCarrier(req.Header),
+		operation:    c.operation,
+		request:      req,
+		pathTemplate: c.pathTemplate,
 	})
 	return client.invoke(ctx, req, args, reply, c)
 }
diff --git a/transport/http/server.go b/transport/http/server.go
index 93f31e95..f8cc7b35 100644
--- a/transport/http/server.go
+++ b/transport/http/server.go
@@ -162,11 +162,17 @@ func (s *Server) filter() mux.MiddlewareFunc {
 				ctx, cancel = context.WithTimeout(ctx, s.timeout)
 				defer cancel()
 			}
+			pathTemplate := req.URL.Path
+			if route := mux.CurrentRoute(req); route != nil {
+				// /path/123 -> /path/{id}
+				pathTemplate, _ = route.GetPathTemplate()
+			}
 			tr := &Transport{
-				endpoint:  s.endpoint.String(),
-				operation: req.RequestURI,
-				header:    headerCarrier(req.Header),
-				request:   req,
+				endpoint:     s.endpoint.String(),
+				operation:    pathTemplate,
+				header:       headerCarrier(req.Header),
+				request:      req,
+				pathTemplate: pathTemplate,
 			}
 			if r := mux.CurrentRoute(req); r != nil {
 				if path, err := r.GetPathTemplate(); err == nil {
diff --git a/transport/http/transport.go b/transport/http/transport.go
index 743cd316..4080ee4b 100644
--- a/transport/http/transport.go
+++ b/transport/http/transport.go
@@ -13,10 +13,11 @@ var (
 
 // Transport is an HTTP transport.
 type Transport struct {
-	endpoint  string
-	operation string
-	header    headerCarrier
-	request   *http.Request
+	endpoint     string
+	operation    string
+	header       headerCarrier
+	request      *http.Request
+	pathTemplate string
 }
 
 // Kind returns the transport kind.
@@ -44,6 +45,11 @@ func (tr *Transport) Request() *http.Request {
 	return tr.request
 }
 
+// PathTemplate returns the http path template.
+func (tr *Transport) PathTemplate() string {
+	return tr.pathTemplate
+}
+
 // SetOperation sets the transport operation.
 func SetOperation(ctx context.Context, op string) {
 	if tr, ok := transport.FromServerContext(ctx); ok {


Test patch:
diff --git a/internal/testproto/echo_service_http.pb.go b/internal/testproto/echo_service_http.pb.go
index 819db2c8..c12452fd 100644
--- a/internal/testproto/echo_service_http.pb.go
+++ b/internal/testproto/echo_service_http.pb.go
@@ -1,6 +1,6 @@
 // Code generated by protoc-gen-go-http. DO NOT EDIT.
 // versions:
-// protoc-gen-go-http v2.0.0-rc3
+// protoc-gen-go-http v2.0.0-rc5
 
 package testproto
 
@@ -245,8 +245,10 @@ func NewEchoServiceHTTPClient(client *http.Client) EchoServiceHTTPClient {
 
 func (c *EchoServiceHTTPClientImpl) Echo(ctx context.Context, in *SimpleMessage, opts ...http.CallOption) (*SimpleMessage, error) {
 	var out SimpleMessage
-	path := binding.EncodeURL("/v1/example/echo/{id}", in, false)
+	pattern := "/v1/example/echo/{id}"
+	path := binding.EncodeURL(pattern, in, false)
 	opts = append(opts, http.Operation("/testproto.EchoService/Echo"))
+	opts = append(opts, http.PathTemplate(pattern))
 	err := c.cc.Invoke(ctx, "POST", path, nil, &out, opts...)
 	if err != nil {
 		return nil, err
@@ -256,8 +258,10 @@ func (c *EchoServiceHTTPClientImpl) Echo(ctx context.Context, in *SimpleMessage,
 
 func (c *EchoServiceHTTPClientImpl) EchoBody(ctx context.Context, in *SimpleMessage, opts ...http.CallOption) (*SimpleMessage, error) {
 	var out SimpleMessage
-	path := binding.EncodeURL("/v1/example/echo_body", in, false)
+	pattern := "/v1/example/echo_body"
+	path := binding.EncodeURL(pattern, in, false)
 	opts = append(opts, http.Operation("/testproto.EchoService/EchoBody"))
+	opts = append(opts, http.PathTemplate(pattern))
 	err := c.cc.Invoke(ctx, "POST", path, in, &out, opts...)
 	if err != nil {
 		return nil, err
@@ -267,8 +271,10 @@ func (c *EchoServiceHTTPClientImpl) EchoBody(ctx context.Context, in *SimpleMess
 
 func (c *EchoServiceHTTPClientImpl) EchoDelete(ctx context.Context, in *SimpleMessage, opts ...http.CallOption) (*SimpleMessage, error) {
 	var out SimpleMessage
-	path := binding.EncodeURL("/v1/example/echo_delete/{id}/{num}", in, false)
+	pattern := "/v1/example/echo_delete/{id}/{num}"
+	path := binding.EncodeURL(pattern, in, false)
 	opts = append(opts, http.Operation("/testproto.EchoService/EchoDelete"))
+	opts = append(opts, http.PathTemplate(pattern))
 	err := c.cc.Invoke(ctx, "DELETE", path, nil, &out, opts...)
 	if err != nil {
 		return nil, err
@@ -278,8 +284,10 @@ func (c *EchoServiceHTTPClientImpl) EchoDelete(ctx context.Context, in *SimpleMe
 
 func (c *EchoServiceHTTPClientImpl) EchoPatch(ctx context.Context, in *DynamicMessageUpdate, opts ...http.CallOption) (*DynamicMessageUpdate, error) {
 	var out DynamicMessageUpdate
-	path := binding.EncodeURL("/v1/example/echo_patch", in, false)
+	pattern := "/v1/example/echo_patch"
+	path := binding.EncodeURL(pattern, in, false)
 	opts = append(opts, http.Operation("/testproto.EchoService/EchoPatch"))
+	opts = append(opts, http.PathTemplate(pattern))
 	err := c.cc.Invoke(ctx, "PATCH", path, in.Body, &out, opts...)
 	if err != nil {
 		return nil, err
@@ -289,8 +297,10 @@ func (c *EchoServiceHTTPClientImpl) EchoPatch(ctx context.Context, in *DynamicMe
 
 func (c *EchoServiceHTTPClientImpl) EchoResponseBody(ctx context.Context, in *DynamicMessageUpdate, opts ...http.CallOption) (*DynamicMessageUpdate, error) {
 	var out DynamicMessageUpdate
-	path := binding.EncodeURL("/v1/example/echo_response_body", in, false)
+	pattern := "/v1/example/echo_response_body"
+	path := binding.EncodeURL(pattern, in, false)
 	opts = append(opts, http.Operation("/testproto.EchoService/EchoResponseBody"))
+	opts = append(opts, http.PathTemplate(pattern))
 	err := c.cc.Invoke(ctx, "POST", path, in, &out.Body, opts...)
 	if err != nil {
 		return nil, err