From d7097925a232aeaf7a04c277152c8a42dcdd0c47 Mon Sep 17 00:00:00 2001 From: raymonder jin Date: Mon, 5 Dec 2022 15:27:30 +0800 Subject: [PATCH 01/19] test: add tests for pkg/app/client/retry/retry.go (#419) --- pkg/app/client/retry/retry_test.go | 87 ++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 pkg/app/client/retry/retry_test.go diff --git a/pkg/app/client/retry/retry_test.go b/pkg/app/client/retry/retry_test.go new file mode 100644 index 000000000..c77c4f088 --- /dev/null +++ b/pkg/app/client/retry/retry_test.go @@ -0,0 +1,87 @@ +/* + * Copyright 2022 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package retry + +import ( + "math" + "testing" + "time" + + "github.com/cloudwego/hertz/pkg/common/test/assert" +) + +func TestApply(t *testing.T) { + delayPolicyFunc := func(attempts uint, err error, retryConfig *Config) time.Duration { + return time.Second + } + options := []Option{} + options = append(options, WithMaxAttemptTimes(100), WithInitDelay(time.Second), + WithMaxDelay(time.Second), WithDelayPolicy(delayPolicyFunc), WithMaxJitter(time.Second)) + + config := Config{} + config.Apply(options) + + assert.DeepEqual(t, uint(100), config.MaxAttemptTimes) + assert.DeepEqual(t, time.Second, config.Delay) + assert.DeepEqual(t, time.Second, config.MaxDelay) + assert.DeepEqual(t, time.Second, Delay(0, nil, &config)) + assert.DeepEqual(t, time.Second, config.MaxJitter) +} + +func TestPolicy(t *testing.T) { + dur := DefaultDelayPolicy(0, nil, nil) + assert.DeepEqual(t, 0*time.Millisecond, dur) + + config := Config{ + Delay: time.Second, + } + dur = FixedDelayPolicy(0, nil, &config) + assert.DeepEqual(t, time.Second, dur) + + dur = RandomDelayPolicy(0, nil, &config) + assert.DeepEqual(t, 0*time.Millisecond, dur) + config.MaxJitter = time.Second * 1 + dur = RandomDelayPolicy(0, nil, &config) + assert.NotEqual(t, time.Second*1, dur) + + dur = BackOffDelayPolicy(0, nil, &config) + assert.DeepEqual(t, time.Second*1, dur) + config.Delay = time.Duration(-1) + dur = BackOffDelayPolicy(0, nil, &config) + assert.DeepEqual(t, time.Second*0, dur) + config.Delay = time.Duration(1) + dur = BackOffDelayPolicy(63, nil, &config) + durExp := config.Delay << 62 + assert.DeepEqual(t, durExp, dur) + + dur = Delay(0, nil, &config) + assert.DeepEqual(t, 0*time.Millisecond, dur) + delayPolicyFunc := func(attempts uint, err error, retryConfig *Config) time.Duration { + return time.Second + } + config.DelayPolicy = delayPolicyFunc + config.MaxDelay = time.Second / 2 + dur = Delay(0, nil, &config) + assert.DeepEqual(t, config.MaxDelay, dur) + + delayPolicyFunc2 := func(attempts uint, err error, retryConfig *Config) time.Duration { + return time.Duration(math.MaxInt64) + } + delayFunc := CombineDelay(delayPolicyFunc2, delayPolicyFunc) + dur = delayFunc(0, nil, &config) + assert.DeepEqual(t, time.Duration(math.MaxInt64), dur) +} From 9b9ec92704e19626225f6fc10d69a62f1762dbda Mon Sep 17 00:00:00 2001 From: Xuran <37136584+Duslia@users.noreply.github.com> Date: Mon, 5 Dec 2022 15:32:13 +0800 Subject: [PATCH 02/19] fix: `doRequestFollowRedirectsBuffer` cannot get body in HTTP2 scenario (#421) --- pkg/app/client/client_test.go | 34 ++++++++++++++++++++++++++++++++++ pkg/protocol/client/client.go | 4 +++- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/pkg/app/client/client_test.go b/pkg/app/client/client_test.go index 8c86aff30..e325a0e14 100644 --- a/pkg/app/client/client_test.go +++ b/pkg/app/client/client_test.go @@ -68,6 +68,7 @@ import ( "github.com/cloudwego/hertz/pkg/app/client/retry" "github.com/cloudwego/hertz/pkg/common/config" errs "github.com/cloudwego/hertz/pkg/common/errors" + "github.com/cloudwego/hertz/pkg/common/test/assert" "github.com/cloudwego/hertz/pkg/network" "github.com/cloudwego/hertz/pkg/network/dialer" "github.com/cloudwego/hertz/pkg/network/netpoll" @@ -193,6 +194,39 @@ func TestClientGetWithBody(t *testing.T) { } } +func TestClientPostBodyStream(t *testing.T) { + t.Parallel() + + opt := config.NewOptions([]config.Option{}) + opt.Addr = "unix-test-10102" + opt.Network = "unix" + engine := route.NewEngine(opt) + engine.POST("/", func(c context.Context, ctx *app.RequestContext) { + body := ctx.Request.Body() + ctx.Write(body) //nolint:errcheck + }) + go engine.Run() + defer func() { + engine.Close() + }() + time.Sleep(time.Millisecond * 500) + + cStream, _ := NewClient(WithDialer(newMockDialerWithCustomFunc(opt.Network, opt.Addr, 1*time.Second, nil)), WithResponseBodyStream(true)) + args := &protocol.Args{} + // There is some data in databuf and others is in bodystream, so we need + // to let the data exceed the max bodysize of bodystream + v := "" + for i := 0; i < 10240; i++ { + v += "b" + } + args.Add("a", v) + _, body, err := cStream.Post(context.Background(), nil, "http://example.com", args) + if err != nil { + t.Fatal(err) + } + assert.DeepEqual(t, "a="+v, string(body)) +} + func TestClientURLAuth(t *testing.T) { t.Parallel() diff --git a/pkg/protocol/client/client.go b/pkg/protocol/client/client.go index 33d7c3906..e770547b7 100644 --- a/pkg/protocol/client/client.go +++ b/pkg/protocol/client/client.go @@ -220,7 +220,9 @@ func doRequestFollowRedirectsBuffer(ctx context.Context, req *protocol.Request, statusCode, _, err = DoRequestFollowRedirects(ctx, req, resp, url, defaultMaxRedirectsCount, c) - body = bodyBuf.B + // In HTTP2 scenario, client use stream mode to create a request and its body is in body stream. + // In HTTP1, only client recv body exceed max body size and client is in stream mode can trig it. + body = resp.Body() bodyBuf.B = oldBody protocol.ReleaseResponse(resp) From a470f4abaec9b073aa9688a628992f3be472e911 Mon Sep 17 00:00:00 2001 From: Wenju Gao Date: Wed, 7 Dec 2022 11:17:59 +0800 Subject: [PATCH 03/19] optimize(http1): return 413 status code if request body is too large (#430) --- pkg/app/server/hertz_test.go | 8 ++++++-- pkg/protocol/http1/server.go | 2 ++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/pkg/app/server/hertz_test.go b/pkg/app/server/hertz_test.go index 80ddc0c9b..f15db14e6 100644 --- a/pkg/app/server/hertz_test.go +++ b/pkg/app/server/hertz_test.go @@ -23,6 +23,7 @@ import ( "fmt" "html/template" "io" + "io/ioutil" "net" "net/http" "os" @@ -379,8 +380,11 @@ func TestNotEnoughBodySize(t *testing.T) { r.ParseForm() r.Form.Add("xxxxxx", "xxx") body := strings.NewReader(r.Form.Encode()) - resp, _ := http.Post("http://127.0.0.1:8889/test", "application/x-www-form-urlencoded", body) - assert.DeepEqual(t, 400, resp.StatusCode) + resp, err := http.Post("http://127.0.0.1:8889/test", "application/x-www-form-urlencoded", body) + assert.Nil(t, err) + assert.DeepEqual(t, 413, resp.StatusCode) + bodyBytes, _ := ioutil.ReadAll(resp.Body) + assert.DeepEqual(t, "Request Entity Too Large", string(bodyBytes)) } func TestEnoughBodySize(t *testing.T) { diff --git a/pkg/protocol/http1/server.go b/pkg/protocol/http1/server.go index 16a65b54a..6ff8d7dd6 100644 --- a/pkg/protocol/http1/server.go +++ b/pkg/protocol/http1/server.go @@ -374,6 +374,8 @@ func writeResponse(ctx *app.RequestContext, w network.Writer) error { func defaultErrorHandler(ctx *app.RequestContext, err error) { if netErr, ok := err.(*net.OpError); ok && netErr.Timeout() { ctx.AbortWithMsg("Request timeout", consts.StatusRequestTimeout) + } else if errors.Is(err, errs.ErrBodyTooLarge) { + ctx.AbortWithMsg("Request Entity Too Large", consts.StatusRequestEntityTooLarge) } else { ctx.AbortWithMsg("Error when parsing request", consts.StatusBadRequest) } From 0329357bf1d810d4e6b7cddb223b81eca9e92c01 Mon Sep 17 00:00:00 2001 From: Wenju Gao Date: Wed, 7 Dec 2022 11:42:00 +0800 Subject: [PATCH 04/19] optimize: ignore sighup signal if binary is run by nohup (#441) --- pkg/app/server/hertz.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pkg/app/server/hertz.go b/pkg/app/server/hertz.go index 651b1ede4..72a19c831 100644 --- a/pkg/app/server/hertz.go +++ b/pkg/app/server/hertz.go @@ -95,8 +95,13 @@ func (h *Hertz) SetCustomSignalWaiter(f func(err chan error) error) { // SIGTERM triggers immediately close. // SIGHUP|SIGINT triggers graceful shutdown. func waitSignal(errCh chan error) error { + signalToNotify := []os.Signal{syscall.SIGINT, syscall.SIGHUP, syscall.SIGTERM} + if signal.Ignored(syscall.SIGHUP) { + signalToNotify = []os.Signal{syscall.SIGINT, syscall.SIGTERM} + } + signals := make(chan os.Signal, 1) - signal.Notify(signals, syscall.SIGINT, syscall.SIGHUP, syscall.SIGTERM) + signal.Notify(signals, signalToNotify...) select { case sig := <-signals: @@ -105,6 +110,7 @@ func waitSignal(errCh chan error) error { // force exit return errors.New(sig.String()) // nolint case syscall.SIGHUP, syscall.SIGINT: + hlog.SystemLogger().Infof("Received signal: %s\n", sig) // graceful shutdown return nil } From a2dce784a10b1e830a20018f5dfddb4aec76e648 Mon Sep 17 00:00:00 2001 From: alice <90381261+alice-yyds@users.noreply.github.com> Date: Wed, 7 Dec 2022 11:48:27 +0800 Subject: [PATCH 05/19] chore: update version v0.4.2 --- version.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.go b/version.go index 006b989dc..2f877ce6b 100644 --- a/version.go +++ b/version.go @@ -19,5 +19,5 @@ package hertz // Name and Version info of this framework, used for statistics and debug const ( Name = "Hertz" - Version = "v0.4.1" + Version = "v0.4.2" ) From e404a113226e7c32425a0f9930d31f120906f3b2 Mon Sep 17 00:00:00 2001 From: "Asterisk L. Yuan" <92938836+L2ncE@users.noreply.github.com> Date: Thu, 8 Dec 2022 14:49:23 +0800 Subject: [PATCH 06/19] docs(README): add HTTP2 extension and optimize extensions sort order (#464) --- README.md | 37 +++++++++++++++++++------------------ README_cn.md | 37 +++++++++++++++++++------------------ 2 files changed, 38 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index ab5c28c51..387b2f493 100644 --- a/README.md +++ b/README.md @@ -60,32 +60,33 @@ Hertz [həːts] is a high-usability, high-performance and high-extensibility Gol | Extensions | Description | |----------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| [Autotls](https://github.com/hertz-contrib/autotls) | Make Hertz support Let's Encrypt. | +| [Http2](https://github.com/hertz-contrib/http2) | HTTP2 support for Hertz. | | [Websocket](https://github.com/hertz-contrib/websocket) | Enable Hertz to support the Websocket protocol. | -| [Pprof](https://github.com/hertz-contrib/pprof) | Extension for Hertz integration with Pprof. | -| [Sessions](https://github.com/hertz-contrib/sessions) | Session middleware with multi-state store support. | -| [Obs-opentelemetry](https://github.com/hertz-contrib/obs-opentelemetry) | Hertz's Opentelemetry extension that supports Metric, Logger, Tracing and works out of the box. | -| [Registry](https://github.com/hertz-contrib/registry) | Provides service registry and discovery functions. So far, the supported service discovery extensions are nacos, consul, etcd, eureka, polaris, servicecomb, zookeeper, redis. | -| [Keyauth](https://github.com/hertz-contrib/keyauth) | Provides token-based authentication. | -| [Secure](https://github.com/hertz-contrib/secure) | Secure middleware with multiple configuration items. | -| [Sentry](https://github.com/hertz-contrib/hertzsentry) | Sentry extension provides some unified interfaces to help users perform real-time error monitoring. | -| [Requestid](https://github.com/hertz-contrib/requestid) | Add request id in response. | | [Limiter](https://github.com/hertz-contrib/limiter) | Provides a current limiter based on the bbr algorithm. | -| [Jwt](https://github.com/hertz-contrib/jwt) | Jwt extension. | -| [Autotls](https://github.com/hertz-contrib/autotls) | Make Hertz support Let's Encrypt. | | [Monitor-prometheus](https://github.com/hertz-contrib/monitor-prometheus) | Provides service monitoring based on Prometheus. | -| [I18n](https://github.com/hertz-contrib/i18n) | Helps translate Hertz programs into multi programming languages. | -| [Reverseproxy](https://github.com/hertz-contrib/reverseproxy) | Implement a reverse proxy. | +| [Obs-opentelemetry](https://github.com/hertz-contrib/obs-opentelemetry) | Hertz's Opentelemetry extension that supports Metric, Logger, Tracing and works out of the box. | | [Opensergo](https://github.com/hertz-contrib/opensergo) | The Opensergo extension. | -| [Gzip](https://github.com/hertz-contrib/gzip) | A Gzip extension with multiple options. | -| [Cors](https://github.com/hertz-contrib/cors) | Provides cross-domain resource sharing support. | -| [Swagger](https://github.com/hertz-contrib/swagger) | Automatically generate RESTful API documentation with Swagger 2.0. | +| [Pprof](https://github.com/hertz-contrib/pprof) | Extension for Hertz integration with Pprof. | +| [Registry](https://github.com/hertz-contrib/registry) | Provides service registry and discovery functions. So far, the supported service discovery extensions are nacos, consul, etcd, eureka, polaris, servicecomb, zookeeper, redis. | +| [Sentry](https://github.com/hertz-contrib/hertzsentry) | Sentry extension provides some unified interfaces to help users perform real-time error monitoring. | | [Tracer](https://github.com/hertz-contrib/tracer) | Link tracing based on Opentracing. | -| [Recovery](https://github.com/cloudwego/hertz/tree/develop/pkg/app/middlewares/server/recovery) | Recovery middleware for Hertz. | | [Basicauth](https://github.com/cloudwego/hertz/tree/develop/pkg/app/middlewares/server/basic_auth) | Basicauth middleware can provide HTTP basic authentication. | -| [Lark](https://github.com/hertz-contrib/lark-hertz) | Use hertz handle Lark/Feishu card message and event callback. | -| [Logger](https://github.com/hertz-contrib/logger) | Logger extension for Hertz, which provides support for zap, logrus, zerologs logging frameworks. | +| [Jwt](https://github.com/hertz-contrib/jwt) | Jwt extension. | +| [Keyauth](https://github.com/hertz-contrib/keyauth) | Provides token-based authentication. | +| [Requestid](https://github.com/hertz-contrib/requestid) | Add request id in response. | +| [Sessions](https://github.com/hertz-contrib/sessions) | Session middleware with multi-state store support. | +| [Cors](https://github.com/hertz-contrib/cors) | Provides cross-domain resource sharing support. | | [Csrf](https://github.com/hertz-contrib/csrf) | Csrf middleware is used to prevent cross-site request forgery attacks. | +| [Secure](https://github.com/hertz-contrib/secure) | Secure middleware with multiple configuration items. | +| [Gzip](https://github.com/hertz-contrib/gzip) | A Gzip extension with multiple options. | +| [I18n](https://github.com/hertz-contrib/i18n) | Helps translate Hertz programs into multi programming languages. | +| [Lark](https://github.com/hertz-contrib/lark-hertz) | Use hertz handle Lark/Feishu card message and event callback. | | [Loadbalance](https://github.com/hertz-contrib/loadbalance) | Provides load balancing algorithms for Hertz. | +| [Logger](https://github.com/hertz-contrib/logger) | Logger extension for Hertz, which provides support for zap, logrus, zerologs logging frameworks. | +| [Recovery](https://github.com/cloudwego/hertz/tree/develop/pkg/app/middlewares/server/recovery) | Recovery middleware for Hertz. | +| [Reverseproxy](https://github.com/hertz-contrib/reverseproxy) | Implement a reverse proxy. | +| [Swagger](https://github.com/hertz-contrib/swagger) | Automatically generate RESTful API documentation with Swagger 2.0. | ## Blogs - [ByteDance Practice on Go Network Library](https://www.cloudwego.io/blog/2021/10/09/bytedance-practices-on-go-network-library/) diff --git a/README_cn.md b/README_cn.md index be60589be..2ca582317 100644 --- a/README_cn.md +++ b/README_cn.md @@ -60,32 +60,33 @@ Hertz[həːts] 是一个 Golang 微服务 HTTP 框架,在设计之初参考了 | 拓展 | 描述 | |----------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------| +| [Autotls](https://github.com/hertz-contrib/autotls) | 为 Hertz 支持 Let's Encrypt 。 | +| [Http2](https://github.com/hertz-contrib/http2) | 提供对 HTTP2 的支持。 | | [Websocket](https://github.com/hertz-contrib/websocket) | 使 Hertz 支持 Websocket 协议。 | -| [Pprof](https://github.com/hertz-contrib/pprof) | Hertz 集成 Pprof 的扩展。 | -| [Sessions](https://github.com/hertz-contrib/sessions) | 具有多状态存储支持的 Session 中间件。 | -| [Obs-opentelemetry](https://github.com/hertz-contrib/obs-opentelemetry) | Hertz 的 Opentelemetry 扩展,支持 Metric、Logger、Tracing并且达到开箱即用。 | -| [Registry](https://github.com/hertz-contrib/registry) | 提供服务注册与发现功能。到现在为止,支持的服务发现拓展有 nacos, consul, etcd, eureka, polaris, servicecomb, zookeeper, redis。 | -| [Keyauth](https://github.com/hertz-contrib/keyauth) | 提供基于 token 的身份验证。 | -| [Secure](https://github.com/hertz-contrib/secure) | 具有多配置项的 Secure 中间件。 | -| [Sentry](https://github.com/hertz-contrib/hertzsentry) | Sentry 拓展提供了一些统一的接口来帮助用户进行实时的错误监控。 | -| [Requestid](https://github.com/hertz-contrib/requestid) | 在 response 中添加 request id。 | | [Limiter](https://github.com/hertz-contrib/limiter) | 提供了基于 bbr 算法的限流器。 | -| [Jwt](https://github.com/hertz-contrib/jwt) | Jwt 拓展。 | -| [Autotls](https://github.com/hertz-contrib/autotls) | 为 Hertz 支持 Let's Encrypt 。 | | [Monitor-prometheus](https://github.com/hertz-contrib/monitor-prometheus) | 提供基于 Prometheus 服务监控功能。 | -| [I18n](https://github.com/hertz-contrib/i18n) | 可帮助将 Hertz 程序翻译成多种语言。 | -| [Reverseproxy](https://github.com/hertz-contrib/reverseproxy) | 实现反向代理。 | +| [Obs-opentelemetry](https://github.com/hertz-contrib/obs-opentelemetry) | Hertz 的 Opentelemetry 扩展,支持 Metric、Logger、Tracing并且达到开箱即用。 | | [Opensergo](https://github.com/hertz-contrib/opensergo) | Opensergo 扩展。 | -| [Gzip](https://github.com/hertz-contrib/gzip) | 含多个可选项的 Gzip 拓展。 | -| [Cors](https://github.com/hertz-contrib/cors) | 提供跨域资源共享支持。 | -| [Swagger](https://github.com/hertz-contrib/swagger) | 使用 Swagger 2.0 自动生成 RESTful API 文档。 | +| [Pprof](https://github.com/hertz-contrib/pprof) | Hertz 集成 Pprof 的扩展。 | +| [Registry](https://github.com/hertz-contrib/registry) | 提供服务注册与发现功能。到现在为止,支持的服务发现拓展有 nacos, consul, etcd, eureka, polaris, servicecomb, zookeeper, redis。 | +| [Sentry](https://github.com/hertz-contrib/hertzsentry) | Sentry 拓展提供了一些统一的接口来帮助用户进行实时的错误监控。 | | [Tracer](https://github.com/hertz-contrib/tracer) | 基于 Opentracing 的链路追踪。 | -| [Recovery](https://github.com/cloudwego/hertz/tree/develop/pkg/app/middlewares/server/recovery) | Hertz 的异常恢复中间件。 | | [Basicauth](https://github.com/cloudwego/hertz/tree/develop/pkg/app/middlewares/server/basic_auth) | Basicauth 中间件能够提供 HTTP 基本身份验证。 | -| [Lark](https://github.com/hertz-contrib/lark-hertz) | 在 Hertz 中处理 Lark/飞书的卡片消息和事件的回调。 | -| [Logger](https://github.com/hertz-contrib/logger) | Hertz 的日志拓展,提供了对 zap、logrus、zerologs 日志框架的支持。 | +| [Jwt](https://github.com/hertz-contrib/jwt) | Jwt 拓展。 | +| [Keyauth](https://github.com/hertz-contrib/keyauth) | 提供基于 token 的身份验证。 | +| [Requestid](https://github.com/hertz-contrib/requestid) | 在 response 中添加 request id。 | +| [Sessions](https://github.com/hertz-contrib/sessions) | 具有多状态存储支持的 Session 中间件。 | +| [Cors](https://github.com/hertz-contrib/cors) | 提供跨域资源共享支持。 | | [Csrf](https://github.com/hertz-contrib/csrf) | Csrf 中间件用于防止跨站点请求伪造攻击。 | +| [Secure](https://github.com/hertz-contrib/secure) | 具有多配置项的 Secure 中间件。 | +| [Gzip](https://github.com/hertz-contrib/gzip) | 含多个可选项的 Gzip 拓展。 | +| [I18n](https://github.com/hertz-contrib/i18n) | 可帮助将 Hertz 程序翻译成多种语言。 | +| [Lark](https://github.com/hertz-contrib/lark-hertz) | 在 Hertz 中处理 Lark/飞书的卡片消息和事件的回调。 | | [Loadbalance](https://github.com/hertz-contrib/loadbalance) | 提供适用于 Hertz 的负载均衡算法。 | +| [Logger](https://github.com/hertz-contrib/logger) | Hertz 的日志拓展,提供了对 zap、logrus、zerologs 日志框架的支持。 | +| [Recovery](https://github.com/cloudwego/hertz/tree/develop/pkg/app/middlewares/server/recovery) | Hertz 的异常恢复中间件。 | +| [Reverseproxy](https://github.com/hertz-contrib/reverseproxy) | 实现反向代理。 | +| [Swagger](https://github.com/hertz-contrib/swagger) | 使用 Swagger 2.0 自动生成 RESTful API 文档。 | ## 相关文章 - [字节跳动在 Go 网络库上的实践](https://www.cloudwego.io/blog/2021/10/09/bytedance-practices-on-go-network-library/) From cda5535475c91bacaaec6c8e04b3eda4d45613ef Mon Sep 17 00:00:00 2001 From: raymonder jin Date: Thu, 8 Dec 2022 16:05:30 +0800 Subject: [PATCH 07/19] test: add more tests for pkg/app/context (#404) --- pkg/app/context_test.go | 203 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 203 insertions(+) diff --git a/pkg/app/context_test.go b/pkg/app/context_test.go index 2e55eeb96..61615063c 100644 --- a/pkg/app/context_test.go +++ b/pkg/app/context_test.go @@ -21,6 +21,7 @@ import ( "errors" "fmt" "io/ioutil" + "reflect" "strings" "testing" "time" @@ -32,7 +33,9 @@ import ( "github.com/cloudwego/hertz/pkg/common/test/assert" "github.com/cloudwego/hertz/pkg/common/test/mock" "github.com/cloudwego/hertz/pkg/common/testdata/proto" + "github.com/cloudwego/hertz/pkg/common/tracer/traceinfo" "github.com/cloudwego/hertz/pkg/common/utils" + "github.com/cloudwego/hertz/pkg/network" "github.com/cloudwego/hertz/pkg/protocol" "github.com/cloudwego/hertz/pkg/protocol/consts" "github.com/cloudwego/hertz/pkg/protocol/http1/req" @@ -135,6 +138,23 @@ func TestNotFound(t *testing.T) { } } +func TestRedirect(t *testing.T) { + ctx := NewContext(0) + ctx.Redirect(302, []byte("/hello")) + assert.DeepEqual(t, 302, ctx.Response.StatusCode()) + + ctx.redirect([]byte("/hello"), 301) + assert.DeepEqual(t, 301, ctx.Response.StatusCode()) +} + +func TestGetRedirectStatusCode(t *testing.T) { + val := getRedirectStatusCode(301) + assert.DeepEqual(t, 301, val) + + val = getRedirectStatusCode(404) + assert.DeepEqual(t, 302, val) +} + func TestCookie(t *testing.T) { ctx := NewContext(0) ctx.Request.Header.SetCookie("cookie", "test cookie") @@ -171,6 +191,15 @@ func TestPost(t *testing.T) { } } +func TestGet(t *testing.T) { + ctx := NewContext(0) + ctx.Request.Header.SetMethod(consts.MethodPost) + assert.False(t, ctx.IsGet()) + + ctx.Request.Header.SetMethod(consts.MethodGet) + assert.True(t, ctx.IsGet()) +} + func TestCopy(t *testing.T) { t.Parallel() ctx := NewContext(0) @@ -311,6 +340,36 @@ hello=world`) } } +func TestDefaultPostForm(t *testing.T) { + ctx := makeCtxByReqString(t, `POST /upload HTTP/1.1 +Host: localhost:10000 +Content-Length: 521 +Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryJwfATyF8tmxSJnLg + +------WebKitFormBoundaryJwfATyF8tmxSJnLg +Content-Disposition: form-data; name="f1" + +value1 +------WebKitFormBoundaryJwfATyF8tmxSJnLg +Content-Disposition: form-data; name="fileaaa"; filename="TODO" +Content-Type: application/octet-stream + +- SessionClient with referer and cookies support. +- Client with requests' pipelining support. +- ProxyHandler similar to FSHandler. +- WebSockets. See https://tools.ietf.org/html/rfc6455 . +- HTTP/2.0. See https://tools.ietf.org/html/rfc7540 . + +------WebKitFormBoundaryJwfATyF8tmxSJnLg-- +`) + + val := ctx.DefaultPostForm("f1", "no val") + assert.DeepEqual(t, "value1", val) + + val = ctx.DefaultPostForm("f99", "no val") + assert.DeepEqual(t, "no val", val) +} + func TestRequestContext_FormFile(t *testing.T) { t.Parallel() @@ -497,6 +556,17 @@ func TestRequestContext_Handler(t *testing.T) { } } +func TestRequestContext_Handlers(t *testing.T) { + c := NewContext(0) + hc := HandlersChain{testFunc, testFunc2} + c.SetHandlers(hc) + c.Handlers()[1](context.Background(), c) + val := c.GetString("key") + if val != "123" { + t.Fatalf("unexpected %v. Expecting %v", val, "123") + } +} + func TestRequestContext_HandlerName(t *testing.T) { c := NewContext(0) c.handlers = HandlersChain{testFunc, testFunc2} @@ -617,6 +687,14 @@ func TestClientIp(t *testing.T) { } } +func TestSetClientIPFunc(t *testing.T) { + fn := func(ctx *RequestContext) string { + return "" + } + SetClientIPFunc(fn) + assert.DeepEqual(t, reflect.ValueOf(fn).Pointer(), reflect.ValueOf(defaultClientIP).Pointer()) +} + func TestGetQuery(t *testing.T) { c := NewContext(0) c.Request.SetRequestURI("http://aaa.com?a=1&b=") @@ -906,6 +984,131 @@ func TestContextGetStringMapStringSlice(t *testing.T) { assert.DeepEqual(t, expected, c.GetStringMapStringSlice("string")) } +func TestContextTraceInfo(t *testing.T) { + ctx := NewContext(0) + traceIn := traceinfo.NewTraceInfo() + ctx.SetTraceInfo(traceIn) + traceOut := ctx.GetTraceInfo() + + assert.DeepEqual(t, traceIn, traceOut) +} + +func TestEnableTrace(t *testing.T) { + ctx := NewContext(0) + ctx.SetEnableTrace(true) + trace := ctx.IsEnableTrace() + assert.True(t, trace) +} + +func TestForEachKey(t *testing.T) { + ctx := NewContext(0) + ctx.Set("1", "2") + handle := func(k string, v interface{}) { + res := k + v.(string) + assert.DeepEqual(t, res, "12") + } + ctx.ForEachKey(handle) + val, ok := ctx.Get("1") + assert.DeepEqual(t, val, "2") + assert.True(t, ok) +} + +func TestHijackHandler(t *testing.T) { + ctx := NewContext(0) + handle := func(c network.Conn) { + c.SetReadTimeout(time.Duration(1) * time.Second) + } + ctx.SetHijackHandler(handle) + handleRes := ctx.GetHijackHandler() + + val1 := reflect.ValueOf(handle).Pointer() + val2 := reflect.ValueOf(handleRes).Pointer() + assert.DeepEqual(t, val1, val2) +} + +func TestIndex(t *testing.T) { + ctx := NewContext(0) + ctx.ResetWithoutConn() + res := ctx.GetIndex() + exc := int8(-1) + assert.DeepEqual(t, exc, res) +} + +func TestHandlerName(t *testing.T) { + h := func(c context.Context, ctx *RequestContext) {} + SetHandlerName(h, "test") + name := GetHandlerName(h) + assert.DeepEqual(t, "test", name) +} + +func TestHijack(t *testing.T) { + ctx := NewContext(0) + h := func(c network.Conn) {} + ctx.Hijack(h) + assert.True(t, ctx.Hijacked()) +} + +func TestFinished(t *testing.T) { + ctx := NewContext(0) + ctx.Finished() + + ch := make(chan struct{}) + ctx.finished = ch + chRes := ctx.Finished() + + send := func() { + time.Sleep(time.Duration(1) * time.Millisecond) + ch <- struct{}{} + } + go send() + val := <-chRes + assert.DeepEqual(t, struct{}{}, val) +} + +func TestString(t *testing.T) { + ctx := NewContext(0) + ctx.String(200, "ok") + assert.DeepEqual(t, 200, ctx.Response.StatusCode()) +} + +func TestFullPath(t *testing.T) { + ctx := NewContext(0) + str := "/hello" + ctx.SetFullPath(str) + val := ctx.FullPath() + assert.DeepEqual(t, str, val) +} + +func TestReset(t *testing.T) { + ctx := NewContext(0) + ctx.Reset() + assert.DeepEqual(t, nil, ctx.conn) +} + +// func TestParam(t *testing.T) { +// ctx := NewContext(0) +// val := ctx.Param("/user/john") +// assert.DeepEqual(t, "john", val) +// } + +func TestGetHeader(t *testing.T) { + ctx := NewContext(0) + ctx.Request.Header.SetContentTypeBytes([]byte("text/plain; charset=utf-8")) + val := ctx.GetHeader("Content-Type") + assert.DeepEqual(t, "text/plain; charset=utf-8", string(val)) +} + +func TestGetRawData(t *testing.T) { + ctx := NewContext(0) + ctx.Request.SetBody([]byte("hello")) + val := ctx.GetRawData() + assert.DeepEqual(t, "hello", string(val)) + + val2, err := ctx.Body() + assert.DeepEqual(t, val, val2) + assert.Nil(t, err) +} + func TestRequestContext_GetRequest(t *testing.T) { c := &RequestContext{} c.Request.Header.Set("key1", "value1") From 926773c46520d28b0eec17c5f774d686562949b6 Mon Sep 17 00:00:00 2001 From: Wenju Gao Date: Thu, 8 Dec 2022 23:12:22 +0800 Subject: [PATCH 08/19] optimize: add requestOptions for global client APIs (#433) --- pkg/app/client/client.go | 41 ++++++++++++++++++++++++++++++---------- 1 file changed, 31 insertions(+), 10 deletions(-) diff --git a/pkg/app/client/client.go b/pkg/app/client/client.go index 99d32a537..3fd25714b 100644 --- a/pkg/app/client/client.go +++ b/pkg/app/client/client.go @@ -116,7 +116,8 @@ func Do(ctx context.Context, req *protocol.Request, resp *protocol.Response) err // Warning: DoTimeout does not terminate the request itself. The request will // continue in the background and the response will be discarded. // If requests take too long and the connection pool gets filled up please -// try using a Client and setting a ReadTimeout. +// try using a customized Client instance with a ReadTimeout config or set the request level read timeout like: +// `req.SetOptions(config.WithReadTimeout(1 * time.Second))` func DoTimeout(ctx context.Context, req *protocol.Request, resp *protocol.Response, timeout time.Duration) error { return defaultClient.DoTimeout(ctx, req, resp, timeout) } @@ -144,6 +145,12 @@ func DoTimeout(ctx context.Context, req *protocol.Request, resp *protocol.Respon // // It is recommended obtaining req and resp via AcquireRequest // and AcquireResponse in performance-critical code. +// +// Warning: DoDeadline does not terminate the request itself. The request will +// continue in the background and the response will be discarded. +// If requests take too long and the connection pool gets filled up please +// try using a customized Client instance with a ReadTimeout config or set the request level read timeout like: +// `req.SetOptions(config.WithReadTimeout(1 * time.Second))` func DoDeadline(ctx context.Context, req *protocol.Request, resp *protocol.Response, deadline time.Time) error { return defaultClient.DoDeadline(ctx, req, resp, deadline) } @@ -178,8 +185,9 @@ func DoRedirects(ctx context.Context, req *protocol.Request, resp *protocol.Resp // is too small a new slice will be allocated. // // The function follows redirects. Use Do* for manually handling redirects. -func Get(ctx context.Context, dst []byte, url string) (statusCode int, body []byte, err error) { - return defaultClient.Get(ctx, dst, url) +// +func Get(ctx context.Context, dst []byte, url string, requestOptions ...config.RequestOption) (statusCode int, body []byte, err error) { + return defaultClient.Get(ctx, dst, url, requestOptions...) } // GetTimeout returns the status code and body of url. @@ -191,8 +199,14 @@ func Get(ctx context.Context, dst []byte, url string) (statusCode int, body []by // // errTimeout error is returned if url contents couldn't be fetched // during the given timeout. -func GetTimeout(ctx context.Context, dst []byte, url string, timeout time.Duration) (statusCode int, body []byte, err error) { - return defaultClient.GetTimeout(ctx, dst, url, timeout) +// +// Warning: GetTimeout does not terminate the request itself. The request will +// continue in the background and the response will be discarded. +// If requests take too long and the connection pool gets filled up please +// try using a customized Client instance with a ReadTimeout config or set the request level read timeout like: +// `GetTimeout(ctx, dst, url, timeout, config.WithReadTimeout(1 * time.Second))` +func GetTimeout(ctx context.Context, dst []byte, url string, timeout time.Duration, requestOptions ...config.RequestOption) (statusCode int, body []byte, err error) { + return defaultClient.GetTimeout(ctx, dst, url, timeout, requestOptions...) } // GetDeadline returns the status code and body of url. @@ -204,8 +218,14 @@ func GetTimeout(ctx context.Context, dst []byte, url string, timeout time.Durati // // errTimeout error is returned if url contents couldn't be fetched // until the given deadline. -func GetDeadline(ctx context.Context, dst []byte, url string, deadline time.Time) (statusCode int, body []byte, err error) { - return defaultClient.GetDeadline(ctx, dst, url, deadline) +// +// Warning: GetDeadline does not terminate the request itself. The request will +// continue in the background and the response will be discarded. +// If requests take too long and the connection pool gets filled up please +// try using a customized Client instance with a ReadTimeout config or set the request level read timeout like: +// `GetDeadline(ctx, dst, url, timeout, config.WithReadTimeout(1 * time.Second))` +func GetDeadline(ctx context.Context, dst []byte, url string, deadline time.Time, requestOptions ...config.RequestOption) (statusCode int, body []byte, err error) { + return defaultClient.GetDeadline(ctx, dst, url, deadline, requestOptions...) } // Post sends POST request to the given url with the given POST arguments. @@ -216,8 +236,8 @@ func GetDeadline(ctx context.Context, dst []byte, url string, deadline time.Time // The function follows redirects. Use Do* for manually handling redirects. // // Empty POST body is sent if postArgs is nil. -func Post(ctx context.Context, dst []byte, url string, postArgs *protocol.Args) (statusCode int, body []byte, err error) { - return defaultClient.Post(ctx, dst, url, postArgs) +func Post(ctx context.Context, dst []byte, url string, postArgs *protocol.Args, requestOptions ...config.RequestOption) (statusCode int, body []byte, err error) { + return defaultClient.Post(ctx, dst, url, postArgs, requestOptions...) } var defaultClient, _ = NewClient(WithDialTimeout(consts.DefaultDialTimeout)) @@ -353,7 +373,8 @@ func (c *Client) Post(ctx context.Context, dst []byte, url string, postArgs *pro // Warning: DoTimeout does not terminate the request itself. The request will // continue in the background and the response will be discarded. // If requests take too long and the connection pool gets filled up please -// try setting a ReadTimeout. +// try using a customized Client instance with a ReadTimeout config or set the request level read timeout like: +// `req.SetOptions(config.WithReadTimeout(1 * time.Second))` func (c *Client) DoTimeout(ctx context.Context, req *protocol.Request, resp *protocol.Response, timeout time.Duration) error { return client.DoTimeout(ctx, req, resp, timeout, c) } From df408af00bfd86db6f967a0c776237d7a3648175 Mon Sep 17 00:00:00 2001 From: Wenju Gao Date: Fri, 9 Dec 2022 11:26:45 +0800 Subject: [PATCH 09/19] optimize(http1): try parse response even if get writing errors (#412) --- pkg/common/errors/errors.go | 23 +++++++------- pkg/network/connection.go | 4 +++ pkg/network/netpoll/connection.go | 9 ++++++ pkg/network/standard/connection.go | 13 ++++++-- pkg/protocol/http1/client.go | 27 +++++++++++++++- pkg/protocol/http1/client_test.go | 51 +++++++++++++++++++++++++++++- 6 files changed, 112 insertions(+), 15 deletions(-) diff --git a/pkg/common/errors/errors.go b/pkg/common/errors/errors.go index dc3f4bb16..e9cccdb6d 100644 --- a/pkg/common/errors/errors.go +++ b/pkg/common/errors/errors.go @@ -49,17 +49,18 @@ import ( var ( // These errors are the base error, which are used for checking in errors.Is() - ErrNeedMore = errors.New("need more data") - ErrChunkedStream = errors.New("chunked stream") - ErrBodyTooLarge = errors.New("body size exceeds the given limit") - ErrHijacked = errors.New("connection has been hijacked") - ErrIdleTimeout = errors.New("idle timeout") - ErrTimeout = errors.New("timeout") - ErrReadTimeout = errors.New("read timeout") - ErrWriteTimeout = errors.New("write timeout") - ErrDialTimeout = errors.New("dial timeout") - ErrNothingRead = errors.New("nothing read") - ErrShortConnection = errors.New("short connection") + ErrNeedMore = errors.New("need more data") + ErrChunkedStream = errors.New("chunked stream") + ErrBodyTooLarge = errors.New("body size exceeds the given limit") + ErrHijacked = errors.New("connection has been hijacked") + ErrIdleTimeout = errors.New("idle timeout") + ErrTimeout = errors.New("timeout") + ErrReadTimeout = errors.New("read timeout") + ErrWriteTimeout = errors.New("write timeout") + ErrDialTimeout = errors.New("dial timeout") + ErrNothingRead = errors.New("nothing read") + ErrShortConnection = errors.New("short connection") + ErrConnectionClosed = errors.New("connection closed") ) // ErrorType is an unsigned 64-bit error code as defined in the hertz spec. diff --git a/pkg/network/connection.go b/pkg/network/connection.go index c963f2b60..1559c1c54 100644 --- a/pkg/network/connection.go +++ b/pkg/network/connection.go @@ -94,3 +94,7 @@ type ConnTLSer interface { type HandleSpecificError interface { HandleSpecificError(err error, rip string) (needIgnore bool) } + +type ErrorNormalization interface { + ToHertzError(err error) error +} diff --git a/pkg/network/netpoll/connection.go b/pkg/network/netpoll/connection.go index 9281e6455..33d287579 100644 --- a/pkg/network/netpoll/connection.go +++ b/pkg/network/netpoll/connection.go @@ -25,15 +25,24 @@ import ( "strings" "syscall" + errs "github.com/cloudwego/hertz/pkg/common/errors" "github.com/cloudwego/hertz/pkg/common/hlog" "github.com/cloudwego/hertz/pkg/network" "github.com/cloudwego/netpoll" + "golang.org/x/sys/unix" ) type Conn struct { network.Conn } +func (c *Conn) ToHertzError(err error) error { + if errors.Is(err, netpoll.ErrConnClosed) || errors.Is(err, unix.EPIPE) { + return errs.ErrConnectionClosed + } + return err +} + func (c *Conn) Peek(n int) (b []byte, err error) { b, err = c.Conn.Peek(n) err = normalizeErr(err) diff --git a/pkg/network/standard/connection.go b/pkg/network/standard/connection.go index b1d96e0f1..b9b2e8343 100644 --- a/pkg/network/standard/connection.go +++ b/pkg/network/standard/connection.go @@ -18,13 +18,15 @@ package standard import ( "crypto/tls" + "errors" "io" "net" "strconv" "time" - "github.com/cloudwego/hertz/pkg/common/errors" + errs "github.com/cloudwego/hertz/pkg/common/errors" "github.com/cloudwego/hertz/pkg/network" + "golang.org/x/sys/unix" ) const ( @@ -44,6 +46,13 @@ type Conn struct { maxSize int // history max malloc size } +func (c *Conn) ToHertzError(err error) error { + if errors.Is(err, unix.EPIPE) || errors.Is(err, unix.ENOTCONN) { + return errs.ErrConnectionClosed + } + return err +} + func (c *Conn) SetWriteTimeout(t time.Duration) error { if t <= 0 { return c.c.SetWriteDeadline(time.Time{}) @@ -383,7 +392,7 @@ func (c *Conn) fill(i int) (err error) { func (c *Conn) Skip(n int) error { // check whether enough or not. if c.Len() < n { - return errors.NewPrivate("link buffer skip[" + strconv.Itoa(n) + "] not enough") + return errs.NewPrivate("link buffer skip[" + strconv.Itoa(n) + "] not enough") } c.inputBuffer.len -= n // re-cal length diff --git a/pkg/protocol/http1/client.go b/pkg/protocol/http1/client.go index a1f18cb11..6d4263618 100644 --- a/pkg/protocol/http1/client.go +++ b/pkg/protocol/http1/client.go @@ -508,7 +508,32 @@ func (c *HostClient) doNonNilReqResp(req *protocol.Request, resp *protocol.Respo } // error happened when writing request, close the connection, and try another connection if retry is enabled if err != nil { - c.closeConn(cc) + defer c.closeConn(cc) + + errNorm, ok := conn.(network.ErrorNormalization) + if ok { + err = errNorm.ToHertzError(err) + } + + if !errors.Is(err, errs.ErrConnectionClosed) { + return true, err + } + + // set a protection timeout to avoid infinite loop. + if conn.SetReadTimeout(time.Second) != nil { + return true, err + } + + // Only if the connection is closed while writing the request. Try to parse the response and return. + // In this case, the request/response is considered as successful. + // Otherwise, return the former error. + + zr := c.acquireReader(conn) + defer zr.Release() + if respI.ReadHeaderAndLimitBody(resp, zr, c.MaxResponseBodySize) == nil { + return false, nil + } + return true, err } diff --git a/pkg/protocol/http1/client_test.go b/pkg/protocol/http1/client_test.go index fa6d6c9ba..ad63f4181 100644 --- a/pkg/protocol/http1/client_test.go +++ b/pkg/protocol/http1/client_test.go @@ -266,6 +266,46 @@ func TestReadTimeoutPriority(t *testing.T) { } } +func TestDoNonNilReqResp(t *testing.T) { + c := &HostClient{ + ClientOptions: &ClientOptions{ + Dialer: newSlowConnDialer(func(network, addr string) (network.Conn, error) { + return &writeErrConn{ + Conn: mock.NewConn("HTTP/1.1 400 OK\nContent-Length: 6\n\n123456"), + }, + nil + }), + }, + } + req := protocol.AcquireRequest() + resp := protocol.AcquireResponse() + req.SetHost("foobar") + retry, err := c.doNonNilReqResp(req, resp) + assert.False(t, retry) + assert.Nil(t, err) + assert.DeepEqual(t, resp.StatusCode(), 400) + assert.DeepEqual(t, resp.Body(), []byte("123456")) +} + +func TestDoNonNilReqResp1(t *testing.T) { + c := &HostClient{ + ClientOptions: &ClientOptions{ + Dialer: newSlowConnDialer(func(network, addr string) (network.Conn, error) { + return &writeErrConn{ + Conn: mock.NewConn(""), + }, + nil + }), + }, + } + req := protocol.AcquireRequest() + resp := protocol.AcquireResponse() + req.SetHost("foobar") + retry, err := c.doNonNilReqResp(req, resp) + assert.True(t, retry) + assert.NotNil(t, err) +} + func TestWriteTimeoutPriority(t *testing.T) { c := &HostClient{ ClientOptions: &ClientOptions{ @@ -289,7 +329,7 @@ func TestWriteTimeoutPriority(t *testing.T) { ch <- c.Do(context.Background(), req, resp) }() select { - case <-time.After(time.Second * 2000): + case <-time.After(time.Second * 2): t.Fatalf("should use writeTimeout in request options") case err := <-ch: assert.DeepEqual(t, errs.ErrWriteTimeout.Error(), err.Error()) @@ -323,3 +363,12 @@ func TestDialTimeoutPriority(t *testing.T) { assert.DeepEqual(t, errs.ErrDialTimeout.Error(), err.Error()) } } + +// mockConn for getting error when write binary data. +type writeErrConn struct { + network.Conn +} + +func (w writeErrConn) WriteBinary(b []byte) (n int, err error) { + return 0, errs.ErrConnectionClosed +} From 216176b2bc631449b73da70f5ded7a0a4a344d5b Mon Sep 17 00:00:00 2001 From: chenghonour Date: Fri, 9 Dec 2022 18:04:01 +0800 Subject: [PATCH 10/19] Standardize the use of HTTP status codes in HZ tools and unit tests (#472) Co-authored-by: GuangyuFan <97507466+FGYFFFF@users.noreply.github.com> --- cmd/hz/generator/layout_tpl.go | 3 +- cmd/hz/generator/package_tpl.go | 9 ++-- pkg/app/client/client_test.go | 8 ++-- pkg/app/context_test.go | 24 +++++------ .../server/recovery/option_test.go | 3 +- .../server/recovery/recovery_test.go | 3 +- pkg/app/server/hertz_test.go | 30 ++++++------- pkg/common/ut/request_test.go | 17 ++++---- pkg/common/ut/response_test.go | 11 ++--- pkg/protocol/http1/resp/response_test.go | 42 +++++++++---------- pkg/protocol/response_test.go | 9 ++-- pkg/route/engine_test.go | 25 +++++------ pkg/route/routes_test.go | 8 ++-- 13 files changed, 100 insertions(+), 92 deletions(-) diff --git a/cmd/hz/generator/layout_tpl.go b/cmd/hz/generator/layout_tpl.go index 38b6f3662..3be154304 100644 --- a/cmd/hz/generator/layout_tpl.go +++ b/cmd/hz/generator/layout_tpl.go @@ -140,11 +140,12 @@ import ( "github.com/cloudwego/hertz/pkg/app" "github.com/cloudwego/hertz/pkg/common/utils" + "github.com/cloudwego/hertz/pkg/protocol/consts" ) // Ping . func Ping(ctx context.Context, c *app.RequestContext) { - c.JSON(200, utils.H{ + c.JSON(consts.StatusOK, utils.H{ "message": "pong", }) } diff --git a/cmd/hz/generator/package_tpl.go b/cmd/hz/generator/package_tpl.go index 5a2324e55..b089fba97 100644 --- a/cmd/hz/generator/package_tpl.go +++ b/cmd/hz/generator/package_tpl.go @@ -62,6 +62,7 @@ import ( "context" "github.com/cloudwego/hertz/pkg/app" + "github.com/cloudwego/hertz/pkg/protocol/consts" {{- range $k, $v := .Imports}} {{$k}} "{{$v.Package}}" @@ -76,13 +77,13 @@ func {{$MethodInfo.Name}}(ctx context.Context, c *app.RequestContext) { var req {{$MethodInfo.RequestTypeName}} err = c.BindAndValidate(&req) if err != nil { - c.String(400, err.Error()) + c.String(consts.StatusBadRequest, err.Error()) return } {{end}} resp := new({{$MethodInfo.ReturnTypeName}}) - c.{{.Serializer}}(200, resp) + c.{{.Serializer}}(consts.StatusOK, resp) } {{end}} `, @@ -222,13 +223,13 @@ func {{.Name}}(ctx context.Context, c *app.RequestContext) { var req {{.RequestTypeName}} err = c.BindAndValidate(&req) if err != nil { - c.String(400, err.Error()) + c.String(consts.StatusBadRequest, err.Error()) return } {{end}} resp := new({{.ReturnTypeName}}) - c.{{.Serializer}}(200, resp) + c.{{.Serializer}}(consts.StatusOK, resp) } `, }, diff --git a/pkg/app/client/client_test.go b/pkg/app/client/client_test.go index e325a0e14..c754ce510 100644 --- a/pkg/app/client/client_test.go +++ b/pkg/app/client/client_test.go @@ -484,7 +484,7 @@ func TestClientDefaultUserAgent(t *testing.T) { engine := route.NewEngine(opt) engine.GET("/", func(c context.Context, ctx *app.RequestContext) { - ctx.Data(200, "text/plain; charset=utf-8", ctx.UserAgent()) + ctx.Data(consts.StatusOK, "text/plain; charset=utf-8", ctx.UserAgent()) }) go engine.Run() defer func() { @@ -518,7 +518,7 @@ func TestClientSetUserAgent(t *testing.T) { engine := route.NewEngine(opt) engine.GET("/", func(c context.Context, ctx *app.RequestContext) { - ctx.Data(200, "text/plain; charset=utf-8", ctx.UserAgent()) + ctx.Data(consts.StatusOK, "text/plain; charset=utf-8", ctx.UserAgent()) }) go engine.Run() defer func() { @@ -549,7 +549,7 @@ func TestClientNoUserAgent(t *testing.T) { engine := route.NewEngine(opt) engine.GET("/", func(c context.Context, ctx *app.RequestContext) { - ctx.Data(200, "text/plain; charset=utf-8", ctx.UserAgent()) + ctx.Data(consts.StatusOK, "text/plain; charset=utf-8", ctx.UserAgent()) }) go engine.Run() defer func() { @@ -1283,7 +1283,7 @@ func TestPostWithFormData(t *testing.T) { ans = ans + string(key) + "=" + string(value) + "&" }) ans = strings.TrimRight(ans, "&") - ctx.Data(200, "text/plain; charset=utf-8", []byte(ans)) + ctx.Data(consts.StatusOK, "text/plain; charset=utf-8", []byte(ans)) }) go engine.Run() diff --git a/pkg/app/context_test.go b/pkg/app/context_test.go index 61615063c..6ae944372 100644 --- a/pkg/app/context_test.go +++ b/pkg/app/context_test.go @@ -47,7 +47,7 @@ import ( func TestProtobuf(t *testing.T) { ctx := NewContext(0) body := proto.TestStruct{Body: []byte("Hello World")} - ctx.ProtoBuf(200, &body) + ctx.ProtoBuf(consts.StatusOK, &body) assert.DeepEqual(t, string(ctx.Response.Body()), "\n\vHello World") } @@ -140,19 +140,19 @@ func TestNotFound(t *testing.T) { func TestRedirect(t *testing.T) { ctx := NewContext(0) - ctx.Redirect(302, []byte("/hello")) - assert.DeepEqual(t, 302, ctx.Response.StatusCode()) + ctx.Redirect(consts.StatusFound, []byte("/hello")) + assert.DeepEqual(t, consts.StatusFound, ctx.Response.StatusCode()) - ctx.redirect([]byte("/hello"), 301) - assert.DeepEqual(t, 301, ctx.Response.StatusCode()) + ctx.redirect([]byte("/hello"), consts.StatusMovedPermanently) + assert.DeepEqual(t, consts.StatusMovedPermanently, ctx.Response.StatusCode()) } func TestGetRedirectStatusCode(t *testing.T) { - val := getRedirectStatusCode(301) - assert.DeepEqual(t, 301, val) + val := getRedirectStatusCode(consts.StatusMovedPermanently) + assert.DeepEqual(t, consts.StatusMovedPermanently, val) - val = getRedirectStatusCode(404) - assert.DeepEqual(t, 302, val) + val = getRedirectStatusCode(consts.StatusNotFound) + assert.DeepEqual(t, consts.StatusFound, val) } func TestCookie(t *testing.T) { @@ -504,7 +504,7 @@ func TestContextRenderAttachment(t *testing.T) { ctx.FileAttachment("./context.go", newFilename) - assert.DeepEqual(t, 200, ctx.Response.StatusCode()) + assert.DeepEqual(t, consts.StatusOK, ctx.Response.StatusCode()) assert.True(t, strings.Contains(resp.GetHTTP1Response(&ctx.Response).String(), "func (ctx *RequestContext) FileAttachment(filepath, filename string) {")) assert.DeepEqual(t, fmt.Sprintf("attachment; filename=\"%s\"", newFilename), @@ -1067,8 +1067,8 @@ func TestFinished(t *testing.T) { func TestString(t *testing.T) { ctx := NewContext(0) - ctx.String(200, "ok") - assert.DeepEqual(t, 200, ctx.Response.StatusCode()) + ctx.String(consts.StatusOK, "ok") + assert.DeepEqual(t, consts.StatusOK, ctx.Response.StatusCode()) } func TestFullPath(t *testing.T) { diff --git a/pkg/app/middlewares/server/recovery/option_test.go b/pkg/app/middlewares/server/recovery/option_test.go index 4a2140d13..be3e7d67f 100644 --- a/pkg/app/middlewares/server/recovery/option_test.go +++ b/pkg/app/middlewares/server/recovery/option_test.go @@ -25,6 +25,7 @@ import ( "github.com/cloudwego/hertz/pkg/common/hlog" "github.com/cloudwego/hertz/pkg/common/test/assert" "github.com/cloudwego/hertz/pkg/common/utils" + "github.com/cloudwego/hertz/pkg/protocol/consts" ) func TestDefaultOption(t *testing.T) { @@ -35,7 +36,7 @@ func TestDefaultOption(t *testing.T) { func newRecoveryHandler(c context.Context, ctx *app.RequestContext, err interface{}, stack []byte) { hlog.SystemLogger().CtxErrorf(c, "[New Recovery] panic recovered:\n%s\n%s\n", err, stack) - ctx.JSON(501, utils.H{"msg": err.(string)}) + ctx.JSON(consts.StatusNotImplemented, utils.H{"msg": err.(string)}) } func TestOption(t *testing.T) { diff --git a/pkg/app/middlewares/server/recovery/recovery_test.go b/pkg/app/middlewares/server/recovery/recovery_test.go index ffaa1a381..b7211b321 100644 --- a/pkg/app/middlewares/server/recovery/recovery_test.go +++ b/pkg/app/middlewares/server/recovery/recovery_test.go @@ -23,6 +23,7 @@ import ( "github.com/cloudwego/hertz/pkg/app" "github.com/cloudwego/hertz/pkg/common/test/assert" + "github.com/cloudwego/hertz/pkg/protocol/consts" ) func TestRecovery(t *testing.T) { @@ -52,7 +53,7 @@ func TestWithRecoveryHandler(t *testing.T) { Recovery(WithRecoveryHandler(newRecoveryHandler))(context.Background(), ctx) - if ctx.Response.StatusCode() != 501 { + if ctx.Response.StatusCode() != consts.StatusNotImplemented { t.Fatalf("unexpected %v. Expecting %v", ctx.Response.StatusCode(), 501) } assert.DeepEqual(t, "{\"msg\":\"test\"}", string(ctx.Response.Body())) diff --git a/pkg/app/server/hertz_test.go b/pkg/app/server/hertz_test.go index f15db14e6..31206b06b 100644 --- a/pkg/app/server/hertz_test.go +++ b/pkg/app/server/hertz_test.go @@ -218,7 +218,7 @@ func TestLoadHTMLGlob(t *testing.T) { go engine.Run() time.Sleep(200 * time.Millisecond) resp, _ := http.Get("http://127.0.0.1:8890/index") - assert.DeepEqual(t, 200, resp.StatusCode) + assert.DeepEqual(t, consts.StatusOK, resp.StatusCode) b := make([]byte, 100) n, _ := resp.Body.Read(b) assert.DeepEqual(t, "\n

\n Main website\n

\n", string(b[0:n])) @@ -240,7 +240,7 @@ func TestLoadHTMLFiles(t *testing.T) { go engine.Run() time.Sleep(200 * time.Millisecond) resp, _ := http.Get("http://127.0.0.1:8891/raw") - assert.DeepEqual(t, 200, resp.StatusCode) + assert.DeepEqual(t, consts.StatusOK, resp.StatusCode) b := make([]byte, 100) n, _ := resp.Body.Read(b) assert.DeepEqual(t, "

Date: 2017/07/01

", string(b[0:n])) @@ -283,18 +283,18 @@ func TestServer_Run(t *testing.T) { time.Sleep(100 * time.Microsecond) resp, err := http.Get("http://127.0.0.1:8888/test") assert.Nil(t, err) - assert.DeepEqual(t, 200, resp.StatusCode) + assert.DeepEqual(t, consts.StatusOK, resp.StatusCode) b := make([]byte, 5) resp.Body.Read(b) assert.DeepEqual(t, "/test", string(b)) resp, err = http.Get("http://127.0.0.1:8888/foo") assert.Nil(t, err) - assert.DeepEqual(t, 404, resp.StatusCode) + assert.DeepEqual(t, consts.StatusNotFound, resp.StatusCode) resp, err = http.Post("http://127.0.0.1:8888/redirect", "", nil) assert.Nil(t, err) - assert.DeepEqual(t, 200, resp.StatusCode) + assert.DeepEqual(t, consts.StatusOK, resp.StatusCode) b = make([]byte, 5) resp.Body.Read(b) assert.DeepEqual(t, "/test", string(b)) @@ -323,7 +323,7 @@ func TestNotAbsolutePath(t *testing.T) { t.Fatalf("unexpected error: %s", err) } engine.ServeHTTP(context.Background(), ctx) - assert.DeepEqual(t, 200, ctx.Response.StatusCode()) + assert.DeepEqual(t, consts.StatusOK, ctx.Response.StatusCode()) assert.DeepEqual(t, ctx.Request.Body(), ctx.Response.Body()) s = "POST a?a=b HTTP/1.1\r\nContent-Length: 5\r\nContent-Type: foo/bar\r\n\r\nabcdef4343" @@ -334,7 +334,7 @@ func TestNotAbsolutePath(t *testing.T) { t.Fatalf("unexpected error: %s", err) } engine.ServeHTTP(context.Background(), ctx) - assert.DeepEqual(t, 200, ctx.Response.StatusCode()) + assert.DeepEqual(t, consts.StatusOK, ctx.Response.StatusCode()) assert.DeepEqual(t, ctx.Request.Body(), ctx.Response.Body()) } @@ -355,7 +355,7 @@ func TestNotAbsolutePathWithRawPath(t *testing.T) { t.Fatalf("unexpected error: %s", err) } engine.ServeHTTP(context.Background(), ctx) - assert.DeepEqual(t, 400, ctx.Response.StatusCode()) + assert.DeepEqual(t, consts.StatusBadRequest, ctx.Response.StatusCode()) assert.DeepEqual(t, default400Body, ctx.Response.Body()) s = "POST a?a=b HTTP/1.1\r\nContent-Length: 5\r\nContent-Type: foo/bar\r\n\r\nabcdef4343" @@ -366,7 +366,7 @@ func TestNotAbsolutePathWithRawPath(t *testing.T) { t.Fatalf("unexpected error: %s", err) } engine.ServeHTTP(context.Background(), ctx) - assert.DeepEqual(t, 400, ctx.Response.StatusCode()) + assert.DeepEqual(t, consts.StatusBadRequest, ctx.Response.StatusCode()) assert.DeepEqual(t, default400Body, ctx.Response.Body()) } @@ -398,7 +398,7 @@ func TestEnoughBodySize(t *testing.T) { r.Form.Add("xxxxxx", "xxx") body := strings.NewReader(r.Form.Encode()) resp, _ := http.Post("http://127.0.0.1:8892/test", "application/x-www-form-urlencoded", body) - assert.DeepEqual(t, 200, resp.StatusCode) + assert.DeepEqual(t, consts.StatusOK, resp.StatusCode) } func TestRequestCtxHijack(t *testing.T) { @@ -591,16 +591,16 @@ func TestReusePorts(t *testing.T) { hc := New(WithHostPorts("localhost:10093"), WithListenConfig(cfg)) hd := New(WithHostPorts("localhost:10093"), WithListenConfig(cfg)) ha.GET("/ping", func(c context.Context, ctx *app.RequestContext) { - ctx.JSON(200, utils.H{"ping": "pong"}) + ctx.JSON(consts.StatusOK, utils.H{"ping": "pong"}) }) hc.GET("/ping", func(c context.Context, ctx *app.RequestContext) { - ctx.JSON(200, utils.H{"ping": "pong"}) + ctx.JSON(consts.StatusOK, utils.H{"ping": "pong"}) }) hd.GET("/ping", func(c context.Context, ctx *app.RequestContext) { - ctx.JSON(200, utils.H{"ping": "pong"}) + ctx.JSON(consts.StatusOK, utils.H{"ping": "pong"}) }) hb.GET("/ping", func(c context.Context, ctx *app.RequestContext) { - ctx.JSON(200, utils.H{"ping": "pong"}) + ctx.JSON(consts.StatusOK, utils.H{"ping": "pong"}) }) go ha.Run() go hb.Run() @@ -612,7 +612,7 @@ func TestReusePorts(t *testing.T) { for i := 0; i < 1000; i++ { statusCode, body, err := client.Get(context.Background(), nil, "http://localhost:10093/ping") assert.Nil(t, err) - assert.DeepEqual(t, 200, statusCode) + assert.DeepEqual(t, consts.StatusOK, statusCode) assert.DeepEqual(t, "{\"ping\":\"pong\"}", string(body)) } } diff --git a/pkg/common/ut/request_test.go b/pkg/common/ut/request_test.go index d8883350c..8580d5405 100644 --- a/pkg/common/ut/request_test.go +++ b/pkg/common/ut/request_test.go @@ -25,6 +25,7 @@ import ( "github.com/cloudwego/hertz/pkg/app" "github.com/cloudwego/hertz/pkg/common/config" "github.com/cloudwego/hertz/pkg/common/test/assert" + "github.com/cloudwego/hertz/pkg/protocol/consts" "github.com/cloudwego/hertz/pkg/route" ) @@ -40,12 +41,12 @@ func TestPerformRequest(t *testing.T) { if string(c.Request.Body()) == "1" { assert.DeepEqual(t, "close", c.Request.Header.Get("Connection")) c.Response.SetConnectionClose() - c.JSON(201, map[string]string{"hi": user}) + c.JSON(consts.StatusCreated, map[string]string{"hi": user}) } else if string(c.Request.Body()) == "" { - c.AbortWithMsg("unauthorized", 401) + c.AbortWithMsg("unauthorized", consts.StatusUnauthorized) } else { assert.DeepEqual(t, "PUT /hey/dy HTTP/1.1\r\nContent-Type: application/x-www-form-urlencoded\r\nTransfer-Encoding: chunked\r\n\r\n", string(c.Request.Header.Header())) - c.String(202, "body:%v", string(c.Request.Body())) + c.String(consts.StatusAccepted, "body:%v", string(c.Request.Body())) } }) router.GET("/her/header", func(ctx context.Context, c *app.RequestContext) { @@ -58,7 +59,7 @@ func TestPerformRequest(t *testing.T) { w := PerformRequest(router, "PUT", "/hey/dy", &Body{bytes.NewBufferString("1"), 1}, Header{"Connection", "close"}) resp := w.Result() - assert.DeepEqual(t, 201, resp.StatusCode()) + assert.DeepEqual(t, consts.StatusCreated, resp.StatusCode()) assert.DeepEqual(t, "{\"hi\":\"dy\"}", string(resp.Body())) assert.DeepEqual(t, "application/json; charset=utf-8", string(resp.Header.ContentType())) assert.DeepEqual(t, true, resp.Header.ConnectionClose()) @@ -67,7 +68,7 @@ func TestPerformRequest(t *testing.T) { w = PerformRequest(router, "PUT", "/hey/dy", nil) _ = w.Result() resp = w.Result() - assert.DeepEqual(t, 401, resp.StatusCode()) + assert.DeepEqual(t, consts.StatusUnauthorized, resp.StatusCode()) assert.DeepEqual(t, "unauthorized", string(resp.Body())) assert.DeepEqual(t, "text/plain; charset=utf-8", string(resp.Header.ContentType())) assert.DeepEqual(t, 12, resp.Header.ContentLength()) @@ -83,21 +84,21 @@ func TestPerformRequest(t *testing.T) { // not found w = PerformRequest(router, "GET", "/hey", nil) resp = w.Result() - assert.DeepEqual(t, 404, resp.StatusCode()) + assert.DeepEqual(t, consts.StatusNotFound, resp.StatusCode()) // fake body w = PerformRequest(router, "GET", "/hey", nil) _, err := w.WriteString(", faker") resp = w.Result() assert.Nil(t, err) - assert.DeepEqual(t, 404, resp.StatusCode()) + assert.DeepEqual(t, consts.StatusNotFound, resp.StatusCode()) assert.DeepEqual(t, "404 page not found, faker", string(resp.Body())) // chunked body body := bytes.NewReader(createChunkedBody([]byte("hello world!"))) w = PerformRequest(router, "PUT", "/hey/dy", &Body{body, -1}) resp = w.Result() - assert.DeepEqual(t, 202, resp.StatusCode()) + assert.DeepEqual(t, consts.StatusAccepted, resp.StatusCode()) assert.DeepEqual(t, "body:1\r\nh\r\n2\r\nel\r\n3\r\nlo \r\n4\r\nworl\r\n2\r\nd!\r\n0\r\n\r\n", string(resp.Body())) } diff --git a/pkg/common/ut/response_test.go b/pkg/common/ut/response_test.go index 662714ea7..bec9f1389 100644 --- a/pkg/common/ut/response_test.go +++ b/pkg/common/ut/response_test.go @@ -20,27 +20,28 @@ import ( "testing" "github.com/cloudwego/hertz/pkg/common/test/assert" + "github.com/cloudwego/hertz/pkg/protocol/consts" ) func TestResult(t *testing.T) { r := new(ResponseRecorder) ret := r.Result() - assert.DeepEqual(t, 200, ret.StatusCode()) + assert.DeepEqual(t, consts.StatusOK, ret.StatusCode()) } func TestFlush(t *testing.T) { r := new(ResponseRecorder) r.Flush() ret := r.Result() - assert.DeepEqual(t, 200, ret.StatusCode()) + assert.DeepEqual(t, consts.StatusOK, ret.StatusCode()) } func TestWriterHeader(t *testing.T) { r := NewRecorder() - r.WriteHeader(201) - r.WriteHeader(200) + r.WriteHeader(consts.StatusCreated) + r.WriteHeader(consts.StatusOK) ret := r.Result() - assert.DeepEqual(t, 201, ret.StatusCode()) + assert.DeepEqual(t, consts.StatusCreated, ret.StatusCode()) } func TestWriteString(t *testing.T) { diff --git a/pkg/protocol/http1/resp/response_test.go b/pkg/protocol/http1/resp/response_test.go index 83680c726..f23f6dfeb 100644 --- a/pkg/protocol/http1/resp/response_test.go +++ b/pkg/protocol/http1/resp/response_test.go @@ -155,7 +155,7 @@ func testResponseReadError(t *testing.T, resp *protocol.Response, response strin } testResponseReadSuccess(t, resp, "HTTP/1.1 303 Redisred sedfs sdf\r\nContent-Type: aaa\r\nContent-Length: 5\r\n\r\nHELLOaaa", - 303, 5, "aaa", "HELLO", "aaa") + consts.StatusSeeOther, 5, "aaa", "HELLO", "aaa") } func testResponseReadSuccess(t *testing.T, resp *protocol.Response, response string, expectedStatusCode, expectedContentLength int, @@ -181,19 +181,19 @@ func TestResponseReadSuccess(t *testing.T) { // usual response testResponseReadSuccess(t, resp, "HTTP/1.1 200 OK\r\nContent-Length: 10\r\nContent-Type: foo/bar\r\n\r\n0123456789", - 200, 10, "foo/bar", "0123456789", "") + consts.StatusOK, 10, "foo/bar", "0123456789", "") // zero response testResponseReadSuccess(t, resp, "HTTP/1.1 500 OK\r\nContent-Length: 0\r\nContent-Type: foo/bar\r\n\r\n", - 500, 0, "foo/bar", "", "") + consts.StatusInternalServerError, 0, "foo/bar", "", "") // response with trailer testResponseReadSuccess(t, resp, "HTTP/1.1 300 OK\r\nContent-Length: 5\r\nContent-Type: bar\r\n\r\n56789aaa", - 300, 5, "bar", "56789", "aaa") + consts.StatusMultipleChoices, 5, "bar", "56789", "aaa") // no content-length ('identity' transfer-encoding) testResponseReadSuccess(t, resp, "HTTP/1.1 200 OK\r\nContent-Type: foobar\r\n\r\nzxxc", - 200, 4, "foobar", "zxxc", "") + consts.StatusOK, 4, "foobar", "zxxc", "") // explicitly stated 'Transfer-Encoding: identity' testResponseReadSuccess(t, resp, "HTTP/1.1 234 ss\r\nContent-Type: xxx\r\n\r\nxag", @@ -202,11 +202,11 @@ func TestResponseReadSuccess(t *testing.T) { // big 'identity' response body := string(mock.CreateFixedBody(100500)) testResponseReadSuccess(t, resp, "HTTP/1.1 200 OK\r\nContent-Type: aa\r\n\r\n"+body, - 200, 100500, "aa", body, "") + consts.StatusOK, 100500, "aa", body, "") // chunked response testResponseReadSuccess(t, resp, "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nTransfer-Encoding: chunked\r\n\r\n4\r\nqwer\r\n2\r\nty\r\n0\r\n\r\nzzzzz", - 200, 6, "text/html", "qwerty", "zzzzz") + consts.StatusOK, 6, "text/html", "qwerty", "zzzzz") // chunked response with non-chunked Transfer-Encoding. testResponseReadSuccess(t, resp, "HTTP/1.1 230 OK\r\nContent-Type: text\r\nTransfer-Encoding: aaabbb\r\n\r\n2\r\ner\r\n2\r\nty\r\n0\r\n\r\nwe", @@ -214,7 +214,7 @@ func TestResponseReadSuccess(t *testing.T) { // zero chunked response testResponseReadSuccess(t, resp, "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nTransfer-Encoding: chunked\r\n\r\n0\r\n\r\nzzz", - 200, 0, "text/html", "", "zzz") + consts.StatusOK, 0, "text/html", "", "zzz") } func TestResponseReadError(t *testing.T) { @@ -360,24 +360,24 @@ func TestResponseSuccess(t *testing.T) { t.Parallel() // 200 response - testResponseSuccess(t, 200, "test/plain", "server", "foobar", - 200, "test/plain", "server") + testResponseSuccess(t, consts.StatusOK, "test/plain", "server", "foobar", + consts.StatusOK, "test/plain", "server") // response with missing statusCode testResponseSuccess(t, 0, "text/plain", "server", "foobar", - 200, "text/plain", "server") + consts.StatusOK, "text/plain", "server") // response with missing server - testResponseSuccess(t, 500, "aaa", "", "aaadfsd", - 500, "aaa", "") + testResponseSuccess(t, consts.StatusInternalServerError, "aaa", "", "aaadfsd", + consts.StatusInternalServerError, "aaa", "") // empty body - testResponseSuccess(t, 200, "bbb", "qwer", "", - 200, "bbb", "qwer") + testResponseSuccess(t, consts.StatusOK, "bbb", "qwer", "", + consts.StatusOK, "bbb", "qwer") // missing content-type - testResponseSuccess(t, 200, "", "asdfsd", "asdf", - 200, string(bytestr.DefaultContentType), "asdfsd") + testResponseSuccess(t, consts.StatusOK, "", "asdfsd", "asdf", + consts.StatusOK, string(bytestr.DefaultContentType), "asdfsd") } func TestResponseReadLimitBody(t *testing.T) { @@ -405,16 +405,16 @@ func TestResponseReadWithoutBody(t *testing.T) { var resp protocol.Response testResponseReadWithoutBody(t, &resp, "HTTP/1.1 304 Not Modified\r\nContent-Type: aa\r\nContent-Length: 1235\r\n\r\nfoobar", false, - 304, 1235, "aa", "foobar") + consts.StatusNotModified, 1235, "aa", "foobar") testResponseReadWithoutBody(t, &resp, "HTTP/1.1 204 Foo Bar\r\nContent-Type: aab\r\nTransfer-Encoding: chunked\r\n\r\n123\r\nss", false, - 204, -1, "aab", "123\r\nss") + consts.StatusNoContent, -1, "aab", "123\r\nss") testResponseReadWithoutBody(t, &resp, "HTTP/1.1 123 AAA\r\nContent-Type: xxx\r\nContent-Length: 3434\r\n\r\naaaa", false, 123, 3434, "xxx", "aaaa") testResponseReadWithoutBody(t, &resp, "HTTP 200 OK\r\nContent-Type: text/xml\r\nContent-Length: 123\r\n\r\nxxxx", true, - 200, 123, "text/xml", "xxxx") + consts.StatusOK, 123, "text/xml", "xxxx") // '100 Continue' must be skipped. testResponseReadWithoutBody(t, &resp, "HTTP/1.1 100 Continue\r\nFoo-bar: baz\r\n\r\nHTTP/1.1 329 aaa\r\nContent-Type: qwe\r\nContent-Length: 894\r\n\r\nfoobar", true, @@ -495,7 +495,7 @@ func testResponseReadWithoutBody(t *testing.T, resp *protocol.Response, s string // verify that ordinal response is read after null-body response resp.SkipBody = false testResponseReadSuccess(t, resp, "HTTP/1.1 300 OK\r\nContent-Length: 5\r\nContent-Type: bar\r\n\r\n56789aaa", - 300, 5, "bar", "56789", "aaa") + consts.StatusMultipleChoices, 5, "bar", "56789", "aaa") } func testResponseReadLimitBodyError(t *testing.T, s string, maxBodySize int) { diff --git a/pkg/protocol/response_test.go b/pkg/protocol/response_test.go index 4f2f56e89..7d88e0b34 100644 --- a/pkg/protocol/response_test.go +++ b/pkg/protocol/response_test.go @@ -51,6 +51,7 @@ import ( "github.com/cloudwego/hertz/pkg/common/bytebufferpool" "github.com/cloudwego/hertz/pkg/common/compress" "github.com/cloudwego/hertz/pkg/common/test/assert" + "github.com/cloudwego/hertz/pkg/protocol/consts" ) func TestResponseCopyTo(t *testing.T) { @@ -64,7 +65,7 @@ func TestResponseCopyTo(t *testing.T) { // init resp // resp.laddr = zeroTCPAddr resp.SkipBody = true - resp.Header.SetStatusCode(200) + resp.Header.SetStatusCode(consts.StatusOK) resp.SetBodyString("test") testResponseCopyTo(t, &resp) } @@ -179,11 +180,11 @@ func testResponseCopyTo(t *testing.T, src *Response) { func TestResponseMustSkipBody(t *testing.T) { resp := Response{} - resp.SetStatusCode(200) + resp.SetStatusCode(consts.StatusOK) resp.SetBodyString("test") assert.False(t, resp.MustSkipBody()) // no content 204 means that skip body is necessary - resp.SetStatusCode(204) + resp.SetStatusCode(consts.StatusNoContent) resp.ResetBody() assert.True(t, resp.MustSkipBody()) } @@ -225,7 +226,7 @@ func TestResponseAcquireResponse(t *testing.T) { resp1 := AcquireResponse() assert.NotNil(t, resp1) resp1.SetBody([]byte("test")) - resp1.SetStatusCode(200) + resp1.SetStatusCode(consts.StatusOK) ReleaseResponse(resp1) assert.Nil(t, resp1.body) } diff --git a/pkg/route/engine_test.go b/pkg/route/engine_test.go index 379b53f63..83ffc1032 100644 --- a/pkg/route/engine_test.go +++ b/pkg/route/engine_test.go @@ -61,6 +61,7 @@ import ( "github.com/cloudwego/hertz/pkg/network" "github.com/cloudwego/hertz/pkg/network/netpoll" "github.com/cloudwego/hertz/pkg/network/standard" + "github.com/cloudwego/hertz/pkg/protocol/consts" ) func TestNew_Engine(t *testing.T) { @@ -105,7 +106,7 @@ func TestEngineUnescape(t *testing.T) { for _, r := range routes { e.GET(r, func(c context.Context, ctx *app.RequestContext) { - ctx.String(200, ctx.Param(ctx.Query("key"))) + ctx.String(consts.StatusOK, ctx.Param(ctx.Query("key"))) }) } @@ -122,7 +123,7 @@ func TestEngineUnescape(t *testing.T) { } for _, tr := range testRoutes { w := performRequest(e, http.MethodGet, tr.route+"?key="+tr.key) - assert.DeepEqual(t, 200, w.Code) + assert.DeepEqual(t, consts.StatusOK, w.Code) assert.DeepEqual(t, tr.want, w.Body.String()) } } @@ -142,7 +143,7 @@ func TestEngineUnescapeRaw(t *testing.T) { for _, r := range routes { e.GET(r, func(c context.Context, ctx *app.RequestContext) { - ctx.String(200, ctx.Param(ctx.Query("key"))) + ctx.String(consts.StatusOK, ctx.Param(ctx.Query("key"))) }) } @@ -169,7 +170,7 @@ func TestEngineUnescapeRaw(t *testing.T) { } for _, tr := range testRoutes { w := performRequest(e, http.MethodGet, tr.route+"?key="+tr.key) - assert.DeepEqual(t, 200, w.Code) + assert.DeepEqual(t, consts.StatusOK, w.Code) assert.DeepEqual(t, tr.want, w.Body.String()) } } @@ -179,7 +180,7 @@ func TestConnectionClose(t *testing.T) { atomic.StoreUint32(&engine.status, statusRunning) engine.Init() engine.GET("/foo", func(c context.Context, ctx *app.RequestContext) { - ctx.String(200, "ok") + ctx.String(consts.StatusOK, "ok") }) conn := mock.NewConn("GET /foo HTTP/1.1\r\nHost: google.com\r\nConnection: close\r\n\r\n") err := engine.Serve(context.Background(), conn) @@ -192,7 +193,7 @@ func TestConnectionClose01(t *testing.T) { engine.Init() engine.GET("/foo", func(c context.Context, ctx *app.RequestContext) { ctx.SetConnectionClose() - ctx.String(200, "ok") + ctx.String(consts.StatusOK, "ok") }) conn := mock.NewConn("GET /foo HTTP/1.1\r\nHost: google.com\r\n\r\n") err := engine.Serve(context.Background(), conn) @@ -206,7 +207,7 @@ func TestIdleTimeout(t *testing.T) { engine.Init() engine.GET("/foo", func(c context.Context, ctx *app.RequestContext) { time.Sleep(100 * time.Millisecond) - ctx.String(200, "ok") + ctx.String(consts.StatusOK, "ok") }) conn := mock.NewConn("GET /foo HTTP/1.1\r\nHost: google.com\r\n\r\n") @@ -237,7 +238,7 @@ func TestIdleTimeout01(t *testing.T) { atomic.StoreUint32(&engine.status, statusRunning) engine.GET("/foo", func(c context.Context, ctx *app.RequestContext) { time.Sleep(10 * time.Millisecond) - ctx.String(200, "ok") + ctx.String(consts.StatusOK, "ok") }) conn := mock.NewConn("GET /foo HTTP/1.1\r\nHost: google.com\r\n\r\n") @@ -266,7 +267,7 @@ func TestIdleTimeout03(t *testing.T) { atomic.StoreUint32(&engine.status, statusRunning) engine.GET("/foo", func(c context.Context, ctx *app.RequestContext) { time.Sleep(50 * time.Millisecond) - ctx.String(200, "ok") + ctx.String(consts.StatusOK, "ok") }) conn := mock.NewConn("GET /foo HTTP/1.1\r\nHost: google.com\r\n\r\n" + @@ -412,7 +413,7 @@ func TestRenderHtml(t *testing.T) { }) rr := performRequest(e, "GET", "/templateName") b, _ := ioutil.ReadAll(rr.Body) - assert.DeepEqual(t, 200, rr.Code) + assert.DeepEqual(t, consts.StatusOK, rr.Code) assert.DeepEqual(t, []byte("

Date: 2017/07/01

"), b) assert.DeepEqual(t, "text/html; charset=utf-8", rr.Header().Get("Content-Type")) } @@ -460,7 +461,7 @@ func TestRenderHtmlOfGlobWithAutoRender(t *testing.T) { }) rr := performRequest(e, "GET", "/templateName") b, _ := ioutil.ReadAll(rr.Body) - assert.DeepEqual(t, 200, rr.Code) + assert.DeepEqual(t, consts.StatusOK, rr.Code) assert.DeepEqual(t, []byte("

Date: 2017/07/01

"), b) assert.DeepEqual(t, "text/html; charset=utf-8", rr.Header().Get("Content-Type")) } @@ -481,7 +482,7 @@ func TestRenderHtmlOfFilesWithAutoRender(t *testing.T) { }) rr := performRequest(e, "GET", "/templateName") b, _ := ioutil.ReadAll(rr.Body) - assert.DeepEqual(t, 200, rr.Code) + assert.DeepEqual(t, consts.StatusOK, rr.Code) assert.DeepEqual(t, []byte("

Date: 2017/07/01

"), b) assert.DeepEqual(t, "text/html; charset=utf-8", rr.Header().Get("Content-Type")) } diff --git a/pkg/route/routes_test.go b/pkg/route/routes_test.go index 6d8cac69c..1e76d673e 100644 --- a/pkg/route/routes_test.go +++ b/pkg/route/routes_test.go @@ -267,10 +267,10 @@ func TestRouteRedirectTrailingSlash(t *testing.T) { w = performRequest(router, consts.MethodGet, "/path2", header{Key: "X-Forwarded-Prefix", Value: "/api"}) assert.DeepEqual(t, "/api/path2/", w.Header().Get("Location")) - assert.DeepEqual(t, 301, w.Code) + assert.DeepEqual(t, consts.StatusMovedPermanently, w.Code) w = performRequest(router, consts.MethodGet, "/path2/", header{Key: "X-Forwarded-Prefix", Value: "/api/"}) - assert.DeepEqual(t, 200, w.Code) + assert.DeepEqual(t, consts.StatusOK, w.Code) router.options.RedirectTrailingSlash = false @@ -593,7 +593,7 @@ func TestRouterStaticFSNotFound(t *testing.T) { router := NewEngine(config.NewOptions(nil)) router.StaticFS("/", &app.FS{Root: "/thisreallydoesntexist/"}) router.NoRoute(func(c context.Context, ctx *app.RequestContext) { - ctx.String(404, "non existent") + ctx.String(consts.StatusNotFound, "non existent") }) w := performRequest(router, consts.MethodGet, "/nonexistent") @@ -825,7 +825,7 @@ func TestRouterParamWithSlash(t *testing.T) { ctx.Request.Header.SetMethod(consts.MethodGet) e.ServeHTTP(context.Background(), ctx) assert.Nil(t, getHelper(ctx, "path")) - assert.DeepEqual(t, 404, ctx.Response.StatusCode()) + assert.DeepEqual(t, consts.StatusNotFound, ctx.Response.StatusCode()) } func TestRouteMultiLevelBacktracking(t *testing.T) { From 3225094b7df4e55e40c4dae54425a38707918c15 Mon Sep 17 00:00:00 2001 From: "Asterisk L. Yuan" <92938836+L2ncE@users.noreply.github.com> Date: Fri, 9 Dec 2022 18:22:23 +0800 Subject: [PATCH 11/19] ci: add a labeler to limit the specification of the issue template (#437) --- .github/labels.json | 24 ++++++++++++++++++++++++ .github/workflows/invalid_question.yml | 26 ++++++++++++++++++++++++++ .github/workflows/labeler.yml | 17 +++++++++++++++++ 3 files changed, 67 insertions(+) create mode 100644 .github/labels.json create mode 100644 .github/workflows/invalid_question.yml create mode 100644 .github/workflows/labeler.yml diff --git a/.github/labels.json b/.github/labels.json new file mode 100644 index 000000000..ca8ef3b74 --- /dev/null +++ b/.github/labels.json @@ -0,0 +1,24 @@ +{ + "labels": { + "invalid_issue": { + "name": "invalid issue", + "colour": "#CF2E1F", + "description": "invalid issue (not related to Hertz or described in document or not enough information provided)" + } + }, + "issue": { + "invalid_issue": { + "requires": 2, + "conditions": [ + { + "type": "descriptionMatches", + "pattern": "/^((?!Describe the bug).)*$/is" + }, + { + "type": "descriptionMatches", + "pattern": "/^((?!Is your feature request related to a problem\\? Please describe).)*$/is" + } + ] + } + } +} diff --git a/.github/workflows/invalid_question.yml b/.github/workflows/invalid_question.yml new file mode 100644 index 000000000..f465cd4d9 --- /dev/null +++ b/.github/workflows/invalid_question.yml @@ -0,0 +1,26 @@ +name: "Close Invalid Issue" +on: + schedule: + - cron: "*/10 * * * *" + +permissions: + contents: read + +jobs: + stale: + permissions: + issues: write + runs-on: ubuntu-latest + env: + ACTIONS_STEP_DEBUG: true + steps: + - name: Close Stale Issues + uses: actions/stale@v6 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + stale-issue-message: "This issue has been marked as invalid question, please give more information by following the `issue` template. The issue will be closed in 3 days if no further activity occurs." + stale-issue-label: "stale" + days-before-stale: 0 + days-before-close: 3 + remove-stale-when-updated: true + only-labels: "invalid issue" diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml new file mode 100644 index 000000000..080e937a6 --- /dev/null +++ b/.github/workflows/labeler.yml @@ -0,0 +1,17 @@ +name: "Labeler" +on: + issues: + types: [ opened, edited, reopened ] + +jobs: + triage: + runs-on: ubuntu-latest + name: Label issues + steps: + - name: check out + uses: actions/checkout@v3 + + - name: labeler + uses: jbinda/super-labeler-action@develop + with: + GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" From c7268c65cb5a0391a4f392430fa2ecc6befea0f9 Mon Sep 17 00:00:00 2001 From: raymonder jin Date: Mon, 12 Dec 2022 14:26:01 +0800 Subject: [PATCH 12/19] test: add tests for pkg/app/server/registry (#457) --- pkg/app/server/registry/registry_test.go | 29 ++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 pkg/app/server/registry/registry_test.go diff --git a/pkg/app/server/registry/registry_test.go b/pkg/app/server/registry/registry_test.go new file mode 100644 index 000000000..77afdb463 --- /dev/null +++ b/pkg/app/server/registry/registry_test.go @@ -0,0 +1,29 @@ +/* + * Copyright 2022 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package registry + +import ( + "testing" + + "github.com/cloudwego/hertz/pkg/common/test/assert" +) + +func TestNoopRegistry(t *testing.T) { + reg := noopRegistry{} + assert.Nil(t, reg.Deregister(&Info{})) + assert.Nil(t, reg.Register(&Info{})) +} From bcedb134e722871154e9227e8842c87f30b0cc64 Mon Sep 17 00:00:00 2001 From: gityh2021 <85598202+gityh2021@users.noreply.github.com> Date: Mon, 12 Dec 2022 17:13:54 +0800 Subject: [PATCH 13/19] test: add new test cases for pkg/protocol/multipart.go (#416) --- pkg/protocol/multipart_test.go | 162 ++++++++++++++++++++++++++++++++- 1 file changed, 160 insertions(+), 2 deletions(-) diff --git a/pkg/protocol/multipart_test.go b/pkg/protocol/multipart_test.go index b575d7894..be93b8953 100644 --- a/pkg/protocol/multipart_test.go +++ b/pkg/protocol/multipart_test.go @@ -44,13 +44,15 @@ package protocol import ( "bytes" "mime/multipart" + "os" "strings" "testing" + + "github.com/cloudwego/hertz/pkg/common/test/assert" ) func TestWriteMultipartForm(t *testing.T) { t.Parallel() - var w bytes.Buffer s := strings.Replace(`--foo Content-Disposition: form-data; name="key" @@ -69,7 +71,18 @@ Content-Type: application/json t.Fatalf("unexpected error: %s", err) } - if err := WriteMultipartForm(&w, form, "foo"); err != nil { + // The length of boundary is in the range of [1,70], which can be verified for strings outside this range. + err = WriteMultipartForm(&w, form, s) + assert.NotNil(t, err) + + // set Boundary as empty + assert.Panic(t, func() { + err = WriteMultipartForm(&w, form, "") + }) + + // normal test + err = WriteMultipartForm(&w, form, "foo") + if err != nil { t.Fatalf("unexpected error: %s", err) } @@ -77,3 +90,148 @@ Content-Type: application/json t.Fatalf("unexpected output %q", w.Bytes()) } } + +func TestParseMultipartForm(t *testing.T) { + t.Parallel() + s := strings.Replace(`--foo +Content-Disposition: form-data; name="key" + +value +--foo-- +`, "\n", "\r\n", -1) + req1 := Request{} + req1.SetMultipartFormBoundary("foo") + // test size 0 + assert.NotNil(t, ParseMultipartForm(strings.NewReader(s), &req1, 0, 0)) + + err := ParseMultipartForm(strings.NewReader(s), &req1, 1024, 1024) + if err != nil { + t.Fatalf("unexpected error %s", err) + } + + req2 := Request{} + mr := multipart.NewReader(strings.NewReader(s), "foo") + form, err := mr.ReadForm(1024) + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + SetMultipartFormWithBoundary(&req2, form, "foo") + assert.DeepEqual(t, &req1, &req2) + + // set Boundary as " " + req1.SetMultipartFormBoundary(" ") + err = ParseMultipartForm(strings.NewReader(s), &req1, 1024, 1024) + assert.NotNil(t, err) + + // set size 0 + err = ParseMultipartForm(strings.NewReader(s), &req1, 0, 0) + assert.NotNil(t, err) +} + +func TestWriteMultipartFormFile(t *testing.T) { + t.Parallel() + bodyBuffer := &bytes.Buffer{} + w := multipart.NewWriter(bodyBuffer) + + // read multipart.go to buf1 + f1, err := os.Open("./multipart.go") + if err != nil { + t.Fatalf("open file %s error: %s", f1.Name(), err) + } + defer f1.Close() + + multipartFile := File{ + Name: f1.Name(), + ParamName: "multipartCode", + Reader: f1, + } + + err = WriteMultipartFormFile(w, multipartFile.ParamName, f1.Name(), multipartFile.Reader) + if err != nil { + t.Fatalf("write multipart error: %s", err) + } + + fileInfo1, err := f1.Stat() + if err != nil { + t.Fatalf("get file state error: %s", err) + } + + buf1 := make([]byte, fileInfo1.Size()) + _, err = f1.ReadAt(buf1, 0) + if err != nil { + t.Fatalf("read file to bytes error: %s", err) + } + assert.True(t, strings.Contains(bodyBuffer.String(), string(buf1))) + + // test file not found + assert.NotNil(t, WriteMultipartFormFile(w, multipartFile.ParamName, "test.go", multipartFile.Reader)) + + // Test Add File Function + err = AddFile(w, "responseCode", "./response.go") + if err != nil { + t.Fatalf("add file error: %s", err) + } + + // read response.go to buf2 + f2, err := os.Open("./response.go") + if err != nil { + t.Fatalf("open file %s error: %s", f2.Name(), err) + } + defer f2.Close() + + fileInfo2, err := f2.Stat() + if err != nil { + t.Fatalf("get file state error: %s", err) + } + buf2 := make([]byte, fileInfo2.Size()) + _, err = f2.ReadAt(buf2, 0) + if err != nil { + t.Fatalf("read file to bytes error: %s", err) + } + assert.True(t, strings.Contains(bodyBuffer.String(), string(buf2))) + + // test file not found + err = AddFile(w, "responseCode", "./test.go") + assert.NotNil(t, err) + + // test WriteMultipartFormFile without file name + bodyBuffer = &bytes.Buffer{} + w = multipart.NewWriter(bodyBuffer) + // read multipart.go to buf1 + f3, err := os.Open("./multipart.go") + if err != nil { + t.Fatalf("open file %s error: %s", f3.Name(), err) + } + defer f3.Close() + err = WriteMultipartFormFile(w, "multipart", " ", f3) + if err != nil { + t.Fatalf("write multipart error: %s", err) + } + assert.False(t, strings.Contains(bodyBuffer.String(), f3.Name())) +} + +func TestMarshalMultipartForm(t *testing.T) { + s := strings.Replace(`--foo +Content-Disposition: form-data; name="key" + +value +--foo +Content-Disposition: form-data; name="file"; filename="test.json" +Content-Type: application/json + +{"foo": "bar"} +--foo-- +`, "\n", "\r\n", -1) + mr := multipart.NewReader(strings.NewReader(s), "foo") + form, err := mr.ReadForm(1024) + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + bufs, err := MarshalMultipartForm(form, "foo") + assert.Nil(t, err) + assert.DeepEqual(t, s, string(bufs)) + + // set boundary invalid + _, err = MarshalMultipartForm(form, " ") + assert.NotNil(t, err) +} From 251a11abce3e5dcc2aae416958d0190024278fcf Mon Sep 17 00:00:00 2001 From: Pure White Date: Tue, 13 Dec 2022 21:05:17 +0800 Subject: [PATCH 14/19] chore: add CODEOWNERS --- .github/CODEOWNERS | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .github/CODEOWNERS diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 000000000..1125a66d9 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,3 @@ +# For more information, please refer to https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners + +* @cloudwego/Hertz-reviewer @cloudwego/Hertz-approver @cloudwego/Hertz-maintainer From 5e1f1b49947aa6c3420be7a355747069915ba548 Mon Sep 17 00:00:00 2001 From: Pure White Date: Wed, 14 Dec 2022 11:29:21 +0800 Subject: [PATCH 15/19] chore: fix CODEOWNERS team name --- .github/CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 1125a66d9..3b085c14c 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,3 +1,3 @@ # For more information, please refer to https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners -* @cloudwego/Hertz-reviewer @cloudwego/Hertz-approver @cloudwego/Hertz-maintainer +* @cloudwego/hertz-reviewers @cloudwego/hertz-approvers @cloudwego/hertz-maintainers From fefa4e1d1b3ac4f061400e6ecae36d5210739272 Mon Sep 17 00:00:00 2001 From: Xuran <37136584+Duslia@users.noreply.github.com> Date: Wed, 14 Dec 2022 14:13:39 +0800 Subject: [PATCH 16/19] feat: add transporter config (#454) --- pkg/app/server/hertz_test.go | 35 +++++++++++++++++++++++++++++++ pkg/app/server/option.go | 17 +++++++++++++++ pkg/common/config/option.go | 10 +++++++++ pkg/network/netpoll/transport.go | 14 +++++++++++++ pkg/network/standard/transport.go | 16 +++++++++++++- 5 files changed, 91 insertions(+), 1 deletion(-) diff --git a/pkg/app/server/hertz_test.go b/pkg/app/server/hertz_test.go index 31206b06b..04ea28be6 100644 --- a/pkg/app/server/hertz_test.go +++ b/pkg/app/server/hertz_test.go @@ -766,3 +766,38 @@ func TestReuseCtx(t *testing.T) { assert.Nil(t, err) } } + +func TestOnprepare(t *testing.T) { + h := New( + WithOnAccept(func(conn network.Conn) context.Context { + timer := time.NewTimer(time.Second) + ch := make(chan error) + go func() { + _, err := conn.Peek(3) + ch <- err + }() + select { + case <-timer.C: + return context.Background() + case <-ch: + t.Fatal("should not read data") + } + return context.Background() + }), + WithHostPorts("localhost:9229"), + WithOnConnect(func(ctx context.Context, conn network.Conn) context.Context { + b, err := conn.Peek(3) + assert.Nil(t, err) + assert.DeepEqual(t, string(b), "GET") + conn.Close() + return ctx + })) + h.GET("/ping", func(ctx context.Context, c *app.RequestContext) { + c.JSON(consts.StatusOK, utils.H{"ping": "pong"}) + }) + + go h.Spin() + time.Sleep(time.Second) + _, _, err := c.Get(context.Background(), nil, "http://127.0.0.1:9229/ping") + assert.DeepEqual(t, "the server closed connection before returning the first response byte. Make sure the server returns 'Connection: close' response header before closing the connection", err.Error()) +} diff --git a/pkg/app/server/option.go b/pkg/app/server/option.go index 4a4a8c606..4334dff4d 100644 --- a/pkg/app/server/option.go +++ b/pkg/app/server/option.go @@ -17,6 +17,7 @@ package server import ( + "context" "crypto/tls" "net" "time" @@ -304,3 +305,19 @@ func WithDisablePrintRoute(b bool) config.Option { o.DisablePrintRoute = b }} } + +// WithOnAccept sets the callback function when a new connection is accepted but cannot +// receive data in netpoll. In go net, it will be called before converting tls connection +func WithOnAccept(fn func(conn network.Conn) context.Context) config.Option { + return config.Option{F: func(o *config.Options) { + o.OnAccept = fn + }} +} + +// WithOnConnect sets the onConnect function. It can received data from connection in netpoll. +// In go net, it will be called after converting tls connection. +func WithOnConnect(fn func(ctx context.Context, conn network.Conn) context.Context) config.Option { + return config.Option{F: func(o *config.Options) { + o.OnConnect = fn + }} +} diff --git a/pkg/common/config/option.go b/pkg/common/config/option.go index 3b24f82f9..871b23bf9 100644 --- a/pkg/common/config/option.go +++ b/pkg/common/config/option.go @@ -17,6 +17,7 @@ package config import ( + "context" "crypto/tls" "net" "time" @@ -73,6 +74,15 @@ type Options struct { // TransporterNewer is the function to create a transporter. TransporterNewer func(opt *Options) network.Transporter + // In netpoll library, OnAccept is called after connection accepted + // but before adding it to epoll. OnConnect is called after adding it to epoll. + // The difference is that onConnect can get data but OnAccept cannot. + // If you'd like to check whether the peer IP is in the blacklist, you can use OnAccept. + // In go net, OnAccept is executed after connection accepted but before establishing + // tls connection. OnConnect is executed after establishing tls connection. + OnAccept func(conn network.Conn) context.Context + OnConnect func(ctx context.Context, conn network.Conn) context.Context + // Registry is used for service registry. Registry registry.Registry // RegistryInfo is base info used for service registry. diff --git a/pkg/network/netpoll/transport.go b/pkg/network/netpoll/transport.go index 1ea41f4de..e9314ccc6 100644 --- a/pkg/network/netpoll/transport.go +++ b/pkg/network/netpoll/transport.go @@ -41,6 +41,8 @@ type transporter struct { listener net.Listener eventLoop netpoll.EventLoop listenConfig *net.ListenConfig + OnAccept func(conn network.Conn) context.Context + OnConnect func(ctx context.Context, conn network.Conn) context.Context } // For transporter switch @@ -54,6 +56,8 @@ func NewTransporter(options *config.Options) network.Transporter { listener: nil, eventLoop: nil, listenConfig: options.ListenConfig, + OnAccept: options.OnAccept, + OnConnect: options.OnConnect, } } @@ -79,9 +83,19 @@ func (t *transporter) ListenAndServe(onReq network.OnData) (err error) { if t.writeTimeout > 0 { conn.SetWriteTimeout(t.writeTimeout) } + if t.OnAccept != nil { + return t.OnAccept(newConn(conn)) + } return context.Background() }), } + + if t.OnConnect != nil { + opts = append(opts, netpoll.WithOnConnect(func(ctx context.Context, conn netpoll.Connection) context.Context { + return t.OnConnect(ctx, newConn(conn)) + })) + } + // Create EventLoop t.Lock() t.eventLoop, err = netpoll.NewEventLoop(func(ctx context.Context, connection netpoll.Connection) error { diff --git a/pkg/network/standard/transport.go b/pkg/network/standard/transport.go index 624f9415b..b6131b224 100644 --- a/pkg/network/standard/transport.go +++ b/pkg/network/standard/transport.go @@ -46,6 +46,8 @@ type transport struct { tls *tls.Config listenConfig *net.ListenConfig lock sync.Mutex + OnAccept func(conn network.Conn) context.Context + OnConnect func(ctx context.Context, conn network.Conn) context.Context } func (t *transport) serve() (err error) { @@ -62,18 +64,28 @@ func (t *transport) serve() (err error) { } hlog.SystemLogger().Infof("HERTZ: HTTP server listening on address=%s", t.ln.Addr().String()) for { + ctx := context.Background() conn, err := t.ln.Accept() var c network.Conn if err != nil { hlog.SystemLogger().Errorf("Error=%s", err.Error()) return err } + + if t.OnAccept != nil { + ctx = t.OnAccept(c) + } + if t.tls != nil { c = newTLSConn(tls.Server(conn, t.tls), t.readBufferSize) } else { c = newConn(conn, t.readBufferSize) } - go t.handler(context.Background(), c) + + if t.OnConnect != nil { + ctx = t.OnConnect(ctx, c) + } + go t.handler(ctx, c) } } @@ -111,5 +123,7 @@ func NewTransporter(options *config.Options) network.Transporter { readTimeout: options.ReadTimeout, tls: options.TLS, listenConfig: options.ListenConfig, + OnAccept: options.OnAccept, + OnConnect: options.OnConnect, } } From 7895df7682472c45c59ddc2ae4a9d9801bca0008 Mon Sep 17 00:00:00 2001 From: kinggo Date: Sat, 17 Dec 2022 12:22:51 +0800 Subject: [PATCH 17/19] optimize: support SetCustomFormValueFunc to define custom ctx.FormValue (#473) Co-authored-by: GuangyuFan <97507466+FGYFFFF@users.noreply.github.com> --- pkg/app/context.go | 106 +++++++++++++++++++++++++-------------- pkg/app/context_test.go | 31 ++++++++++++ pkg/route/engine.go | 14 ++++++ pkg/route/engine_test.go | 17 +++++++ 4 files changed, 129 insertions(+), 39 deletions(-) diff --git a/pkg/app/context.go b/pkg/app/context.go index 16797d747..17b9e4fcf 100644 --- a/pkg/app/context.go +++ b/pkg/app/context.go @@ -76,6 +76,51 @@ type Handler interface { ServeHTTP(c context.Context, ctx *RequestContext) } +type ClientIP func(ctx *RequestContext) string + +var defaultClientIP = func(ctx *RequestContext) string { + RemoteIPHeaders := []string{"X-Real-IP", "X-Forwarded-For"} + for _, headerName := range RemoteIPHeaders { + ip := ctx.Request.Header.Get(headerName) + if ip != "" { + return ip + } + } + + if ip, _, err := net.SplitHostPort(strings.TrimSpace(ctx.RemoteAddr().String())); err == nil { + return ip + } + + return "" +} + +// SetClientIPFunc sets ClientIP function implementation to get ClientIP. +// Deprecated: Use engine.SetClientIPFunc instead of SetClientIPFunc +func SetClientIPFunc(fn ClientIP) { + defaultClientIP = fn +} + +type FormValueFunc func(*RequestContext, string) []byte + +var defaultFormValue = func(ctx *RequestContext, key string) []byte { + v := ctx.QueryArgs().Peek(key) + if len(v) > 0 { + return v + } + v = ctx.PostArgs().Peek(key) + if len(v) > 0 { + return v + } + mf, err := ctx.MultipartForm() + if err == nil && mf.Value != nil { + vv := mf.Value[key] + if len(vv) > 0 { + return []byte(vv[0]) + } + } + return nil +} + type RequestContext struct { conn network.Conn Request protocol.Request @@ -108,6 +153,20 @@ type RequestContext struct { // enableTrace defines whether enable trace. enableTrace bool + + // clientIPFunc get client ip by use custom function. + clientIPFunc ClientIP + + // clientIPFunc get form value by use custom function. + formValueFunc FormValueFunc +} + +func (ctx *RequestContext) SetClientIPFunc(f ClientIP) { + ctx.clientIPFunc = f +} + +func (ctx *RequestContext) SetFormValueFunc(f FormValueFunc) { + ctx.formValueFunc = f } func (ctx *RequestContext) GetTraceInfo() traceinfo.TraceInfo { @@ -459,23 +518,12 @@ func (ctx *RequestContext) FormFile(name string) (*multipart.FileHeader, error) // * FormFile for obtaining uploaded files. // // The returned value is valid until returning from RequestHandler. +// Use engine.SetCustomFormValueFunc to change action of FormValue. func (ctx *RequestContext) FormValue(key string) []byte { - v := ctx.QueryArgs().Peek(key) - if len(v) > 0 { - return v - } - v = ctx.PostArgs().Peek(key) - if len(v) > 0 { - return v + if ctx.formValueFunc != nil { + return ctx.formValueFunc(ctx, key) } - mf, err := ctx.MultipartForm() - if err == nil && mf.Value != nil { - vv := mf.Value[key] - if len(vv) > 0 { - return []byte(vv[0]) - } - } - return nil + return defaultFormValue(ctx, key) } func (ctx *RequestContext) multipartFormValue(key string) (string, bool) { @@ -1056,33 +1104,13 @@ func (ctx *RequestContext) Body() ([]byte, error) { return ctx.Request.BodyE() } -type ClientIP func(ctx *RequestContext) string - -var defaultClientIP = func(ctx *RequestContext) string { - RemoteIPHeaders := []string{"X-Real-IP", "X-Forwarded-For"} - for _, headerName := range RemoteIPHeaders { - ip := ctx.Request.Header.Get(headerName) - if ip != "" { - return ip - } - } - - if ip, _, err := net.SplitHostPort(strings.TrimSpace(ctx.RemoteAddr().String())); err == nil { - return ip - } - - return "" -} - -// SetClientIPFunc sets ClientIP function implementation to get ClientIP. -func SetClientIPFunc(fn ClientIP) { - defaultClientIP = fn -} - // ClientIP tries to parse the headers in [X-Real-Ip, X-Forwarded-For]. // It calls RemoteIP() under the hood. If it cannot satisfy the requirements, -// use SetClientIPFunc to inject your own implementation. +// use engine.SetClientIPFunc to inject your own implementation. func (ctx *RequestContext) ClientIP() string { + if ctx.clientIPFunc != nil { + return ctx.clientIPFunc(ctx) + } return defaultClientIP(ctx) } diff --git a/pkg/app/context_test.go b/pkg/app/context_test.go index 6ae944372..4e62a9457 100644 --- a/pkg/app/context_test.go +++ b/pkg/app/context_test.go @@ -798,6 +798,37 @@ func TestRequestCtxFormValue(t *testing.T) { } } +func TestSetCustomFormValueFunc(t *testing.T) { + ctx := NewContext(0) + ctx.Request.SetRequestURI("/foo/bar?aaa=bbb") + ctx.Request.Header.SetContentTypeBytes([]byte("application/x-www-form-urlencoded")) + ctx.Request.SetBodyString("aaa=port") + + ctx.SetFormValueFunc(func(ctx *RequestContext, key string) []byte { + v := ctx.PostArgs().Peek(key) + if len(v) > 0 { + return v + } + mf, err := ctx.MultipartForm() + if err == nil && mf.Value != nil { + vv := mf.Value[key] + if len(vv) > 0 { + return []byte(vv[0]) + } + } + v = ctx.QueryArgs().Peek(key) + if len(v) > 0 { + return v + } + return nil + }) + + v := ctx.FormValue("aaa") + if string(v) != "port" { + t.Fatalf("unexpected value %q. Expecting %q", v, "port") + } +} + func TestContextSetGet(t *testing.T) { c := &RequestContext{} c.Set("foo", "bar") diff --git a/pkg/route/engine.go b/pkg/route/engine.go index 0096eb5e9..5fe8cb00b 100644 --- a/pkg/route/engine.go +++ b/pkg/route/engine.go @@ -187,6 +187,10 @@ type Engine struct { // Hook functions get triggered simultaneously when engine shutdown OnShutdown []CtxCallback + + // Custom Functions + clientIPFunc app.ClientIP + formValueFunc app.FormValueFunc } func (engine *Engine) IsTraceEnable() bool { @@ -706,6 +710,8 @@ func (engine *Engine) allocateContext() *app.RequestContext { ctx := engine.NewContext() ctx.Request.SetMaxKeepBodySize(engine.options.MaxKeepBodySize) ctx.Response.SetMaxKeepBodySize(engine.options.MaxKeepBodySize) + ctx.SetClientIPFunc(engine.clientIPFunc) + ctx.SetFormValueFunc(engine.formValueFunc) return ctx } @@ -856,6 +862,14 @@ func (engine *Engine) SetFuncMap(funcMap template.FuncMap) { engine.funcMap = funcMap } +func (engine *Engine) SetClientIPFunc(f app.ClientIP) { + engine.clientIPFunc = f +} + +func (engine *Engine) SetFormValueFunc(f app.FormValueFunc) { + engine.formValueFunc = f +} + // Delims sets template left and right delims and returns an Engine instance. func (engine *Engine) Delims(left, right string) *Engine { engine.delims = render.Delims{Left: left, Right: right} diff --git a/pkg/route/engine_test.go b/pkg/route/engine_test.go index 83ffc1032..e8a3dd550 100644 --- a/pkg/route/engine_test.go +++ b/pkg/route/engine_test.go @@ -466,6 +466,23 @@ func TestRenderHtmlOfGlobWithAutoRender(t *testing.T) { assert.DeepEqual(t, "text/html; charset=utf-8", rr.Header().Get("Content-Type")) } +func TestSetClientIPAndSetFormValue(t *testing.T) { + opt := config.NewOptions([]config.Option{}) + e := NewEngine(opt) + e.SetClientIPFunc(func(ctx *app.RequestContext) string { + return "1.1.1.1" + }) + e.SetFormValueFunc(func(requestContext *app.RequestContext, s string) []byte { + return []byte(s) + }) + e.GET("/ping", func(c context.Context, ctx *app.RequestContext) { + assert.DeepEqual(t, ctx.ClientIP(), "1.1.1.1") + assert.DeepEqual(t, string(ctx.FormValue("key")), "key") + }) + + _ = performRequest(e, "GET", "/ping") +} + func TestRenderHtmlOfFilesWithAutoRender(t *testing.T) { opt := config.NewOptions([]config.Option{}) opt.AutoReloadRender = true From f0733731b24a38422a7a3867efdb682f456fde21 Mon Sep 17 00:00:00 2001 From: "Asterisk L. Yuan" <92938836+L2ncE@users.noreply.github.com> Date: Tue, 20 Dec 2022 20:36:58 +0800 Subject: [PATCH 18/19] ci: add `Question` issue template and improve ci about invalid question. (#478) --- .github/ISSUE_TEMPLATE/question.md | 36 ++++++++++++++++++++++++++ .github/labels.json | 6 ++++- .github/workflows/invalid_question.yml | 2 +- 3 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/question.md diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md new file mode 100644 index 000000000..50b3c29e7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/question.md @@ -0,0 +1,36 @@ +--- +name: Question +about: Ask a question, so we can help you easily +title: '' +labels: '' +assignees: '' + +--- + +**Describe the Question** + +A clear and concise description of what the question is. + +**Reproducible Code** + +Please construct a minimum complete and reproducible example for us to get the same error. And tell us how to reproduce it like how you send a request or send what request. + +**Expected behavior** + +A clear and concise description of what you expected to happen. + +**Screenshots** + +If applicable, add screenshots to help explain your question. + +**Hertz version:** + +Please provide the version of Hertz you are using. + +**Environment:** + +The output of `go env`. + +**Additional context** + +Add any other context about the question here. diff --git a/.github/labels.json b/.github/labels.json index ca8ef3b74..9e7e890a7 100644 --- a/.github/labels.json +++ b/.github/labels.json @@ -8,7 +8,7 @@ }, "issue": { "invalid_issue": { - "requires": 2, + "requires": 3, "conditions": [ { "type": "descriptionMatches", @@ -17,6 +17,10 @@ { "type": "descriptionMatches", "pattern": "/^((?!Is your feature request related to a problem\\? Please describe).)*$/is" + }, + { + "type": "descriptionMatches", + "pattern": "/^((?!Describe the Question).)*$/is" } ] } diff --git a/.github/workflows/invalid_question.yml b/.github/workflows/invalid_question.yml index f465cd4d9..4be99f9d3 100644 --- a/.github/workflows/invalid_question.yml +++ b/.github/workflows/invalid_question.yml @@ -1,7 +1,7 @@ name: "Close Invalid Issue" on: schedule: - - cron: "*/10 * * * *" + - cron: "0 * * * *" permissions: contents: read From 257685e8ffcdedffbdc168f02fd00edb8abaa651 Mon Sep 17 00:00:00 2001 From: GuangyuFan <97507466+FGYFFFF@users.noreply.github.com> Date: Wed, 21 Dec 2022 10:46:09 +0800 Subject: [PATCH 19/19] feat(hz): hz generate client (#471) --- cmd/hz/app/app.go | 39 + cmd/hz/generator/client.go | 80 +++ cmd/hz/generator/package.go | 19 +- cmd/hz/generator/package_tpl.go | 679 +++++++++++++++++- cmd/hz/meta/const.go | 7 +- cmd/hz/protobuf/api/api.pb.go | 243 ++++--- cmd/hz/protobuf/api/api.proto | 5 + cmd/hz/protobuf/ast.go | 102 ++- cmd/hz/protobuf/plugin.go | 28 +- cmd/hz/thrift/ast.go | 127 +++- cmd/hz/thrift/plugin.go | 9 +- cmd/hz/thrift/tags.go | 23 +- .../{name_style.go => thriftgo_util.go} | 7 +- 13 files changed, 1227 insertions(+), 141 deletions(-) create mode 100644 cmd/hz/generator/client.go rename cmd/hz/thrift/{name_style.go => thriftgo_util.go} (81%) diff --git a/cmd/hz/app/app.go b/cmd/hz/app/app.go index d8b813931..89e57b5ab 100644 --- a/cmd/hz/app/app.go +++ b/cmd/hz/app/app.go @@ -100,6 +100,22 @@ func Model(c *cli.Context) error { return nil } +func Client(c *cli.Context) error { + args, err := globalArgs.Parse(c, meta.CmdClient) + if err != nil { + return cli.Exit(err, meta.LoadError) + } + setLogVerbose(args.Verbose) + logs.Debugf("Args: %#v\n", args) + + err = TriggerPlugin(args) + if err != nil { + return cli.Exit(err, meta.PluginError) + } + + return nil +} + func PluginMode() { pluginName := filepath.Base(os.Args[0]) if util.IsWindows() { @@ -240,6 +256,29 @@ func Init() *cli.App { }, Action: Model, }, + { + Name: meta.CmdClient, + Usage: "Generate hertz client based on IDL", + Flags: []cli.Flag{ + &idlFlag, + &moduleFlag, + &modelDirFlag, + + &includesFlag, + &thriftOptionsFlag, + &protoOptionsFlag, + &noRecurseFlag, + + &jsonEnumStrFlag, + &unsetOmitemptyFlag, + &protoCamelJSONTag, + &snakeNameFlag, + &excludeFilesFlag, + &protoPluginsFlag, + &thriftPluginsFlag, + }, + Action: Client, + }, } return app } diff --git a/cmd/hz/generator/client.go b/cmd/hz/generator/client.go new file mode 100644 index 000000000..e3320e6cd --- /dev/null +++ b/cmd/hz/generator/client.go @@ -0,0 +1,80 @@ +/* + * Copyright 2022 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package generator + +import ( + "path/filepath" + + "github.com/cloudwego/hertz/cmd/hz/generator/model" + "github.com/cloudwego/hertz/cmd/hz/util" +) + +type ClientMethod struct { + *HttpMethod + BodyParamsCode string + QueryParamsCode string + PathParamsCode string + HeaderParamsCode string + FormValueCode string + FormFileCode string +} + +type ClientFile struct { + FilePath string + ServiceName string + BaseDomain string + Imports map[string]*model.Model + ClientMethods []*ClientMethod +} + +func (pkgGen *HttpPackageGenerator) genClient(pkg *HttpPackage, clientDir string) error { + for _, s := range pkg.Services { + hertzClientPath := filepath.Join(clientDir, "hertz_client.go") + isExist, err := util.PathExist(hertzClientPath) + if err != nil { + return err + } + if !isExist { + err := pkgGen.TemplateGenerator.Generate(nil, hertzClientTplName, hertzClientPath, false) + if err != nil { + return err + } + } + client := ClientFile{ + FilePath: filepath.Join(clientDir, util.ToSnakeCase(s.Name)+".go"), + ServiceName: util.ToSnakeCase(s.Name), + ClientMethods: s.ClientMethods, + BaseDomain: s.BaseDomain, + } + client.Imports = make(map[string]*model.Model, len(client.ClientMethods)) + for _, m := range client.ClientMethods { + // Iterate over the request and return parameters of the method to get import path. + for key, mm := range m.Models { + if v, ok := client.Imports[mm.PackageName]; ok && v.Package != mm.Package { + client.Imports[key] = mm + continue + } + client.Imports[mm.PackageName] = mm + } + } + err = pkgGen.TemplateGenerator.Generate(client, serviceClientName, client.FilePath, false) + if err != nil { + return err + } + } + return nil +} diff --git a/cmd/hz/generator/package.go b/cmd/hz/generator/package.go index 6901e814d..972dc7985 100644 --- a/cmd/hz/generator/package.go +++ b/cmd/hz/generator/package.go @@ -38,20 +38,24 @@ type HttpPackage struct { } type Service struct { - Name string - Methods []*HttpMethod - Models []*model.Model // all dependency models + Name string + Methods []*HttpMethod + ClientMethods []*ClientMethod + Models []*model.Model // all dependency models + BaseDomain string // base domain for client code } type HttpPackageGenerator struct { ConfigPath string Backend meta.Backend Options []Option + CmdType string ProjPackage string HandlerDir string RouterDir string ModelDir string ClientDir string + IdlClientDir string NeedModel bool HandlerByMethod bool @@ -136,6 +140,15 @@ func (pkgGen *HttpPackageGenerator) Generate(pkg *HttpPackage) error { } } + if pkgGen.CmdType == meta.CmdClient { + clientDir := pkgGen.IdlClientDir + clientDir = util.SubDir(clientDir, "hertz") + if err := pkgGen.genClient(pkg, clientDir); err != nil { + return err + } + return nil + } + // this is for handler_by_service, the handler_dir is {$HANDLER_DIR}/{$PKG} handlerDir := util.SubDir(pkgGen.HandlerDir, pkg.Package) if pkgGen.HandlerByMethod { diff --git a/cmd/hz/generator/package_tpl.go b/cmd/hz/generator/package_tpl.go index b089fba97..ac2197b99 100644 --- a/cmd/hz/generator/package_tpl.go +++ b/cmd/hz/generator/package_tpl.go @@ -24,7 +24,9 @@ var ( handlerSingleTplName = "handler_single.go" modelTplName = "model.go" registerTplName = "register.go" - clientTplName = "client.go" + clientTplName = "client.go" // generate a default client for server + hertzClientTplName = "hertz_client.go" // underlying client for client command + serviceClientName = "client.go" // client of service for quick call insertPointNew = "//INSERT_POINT: DO NOT DELETE THIS LINE!" insertPointPatternNew = `//INSERT_POINT\: DO NOT DELETE THIS LINE\!` @@ -39,6 +41,8 @@ var templateNameSet = map[string]string{ modelTplName: modelTplName, registerTplName: registerTplName, clientTplName: clientTplName, + hertzClientTplName: hertzClientTplName, + serviceClientName: serviceClientName, } func IsDefaultTpl(name string) bool { @@ -243,5 +247,678 @@ func {{.MiddleWare}}Mw() []app.HandlerFunc { } `, }, + { + Path: defaultRouterDir + sp + hertzClientTplName, + Delims: [2]string{"{{", "}}"}, + Body: hertzClientTpl, + }, + { + Path: defaultRouterDir + sp + serviceClientName, + Delims: [2]string{"{{", "}}"}, + Body: serviceClientTpl, + }, }, } + +var hertzClientTpl = `// Code generated by hz. + +package hertz + +import ( + "context" + "encoding/json" + "encoding/xml" + "fmt" + "io" + "net/http" + "net/url" + "reflect" + "regexp" + "strings" + + hertz_client "github.com/cloudwego/hertz/pkg/app/client" + "github.com/cloudwego/hertz/pkg/common/config" + "github.com/cloudwego/hertz/pkg/common/errors" + "github.com/cloudwego/hertz/pkg/protocol" + "github.com/cloudwego/hertz/pkg/protocol/client" +) + +type use interface { + Use(mws ...hertz_client.Middleware) +} + +// Definition of global data and types. +type ResponseResultDecider func(statusCode int, rawResponse *protocol.Response) (isError bool) +type bindRequestBodyFunc func(c *cli, r *request) (contentType string, body io.Reader, err error) +type beforeRequestFunc func(*cli, *request) error +type afterResponseFunc func(*cli, *response) error + +var ( + hdrContentTypeKey = http.CanonicalHeaderKey("Content-Type") + hdrContentEncodingKey = http.CanonicalHeaderKey("Content-Encoding") + + plainTextType = "text/plain; charset=utf-8" + jsonContentType = "application/json; charset=utf-8" + formContentType = "multipart/form-data" + + jsonCheck = regexp.MustCompile(` + "`(?i:(application|text)/(json|.*\\+json|json\\-.*)(; |$))`)\n" + + `xmlCheck = regexp.MustCompile(` + "`(?i:(application|text)/(xml|.*\\+xml)(; |$))`)\n" + + ` +) + +// Configuration of client +type Option struct { + f func(*Options) +} + +type Options struct { + hostUrl string + doer client.Doer + header http.Header + requestBodyBind bindRequestBodyFunc + responseResultDecider ResponseResultDecider + middlewares []hertz_client.Middleware + clientOption []config.ClientOption +} + +func getOptions(ops ...Option) *Options { + opts := &Options{} + for _, do := range ops { + do.f(opts) + } + return opts +} + +// WithHertzClientOption is used to pass configuration for the hertz client +func WithHertzClientOption(opt ...config.ClientOption) Option { + return Option{func(op *Options) { + op.clientOption = append(op.clientOption, opt...) + }} +} + +// WithHertzClientMiddleware is used to register the middleware for the hertz client +func WithHertzClientMiddleware(mws ...hertz_client.Middleware) Option { + return Option{func(op *Options) { + op.middlewares = append(op.middlewares, mws...) + }} +} + +// WithHertzClient is used to register a custom hertz client +func WithHertzClient(client client.Doer) Option { + return Option{func(op *Options) { + op.doer = client + }} +} + +// WithHeader is used to add the default header, which is carried by every request +func WithHeader(header http.Header) Option { + return Option{func(op *Options) { + op.header = header + }} +} + +// WithResponseResultDecider configure custom deserialization of http response to response struct +func WithResponseResultDecider(decider ResponseResultDecider) Option { + return Option{func(op *Options) { + op.responseResultDecider = decider + }} +} + +func withHostUrl(HostUrl string) Option { + return Option{func(op *Options) { + op.hostUrl = HostUrl + }} +} + +// underlying client +type cli struct { + hostUrl string + doer client.Doer + header http.Header + bindRequestBody bindRequestBodyFunc + responseResultDecider ResponseResultDecider + + beforeRequest []beforeRequestFunc + afterResponse []afterResponseFunc +} + +func (c *cli) Use(mws ...hertz_client.Middleware) error { + u, ok := c.doer.(use) + if !ok { + return errors.NewPublic("doer does not support middleware, choose the right doer.") + } + u.Use(mws...) + return nil +} + +func newClient(opts *Options) (*cli, error) { + if opts.requestBodyBind == nil { + opts.requestBodyBind = defaultRequestBodyBind + } + if opts.responseResultDecider == nil { + opts.responseResultDecider = defaultResponseResultDecider + } + if opts.doer == nil { + cli, err := hertz_client.NewClient(opts.clientOption...) + if err != nil { + return nil, err + } + opts.doer = cli + } + + c := &cli{ + hostUrl: opts.hostUrl, + doer: opts.doer, + bindRequestBody: opts.requestBodyBind, + responseResultDecider: opts.responseResultDecider, + beforeRequest: []beforeRequestFunc{ + parseRequestURL, + parseRequestHeader, + createHTTPRequest, + }, + afterResponse: []afterResponseFunc{ + parseResponseBody, + }, + } + + if len(opts.middlewares) != 0 { + if err := c.Use(opts.middlewares...); err != nil { + return nil, err + } + } + return c, nil +} + +func (c *cli) execute(req *request) (*response, error) { + var err error + for _, f := range c.beforeRequest { + if err = f(c, req); err != nil { + return nil, err + } + } + + if hostHeader := req.header.Get("Host"); hostHeader != "" { + req.rawRequest.Header.SetHost(hostHeader) + } + + resp := protocol.Response{} + + err = c.doer.Do(req.ctx, req.rawRequest, &resp) + + response := &response{ + request: req, + rawResponse: &resp, + } + + if err != nil { + return response, err + } + + body, err := resp.BodyE() + if err != nil { + return nil, err + } + + if strings.EqualFold(resp.Header.Get(hdrContentEncodingKey), "gzip") && resp.Header.ContentLength() != 0 { + body, err = resp.BodyGunzip() + if err != nil { + return nil, err + } + } + + response.bodyByte = body + + response.size = int64(len(response.bodyByte)) + + // Apply Response middleware + for _, f := range c.afterResponse { + if err = f(c, response); err != nil { + break + } + } + + return response, err +} + +// r get request +func (c *cli) r() *request { + return &request{ + queryParam: url.Values{}, + header: http.Header{}, + pathParam: map[string]string{}, + formParam: map[string]string{}, + fileParam: map[string]string{}, + client: c, + } +} + +type response struct { + request *request + rawResponse *protocol.Response + + bodyByte []byte + size int64 +} + +// statusCode method returns the HTTP status code for the executed request. +func (r *response) statusCode() int { + if r.rawResponse == nil { + return 0 + } + + return r.rawResponse.StatusCode() +} + +// body method returns HTTP response as []byte array for the executed request. +func (r *response) body() []byte { + if r.rawResponse == nil { + return []byte{} + } + return r.bodyByte +} + +// Header method returns the response headers +func (r *response) header() http.Header { + if r.rawResponse == nil { + return http.Header{} + } + h := http.Header{} + r.rawResponse.Header.VisitAll(func(key, value []byte) { + h.Add(string(key), string(value)) + }) + + return h +} + +type request struct { + client *cli + url string + method string + queryParam url.Values + header http.Header + pathParam map[string]string + formParam map[string]string + fileParam map[string]string + bodyParam interface{} + rawRequest *protocol.Request + ctx context.Context + requestOptions []config.RequestOption + result interface{} + Error interface{} +} + +func (r *request) setContext(ctx context.Context) *request { + r.ctx = ctx + return r +} + +func (r *request) context() context.Context { + return r.ctx +} + +func (r *request) setHeader(header, value string) *request { + r.header.Set(header, value) + return r +} + +func (r *request) setQueryParam(param string, value interface{}) *request { + v := reflect.ValueOf(value) + switch v.Kind() { + case reflect.Slice, reflect.Array: + for index := 0; index < v.Len(); index++ { + r.queryParam.Add(param, fmt.Sprint(v.Index(index).Interface())) + } + default: + r.queryParam.Set(param, fmt.Sprint(v)) + } + return r +} + +func (r *request) setResult(res interface{}) *request { + r.result = res + return r +} + +func (r *request) setError(err interface{}) *request { + r.Error = err + return r +} + +func (r *request) setHeaders(headers map[string]string) *request { + for h, v := range headers { + r.setHeader(h, v) + } + + return r +} + +func (r *request) setQueryParams(params map[string]interface{}) *request { + for p, v := range params { + r.setQueryParam(p, v) + } + + return r +} + +func (r *request) setPathParams(params map[string]string) *request { + for p, v := range params { + r.pathParam[p] = v + } + return r +} + +func (r *request) setFormParams(params map[string]string) *request { + for p, v := range params { + r.formParam[p] = v + } + return r +} + +func (r *request) setFormFileParams(params map[string]string) *request { + for p, v := range params { + r.fileParam[p] = v + } + return r +} + +func (r *request) setBodyParam(body interface{}) *request { + r.bodyParam = body + return r +} + +func (r *request) setRequestOption(option ...config.RequestOption) *request { + r.requestOptions = append(r.requestOptions, option...) + return r +} + +func (r *request) execute(method, url string) (*response, error) { + r.method = method + r.url = url + return r.client.execute(r) +} + +func parseRequestURL(c *cli, r *request) error { + if len(r.pathParam) > 0 { + for p, v := range r.pathParam { + r.url = strings.Replace(r.url, ":"+p, url.PathEscape(v), -1) + } + } + + // Parsing request URL + reqURL, err := url.Parse(r.url) + if err != nil { + return err + } + + // If request.URL is relative path then added c.HostURL into + // the request URL otherwise request.URL will be used as-is + if !reqURL.IsAbs() { + r.url = reqURL.String() + if len(r.url) > 0 && r.url[0] != '/' { + r.url = "/" + r.url + } + + reqURL, err = url.Parse(c.hostUrl + r.url) + if err != nil { + return err + } + } + + // Adding Query Param + query := make(url.Values) + + for k, v := range r.queryParam { + // remove query param from client level by key + // since overrides happens for that key in the request + query.Del(k) + for _, iv := range v { + query.Add(k, iv) + } + } + + if len(query) > 0 { + if isStringEmpty(reqURL.RawQuery) { + reqURL.RawQuery = query.Encode() + } else { + reqURL.RawQuery = reqURL.RawQuery + "&" + query.Encode() + } + } + + r.url = reqURL.String() + + return nil +} + +func isStringEmpty(str string) bool { + return len(strings.TrimSpace(str)) == 0 +} + +func parseRequestHeader(c *cli, r *request) error { + hdr := make(http.Header) + if c.header != nil { + for k := range c.header { + hdr[k] = append(hdr[k], c.header[k]...) + } + } + + for k := range r.header { + hdr.Del(k) + hdr[k] = append(hdr[k], r.header[k]...) + } + + if len(r.formParam) != 0 && len(r.fileParam) != 0 { + hdr.Add(hdrContentTypeKey, formContentType) + } + + r.header = hdr + return nil +} + +// detectContentType method is used to figure out "request.Body" content type for request header +func detectContentType(body interface{}) string { + contentType := plainTextType + kind := reflect.Indirect(reflect.ValueOf(body)).Kind() + switch kind { + case reflect.Struct, reflect.Map: + contentType = jsonContentType + case reflect.String: + contentType = plainTextType + default: + if b, ok := body.([]byte); ok { + contentType = http.DetectContentType(b) + } else if kind == reflect.Slice { + contentType = jsonContentType + } + } + + return contentType +} + +func defaultRequestBodyBind(c *cli, r *request) (contentType string, body io.Reader, err error) { + if !isPayloadSupported(r.method) { + return + } + var bodyBytes []byte + contentType = r.header.Get(hdrContentTypeKey) + if isStringEmpty(contentType) { + contentType = detectContentType(r.bodyParam) + r.header.Set(hdrContentTypeKey, contentType) + } + kind := reflect.Indirect(reflect.ValueOf(r.bodyParam)).Kind() + if isJSONType(contentType) && + (kind == reflect.Struct || kind == reflect.Map || kind == reflect.Slice) { + bodyBytes, err = json.Marshal(r.bodyParam) + } else if isXMLType(contentType) && (kind == reflect.Struct) { + bodyBytes, err = xml.Marshal(r.bodyParam) + } + if err != nil { + return + } + return contentType, strings.NewReader(string(bodyBytes)), nil +} + +func isPayloadSupported(m string) bool { + return !(m == http.MethodHead || m == http.MethodOptions || m == http.MethodGet || m == http.MethodDelete) +} + +func createHTTPRequest(c *cli, r *request) (err error) { + contentType, body, err := c.bindRequestBody(c, r) + if !isStringEmpty(contentType) { + r.header.Set(hdrContentTypeKey, contentType) + } + if err == nil { + r.rawRequest = protocol.NewRequest(r.method, r.url, body) + if contentType == formContentType && isPayloadSupported(r.method) { + if r.rawRequest.IsBodyStream() { + r.rawRequest.ResetBody() + } + r.rawRequest.SetMultipartFormData(r.formParam) + r.rawRequest.SetFiles(r.fileParam) + } + for key, values := range r.header { + for _, val := range values { + r.rawRequest.Header.Add(key, val) + } + } + r.rawRequest.SetOptions(r.requestOptions...) + } + return err +} + +func silently(_ ...interface{}) {} + +// defaultResponseResultDecider method returns true if HTTP status code >= 400 otherwise false. +func defaultResponseResultDecider(statusCode int, rawResponse *protocol.Response) bool { + return statusCode > 399 +} + +// IsJSONType method is to check JSON content type or not +func isJSONType(ct string) bool { + return jsonCheck.MatchString(ct) +} + +// IsXMLType method is to check XML content type or not +func isXMLType(ct string) bool { + return xmlCheck.MatchString(ct) +} + +func parseResponseBody(c *cli, res *response) (err error) { + if res.statusCode() == http.StatusNoContent { + return + } + // Handles only JSON or XML content type + ct := res.header().Get(hdrContentTypeKey) + + isError := c.responseResultDecider(res.statusCode(), res.rawResponse) + if isError { + if res.request.Error != nil { + if isJSONType(ct) || isXMLType(ct) { + err = unmarshalContent(ct, res.bodyByte, res.request.Error) + } + } else { + jsonByte, jsonErr := json.Marshal(map[string]interface{}{ + "status_code": res.rawResponse.StatusCode, + "body": string(res.bodyByte), + }) + if jsonErr != nil { + return jsonErr + } + err = fmt.Errorf(string(jsonByte)) + } + } else if res.request.result != nil { + if isJSONType(ct) || isXMLType(ct) { + err = unmarshalContent(ct, res.bodyByte, res.request.result) + return + } + } + return +} + +// unmarshalContent content into object from JSON or XML +func unmarshalContent(ct string, b []byte, d interface{}) (err error) { + if isJSONType(ct) { + err = json.Unmarshal(b, d) + } else if isXMLType(ct) { + err = xml.Unmarshal(b, d) + } + + return +} + +` + +var serviceClientTpl = `// Code generated by hertz generator. + +package hertz + +import ( + "context" + + "github.com/cloudwego/hertz/pkg/common/config" + "github.com/cloudwego/hertz/pkg/protocol" +{{- range $k, $v := .Imports}} + {{$k}} "{{$v.Package}}" +{{- end}} +) + +type Client interface { + {{range $_, $MethodInfo := .ClientMethods}} + {{$MethodInfo.Name}}(context context.Context, req *{{$MethodInfo.RequestTypeName}}, reqOpt ...config.RequestOption) (resp *{{$MethodInfo.ReturnTypeName}}, rawResponse *protocol.Response, err error) + {{end}} +} + +type {{.ServiceName}}Client struct { + client *cli +} + +func New{{.ServiceName}}Client(hostUrl string, ops ...Option) (Client, error) { + opts := getOptions(append(ops, withHostUrl(hostUrl))...) + cli, err := newClient(opts) + if err != nil { + return nil, err + } + return &{{.ServiceName}}Client{ + client: cli, + }, nil +} + +{{range $_, $MethodInfo := .ClientMethods}} +func (s *{{$.ServiceName}}Client) {{$MethodInfo.Name}}(context context.Context, req *{{$MethodInfo.RequestTypeName}}, reqOpt ...config.RequestOption) (resp *{{$MethodInfo.ReturnTypeName}}, rawResponse *protocol.Response, err error) { + httpResp := &{{$MethodInfo.ReturnTypeName}}{} + ret, err := s.client.r(). + setContext(context). + setQueryParams(map[string]interface{}{ + {{$MethodInfo.QueryParamsCode}} + }). + setPathParams(map[string]string{ + {{$MethodInfo.PathParamsCode}} + }). + setHeaders(map[string]string{ + {{$MethodInfo.HeaderParamsCode}} + }). + setFormParams(map[string]string{ + {{$MethodInfo.FormValueCode}} + }). + setFormFileParams(map[string]string{ + {{$MethodInfo.FormFileCode}} + }). + {{$MethodInfo.BodyParamsCode}} + setRequestOption(reqOpt...). + setResult(httpResp). + execute("{{$MethodInfo.HTTPMethod}}", "{{$MethodInfo.Path}}") + if err == nil { + resp = httpResp + } + rawResponse = ret.rawResponse + return resp, rawResponse, err +} +{{end}} + +var defaultClient, _ = New{{.ServiceName}}Client("{{.BaseDomain}}") + +{{range $_, $MethodInfo := .ClientMethods}} +func {{$MethodInfo.Name}}(context context.Context, req *{{$MethodInfo.RequestTypeName}}, reqOpt ...config.RequestOption) (resp *{{$MethodInfo.ReturnTypeName}}, rawResponse *protocol.Response, err error) { + return defaultClient.{{$MethodInfo.Name}}(context, req, reqOpt...) +} +{{end}} +` diff --git a/cmd/hz/meta/const.go b/cmd/hz/meta/const.go index e5a889198..8259b583f 100644 --- a/cmd/hz/meta/const.go +++ b/cmd/hz/meta/const.go @@ -34,7 +34,7 @@ const ( CmdUpdate = "update" CmdNew = "new" CmdModel = "model" - // CmdClient = "client" + CmdClient = "client" ) // hz IDLs @@ -76,3 +76,8 @@ type Backend string const ( BackendGolang Backend = "golang" ) + +// template const value +const ( + SetBodyParam = "setBodyParam(req).\n" +) diff --git a/cmd/hz/protobuf/api/api.pb.go b/cmd/hz/protobuf/api/api.pb.go index 7fa96fe17..219a0133e 100644 --- a/cmd/hz/protobuf/api/api.pb.go +++ b/cmd/hz/protobuf/api/api.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.28.0 +// protoc-gen-go v1.28.1 // protoc v3.19.4 // source: api.proto @@ -101,6 +101,14 @@ var file_api_proto_extTypes = []protoimpl.ExtensionInfo{ Tag: "bytes,50109,opt,name=js_conv", Filename: "api.proto", }, + { + ExtendedType: (*descriptorpb.FieldOptions)(nil), + ExtensionType: (*string)(nil), + Field: 50110, + Name: "api.file_name", + Tag: "bytes,50110,opt,name=file_name", + Filename: "api.proto", + }, { ExtendedType: (*descriptorpb.MethodOptions)(nil), ExtensionType: (*string)(nil), @@ -245,6 +253,14 @@ var file_api_proto_extTypes = []protoimpl.ExtensionInfo{ Tag: "varint,50401,opt,name=http_code", Filename: "api.proto", }, + { + ExtendedType: (*descriptorpb.ServiceOptions)(nil), + ExtensionType: (*string)(nil), + Field: 50402, + Name: "api.base_domain", + Tag: "bytes,50402,opt,name=base_domain", + Filename: "api.proto", + }, } // Extension fields to descriptorpb.FieldOptions. @@ -269,50 +285,58 @@ var ( E_GoTag = &file_api_proto_extTypes[8] // optional string js_conv = 50109; E_JsConv = &file_api_proto_extTypes[9] + // optional string file_name = 50110; + E_FileName = &file_api_proto_extTypes[10] ) // Extension fields to descriptorpb.MethodOptions. var ( // optional string get = 50201; - E_Get = &file_api_proto_extTypes[10] + E_Get = &file_api_proto_extTypes[11] // optional string post = 50202; - E_Post = &file_api_proto_extTypes[11] + E_Post = &file_api_proto_extTypes[12] // optional string put = 50203; - E_Put = &file_api_proto_extTypes[12] + E_Put = &file_api_proto_extTypes[13] // optional string delete = 50204; - E_Delete = &file_api_proto_extTypes[13] + E_Delete = &file_api_proto_extTypes[14] // optional string patch = 50205; - E_Patch = &file_api_proto_extTypes[14] + E_Patch = &file_api_proto_extTypes[15] // optional string options = 50206; - E_Options = &file_api_proto_extTypes[15] + E_Options = &file_api_proto_extTypes[16] // optional string head = 50207; - E_Head = &file_api_proto_extTypes[16] + E_Head = &file_api_proto_extTypes[17] // optional string any = 50208; - E_Any = &file_api_proto_extTypes[17] + E_Any = &file_api_proto_extTypes[18] // optional string gen_path = 50301; - E_GenPath = &file_api_proto_extTypes[18] // The path specified by the user when the client code is generated, with a higher priority than api_version + E_GenPath = &file_api_proto_extTypes[19] // The path specified by the user when the client code is generated, with a higher priority than api_version // optional string api_version = 50302; - E_ApiVersion = &file_api_proto_extTypes[19] // Specify the value of the :version variable in path when the client code is generated + E_ApiVersion = &file_api_proto_extTypes[20] // Specify the value of the :version variable in path when the client code is generated // optional string tag = 50303; - E_Tag = &file_api_proto_extTypes[20] // rpc tag, can be multiple, separated by commas + E_Tag = &file_api_proto_extTypes[21] // rpc tag, can be multiple, separated by commas // optional string name = 50304; - E_Name = &file_api_proto_extTypes[21] // Name of rpc + E_Name = &file_api_proto_extTypes[22] // Name of rpc // optional string api_level = 50305; - E_ApiLevel = &file_api_proto_extTypes[22] // Interface Level + E_ApiLevel = &file_api_proto_extTypes[23] // Interface Level // optional string serializer = 50306; - E_Serializer = &file_api_proto_extTypes[23] // Serialization method + E_Serializer = &file_api_proto_extTypes[24] // Serialization method // optional string param = 50307; - E_Param = &file_api_proto_extTypes[24] // Whether client requests take public parameters + E_Param = &file_api_proto_extTypes[25] // Whether client requests take public parameters // optional string baseurl = 50308; - E_Baseurl = &file_api_proto_extTypes[25] // Baseurl used in ttnet routing + E_Baseurl = &file_api_proto_extTypes[26] // Baseurl used in ttnet routing // optional string handler_path = 50309; - E_HandlerPath = &file_api_proto_extTypes[26] // handler_path specifies the path to generate the method + E_HandlerPath = &file_api_proto_extTypes[27] // handler_path specifies the path to generate the method ) // Extension fields to descriptorpb.EnumValueOptions. var ( // optional int32 http_code = 50401; - E_HttpCode = &file_api_proto_extTypes[27] + E_HttpCode = &file_api_proto_extTypes[28] +) + +// Extension fields to descriptorpb.ServiceOptions. +var ( + // optional string base_domain = 50402; + E_BaseDomain = &file_api_proto_extTypes[29] ) var File_api_proto protoreflect.FileDescriptor @@ -355,79 +379,88 @@ var file_api_proto_rawDesc = []byte{ 0x38, 0x0a, 0x07, 0x6a, 0x73, 0x5f, 0x63, 0x6f, 0x6e, 0x76, 0x12, 0x1d, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0xbd, 0x87, 0x03, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x06, 0x6a, 0x73, 0x43, 0x6f, 0x6e, 0x76, 0x3a, 0x32, 0x0a, 0x03, 0x67, 0x65, 0x74, + 0x09, 0x52, 0x06, 0x6a, 0x73, 0x43, 0x6f, 0x6e, 0x76, 0x3a, 0x3c, 0x0a, 0x09, 0x66, 0x69, 0x6c, + 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1d, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x4f, 0x70, + 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0xbe, 0x87, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x66, + 0x69, 0x6c, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x3a, 0x32, 0x0a, 0x03, 0x67, 0x65, 0x74, 0x12, 0x1e, + 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, + 0x2e, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x99, + 0x88, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x67, 0x65, 0x74, 0x3a, 0x34, 0x0a, 0x04, 0x70, + 0x6f, 0x73, 0x74, 0x12, 0x1e, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x4f, 0x70, 0x74, 0x69, + 0x6f, 0x6e, 0x73, 0x18, 0x9a, 0x88, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x6f, 0x73, + 0x74, 0x3a, 0x32, 0x0a, 0x03, 0x70, 0x75, 0x74, 0x12, 0x1e, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, + 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x4d, 0x65, 0x74, 0x68, 0x6f, + 0x64, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x9b, 0x88, 0x03, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x03, 0x70, 0x75, 0x74, 0x3a, 0x38, 0x0a, 0x06, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x12, + 0x1e, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, + 0x66, 0x2e, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, + 0x9c, 0x88, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x3a, + 0x36, 0x0a, 0x05, 0x70, 0x61, 0x74, 0x63, 0x68, 0x12, 0x1e, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, + 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x4d, 0x65, 0x74, 0x68, 0x6f, + 0x64, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x9d, 0x88, 0x03, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x05, 0x70, 0x61, 0x74, 0x63, 0x68, 0x3a, 0x3a, 0x0a, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, + 0x6e, 0x73, 0x12, 0x1e, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x4f, 0x70, 0x74, 0x69, 0x6f, + 0x6e, 0x73, 0x18, 0x9e, 0x88, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6f, 0x70, 0x74, 0x69, + 0x6f, 0x6e, 0x73, 0x3a, 0x34, 0x0a, 0x04, 0x68, 0x65, 0x61, 0x64, 0x12, 0x1e, 0x2e, 0x67, 0x6f, + 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x4d, 0x65, + 0x74, 0x68, 0x6f, 0x64, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x9f, 0x88, 0x03, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x04, 0x68, 0x65, 0x61, 0x64, 0x3a, 0x32, 0x0a, 0x03, 0x61, 0x6e, 0x79, 0x12, 0x1e, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, - 0x18, 0x99, 0x88, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x67, 0x65, 0x74, 0x3a, 0x34, 0x0a, - 0x04, 0x70, 0x6f, 0x73, 0x74, 0x12, 0x1e, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x4f, 0x70, - 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x9a, 0x88, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, - 0x6f, 0x73, 0x74, 0x3a, 0x32, 0x0a, 0x03, 0x70, 0x75, 0x74, 0x12, 0x1e, 0x2e, 0x67, 0x6f, 0x6f, - 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x4d, 0x65, 0x74, - 0x68, 0x6f, 0x64, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x9b, 0x88, 0x03, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x03, 0x70, 0x75, 0x74, 0x3a, 0x38, 0x0a, 0x06, 0x64, 0x65, 0x6c, 0x65, 0x74, - 0x65, 0x12, 0x1e, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x62, 0x75, 0x66, 0x2e, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, - 0x73, 0x18, 0x9c, 0x88, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x64, 0x65, 0x6c, 0x65, 0x74, - 0x65, 0x3a, 0x36, 0x0a, 0x05, 0x70, 0x61, 0x74, 0x63, 0x68, 0x12, 0x1e, 0x2e, 0x67, 0x6f, 0x6f, - 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x4d, 0x65, 0x74, - 0x68, 0x6f, 0x64, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x9d, 0x88, 0x03, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x05, 0x70, 0x61, 0x74, 0x63, 0x68, 0x3a, 0x3a, 0x0a, 0x07, 0x6f, 0x70, 0x74, - 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x1e, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, + 0x18, 0xa0, 0x88, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x61, 0x6e, 0x79, 0x3a, 0x3b, 0x0a, + 0x08, 0x67, 0x65, 0x6e, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x12, 0x1e, 0x2e, 0x67, 0x6f, 0x6f, 0x67, + 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x4d, 0x65, 0x74, 0x68, + 0x6f, 0x64, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0xfd, 0x88, 0x03, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x07, 0x67, 0x65, 0x6e, 0x50, 0x61, 0x74, 0x68, 0x3a, 0x41, 0x0a, 0x0b, 0x61, 0x70, + 0x69, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1e, 0x2e, 0x67, 0x6f, 0x6f, 0x67, + 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x4d, 0x65, 0x74, 0x68, + 0x6f, 0x64, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0xfe, 0x88, 0x03, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0a, 0x61, 0x70, 0x69, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x3a, 0x32, 0x0a, + 0x03, 0x74, 0x61, 0x67, 0x12, 0x1e, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x4f, 0x70, 0x74, - 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x9e, 0x88, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6f, 0x70, - 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x3a, 0x34, 0x0a, 0x04, 0x68, 0x65, 0x61, 0x64, 0x12, 0x1e, 0x2e, - 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, - 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x9f, 0x88, - 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x68, 0x65, 0x61, 0x64, 0x3a, 0x32, 0x0a, 0x03, 0x61, - 0x6e, 0x79, 0x12, 0x1e, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x4f, 0x70, 0x74, 0x69, 0x6f, - 0x6e, 0x73, 0x18, 0xa0, 0x88, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x61, 0x6e, 0x79, 0x3a, - 0x3b, 0x0a, 0x08, 0x67, 0x65, 0x6e, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x12, 0x1e, 0x2e, 0x67, 0x6f, - 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x4d, 0x65, - 0x74, 0x68, 0x6f, 0x64, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0xfd, 0x88, 0x03, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x07, 0x67, 0x65, 0x6e, 0x50, 0x61, 0x74, 0x68, 0x3a, 0x41, 0x0a, 0x0b, - 0x61, 0x70, 0x69, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1e, 0x2e, 0x67, 0x6f, - 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x4d, 0x65, - 0x74, 0x68, 0x6f, 0x64, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0xfe, 0x88, 0x03, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x0a, 0x61, 0x70, 0x69, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x3a, - 0x32, 0x0a, 0x03, 0x74, 0x61, 0x67, 0x12, 0x1e, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x4f, - 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0xff, 0x88, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, - 0x74, 0x61, 0x67, 0x3a, 0x34, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1e, 0x2e, 0x67, 0x6f, + 0x69, 0x6f, 0x6e, 0x73, 0x18, 0xff, 0x88, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x74, 0x61, + 0x67, 0x3a, 0x34, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1e, 0x2e, 0x67, 0x6f, 0x6f, 0x67, + 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x4d, 0x65, 0x74, 0x68, + 0x6f, 0x64, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x80, 0x89, 0x03, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x3a, 0x3d, 0x0a, 0x09, 0x61, 0x70, 0x69, 0x5f, 0x6c, + 0x65, 0x76, 0x65, 0x6c, 0x12, 0x1e, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x4f, 0x70, 0x74, + 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x81, 0x89, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x61, 0x70, + 0x69, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x3a, 0x40, 0x0a, 0x0a, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, + 0x69, 0x7a, 0x65, 0x72, 0x12, 0x1e, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x4f, 0x70, 0x74, + 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x82, 0x89, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, 0x65, + 0x72, 0x69, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x72, 0x3a, 0x36, 0x0a, 0x05, 0x70, 0x61, 0x72, 0x61, + 0x6d, 0x12, 0x1e, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x62, 0x75, 0x66, 0x2e, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, + 0x73, 0x18, 0x83, 0x89, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x70, 0x61, 0x72, 0x61, 0x6d, + 0x3a, 0x3a, 0x0a, 0x07, 0x62, 0x61, 0x73, 0x65, 0x75, 0x72, 0x6c, 0x12, 0x1e, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x4d, 0x65, - 0x74, 0x68, 0x6f, 0x64, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x80, 0x89, 0x03, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x3a, 0x3d, 0x0a, 0x09, 0x61, 0x70, 0x69, - 0x5f, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x1e, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x4f, - 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x81, 0x89, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, - 0x61, 0x70, 0x69, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x3a, 0x40, 0x0a, 0x0a, 0x73, 0x65, 0x72, 0x69, - 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x72, 0x12, 0x1e, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x4f, - 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x82, 0x89, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, - 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x72, 0x3a, 0x36, 0x0a, 0x05, 0x70, 0x61, - 0x72, 0x61, 0x6d, 0x12, 0x1e, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x4f, 0x70, 0x74, 0x69, - 0x6f, 0x6e, 0x73, 0x18, 0x83, 0x89, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x70, 0x61, 0x72, - 0x61, 0x6d, 0x3a, 0x3a, 0x0a, 0x07, 0x62, 0x61, 0x73, 0x65, 0x75, 0x72, 0x6c, 0x12, 0x1e, 0x2e, - 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, - 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x84, 0x89, - 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x62, 0x61, 0x73, 0x65, 0x75, 0x72, 0x6c, 0x3a, 0x43, - 0x0a, 0x0c, 0x68, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x72, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x12, 0x1e, + 0x74, 0x68, 0x6f, 0x64, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x84, 0x89, 0x03, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x07, 0x62, 0x61, 0x73, 0x65, 0x75, 0x72, 0x6c, 0x3a, 0x43, 0x0a, 0x0c, + 0x68, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x72, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x12, 0x1e, 0x2e, 0x67, + 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x4d, + 0x65, 0x74, 0x68, 0x6f, 0x64, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x85, 0x89, 0x03, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x68, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x72, 0x50, 0x61, 0x74, + 0x68, 0x3a, 0x40, 0x0a, 0x09, 0x68, 0x74, 0x74, 0x70, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x12, 0x21, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, - 0x2e, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x85, - 0x89, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x68, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x72, 0x50, - 0x61, 0x74, 0x68, 0x3a, 0x40, 0x0a, 0x09, 0x68, 0x74, 0x74, 0x70, 0x5f, 0x63, 0x6f, 0x64, 0x65, - 0x12, 0x21, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, - 0x75, 0x66, 0x2e, 0x45, 0x6e, 0x75, 0x6d, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x4f, 0x70, 0x74, 0x69, - 0x6f, 0x6e, 0x73, 0x18, 0xe1, 0x89, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x68, 0x74, 0x74, - 0x70, 0x43, 0x6f, 0x64, 0x65, 0x42, 0x06, 0x5a, 0x04, 0x2f, 0x61, 0x70, 0x69, + 0x2e, 0x45, 0x6e, 0x75, 0x6d, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, + 0x73, 0x18, 0xe1, 0x89, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x68, 0x74, 0x74, 0x70, 0x43, + 0x6f, 0x64, 0x65, 0x3a, 0x42, 0x0a, 0x0b, 0x62, 0x61, 0x73, 0x65, 0x5f, 0x64, 0x6f, 0x6d, 0x61, + 0x69, 0x6e, 0x12, 0x1f, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x4f, 0x70, 0x74, 0x69, + 0x6f, 0x6e, 0x73, 0x18, 0xe2, 0x89, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x62, 0x61, 0x73, + 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x42, 0x06, 0x5a, 0x04, 0x2f, 0x61, 0x70, 0x69, } var file_api_proto_goTypes = []interface{}{ (*descriptorpb.FieldOptions)(nil), // 0: google.protobuf.FieldOptions (*descriptorpb.MethodOptions)(nil), // 1: google.protobuf.MethodOptions (*descriptorpb.EnumValueOptions)(nil), // 2: google.protobuf.EnumValueOptions + (*descriptorpb.ServiceOptions)(nil), // 3: google.protobuf.ServiceOptions } var file_api_proto_depIdxs = []int32{ 0, // 0: api.raw_body:extendee -> google.protobuf.FieldOptions @@ -440,28 +473,30 @@ var file_api_proto_depIdxs = []int32{ 0, // 7: api.form:extendee -> google.protobuf.FieldOptions 0, // 8: api.go_tag:extendee -> google.protobuf.FieldOptions 0, // 9: api.js_conv:extendee -> google.protobuf.FieldOptions - 1, // 10: api.get:extendee -> google.protobuf.MethodOptions - 1, // 11: api.post:extendee -> google.protobuf.MethodOptions - 1, // 12: api.put:extendee -> google.protobuf.MethodOptions - 1, // 13: api.delete:extendee -> google.protobuf.MethodOptions - 1, // 14: api.patch:extendee -> google.protobuf.MethodOptions - 1, // 15: api.options:extendee -> google.protobuf.MethodOptions - 1, // 16: api.head:extendee -> google.protobuf.MethodOptions - 1, // 17: api.any:extendee -> google.protobuf.MethodOptions - 1, // 18: api.gen_path:extendee -> google.protobuf.MethodOptions - 1, // 19: api.api_version:extendee -> google.protobuf.MethodOptions - 1, // 20: api.tag:extendee -> google.protobuf.MethodOptions - 1, // 21: api.name:extendee -> google.protobuf.MethodOptions - 1, // 22: api.api_level:extendee -> google.protobuf.MethodOptions - 1, // 23: api.serializer:extendee -> google.protobuf.MethodOptions - 1, // 24: api.param:extendee -> google.protobuf.MethodOptions - 1, // 25: api.baseurl:extendee -> google.protobuf.MethodOptions - 1, // 26: api.handler_path:extendee -> google.protobuf.MethodOptions - 2, // 27: api.http_code:extendee -> google.protobuf.EnumValueOptions - 28, // [28:28] is the sub-list for method output_type - 28, // [28:28] is the sub-list for method input_type - 28, // [28:28] is the sub-list for extension type_name - 0, // [0:28] is the sub-list for extension extendee + 0, // 10: api.file_name:extendee -> google.protobuf.FieldOptions + 1, // 11: api.get:extendee -> google.protobuf.MethodOptions + 1, // 12: api.post:extendee -> google.protobuf.MethodOptions + 1, // 13: api.put:extendee -> google.protobuf.MethodOptions + 1, // 14: api.delete:extendee -> google.protobuf.MethodOptions + 1, // 15: api.patch:extendee -> google.protobuf.MethodOptions + 1, // 16: api.options:extendee -> google.protobuf.MethodOptions + 1, // 17: api.head:extendee -> google.protobuf.MethodOptions + 1, // 18: api.any:extendee -> google.protobuf.MethodOptions + 1, // 19: api.gen_path:extendee -> google.protobuf.MethodOptions + 1, // 20: api.api_version:extendee -> google.protobuf.MethodOptions + 1, // 21: api.tag:extendee -> google.protobuf.MethodOptions + 1, // 22: api.name:extendee -> google.protobuf.MethodOptions + 1, // 23: api.api_level:extendee -> google.protobuf.MethodOptions + 1, // 24: api.serializer:extendee -> google.protobuf.MethodOptions + 1, // 25: api.param:extendee -> google.protobuf.MethodOptions + 1, // 26: api.baseurl:extendee -> google.protobuf.MethodOptions + 1, // 27: api.handler_path:extendee -> google.protobuf.MethodOptions + 2, // 28: api.http_code:extendee -> google.protobuf.EnumValueOptions + 3, // 29: api.base_domain:extendee -> google.protobuf.ServiceOptions + 30, // [30:30] is the sub-list for method output_type + 30, // [30:30] is the sub-list for method input_type + 30, // [30:30] is the sub-list for extension type_name + 0, // [0:30] is the sub-list for extension extendee 0, // [0:0] is the sub-list for field type_name } @@ -477,7 +512,7 @@ func file_api_proto_init() { RawDescriptor: file_api_proto_rawDesc, NumEnums: 0, NumMessages: 0, - NumExtensions: 28, + NumExtensions: 30, NumServices: 0, }, GoTypes: file_api_proto_goTypes, diff --git a/cmd/hz/protobuf/api/api.proto b/cmd/hz/protobuf/api/api.proto index 54a2db271..5a5cd081d 100644 --- a/cmd/hz/protobuf/api/api.proto +++ b/cmd/hz/protobuf/api/api.proto @@ -17,6 +17,7 @@ extend google.protobuf.FieldOptions { optional string form = 50108; optional string go_tag = 51001; optional string js_conv = 50109; + optional string file_name = 50110; } extend google.protobuf.MethodOptions { @@ -41,4 +42,8 @@ extend google.protobuf.MethodOptions { extend google.protobuf.EnumValueOptions { optional int32 http_code = 50401; +} + +extend google.protobuf.ServiceOptions { + optional string base_domain = 50402; } \ No newline at end of file diff --git a/cmd/hz/protobuf/ast.go b/cmd/hz/protobuf/ast.go index 5ab85e9f8..f5895cfb0 100644 --- a/cmd/hz/protobuf/ast.go +++ b/cmd/hz/protobuf/ast.go @@ -23,10 +23,13 @@ import ( "github.com/cloudwego/hertz/cmd/hz/generator" "github.com/cloudwego/hertz/cmd/hz/generator/model" + "github.com/cloudwego/hertz/cmd/hz/meta" "github.com/cloudwego/hertz/cmd/hz/protobuf/api" "github.com/cloudwego/hertz/cmd/hz/util" "github.com/cloudwego/hertz/cmd/hz/util/logs" "github.com/jhump/protoreflect/desc" + "google.golang.org/protobuf/compiler/protogen" + "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/descriptorpb" ) @@ -101,7 +104,7 @@ func switchBaseType(typ descriptorpb.FieldDescriptorProto_Type) *model.Type { return nil } -func astToService(ast *descriptorpb.FileDescriptorProto, resolver *Resolver) ([]*generator.Service, error) { +func astToService(ast *descriptorpb.FileDescriptorProto, resolver *Resolver, cmdType string, gen *protogen.Plugin) ([]*generator.Service, error) { resolver.ExportReferred(true, false) ss := ast.GetService() out := make([]*generator.Service, 0, len(ss)) @@ -112,10 +115,19 @@ func astToService(ast *descriptorpb.FileDescriptorProto, resolver *Resolver) ([] Name: s.GetName(), } + service.BaseDomain = "" + domainAnno := checkFirstOption(api.E_BaseDomain, s.GetOptions()) + if cmdType == meta.CmdClient { + val, ok := domainAnno.(string) + if ok && len(val) != 0 { + service.BaseDomain = val + } + } + ms := s.GetMethod() methods := make([]*generator.HttpMethod, 0, len(ms)) + clientMethods := make([]*generator.ClientMethod, 0, len(ms)) for _, m := range ms { - hmethod, vpath := checkFirstOptions(HttpMethodOptions, m.GetOptions()) if hmethod == "" { continue @@ -188,8 +200,19 @@ func astToService(ast *descriptorpb.FileDescriptorProto, resolver *Resolver) ([] method.ReturnTypeName = respName methods = append(methods, method) + + if cmdType == meta.CmdClient { + clientMethod := &generator.ClientMethod{} + clientMethod.HttpMethod = method + err := parseAnnotationToClient(clientMethod, gen, ast, m) + if err != nil { + return nil, err + } + clientMethods = append(clientMethods, clientMethod) + } } + service.ClientMethods = clientMethods service.Methods = methods service.Models = merges out = append(out, service) @@ -197,6 +220,81 @@ func astToService(ast *descriptorpb.FileDescriptorProto, resolver *Resolver) ([] return out, nil } +func parseAnnotationToClient(clientMethod *generator.ClientMethod, gen *protogen.Plugin, ast *descriptorpb.FileDescriptorProto, m *descriptorpb.MethodDescriptorProto) error { + file, exist := gen.FilesByPath[ast.GetName()] + if !exist { + return fmt.Errorf("file(%s) can not exist", ast.GetName()) + } + method, err := getMethod(file, m) + if err != nil { + return err + } + inputType := method.Input + var ( + hasBodyAnnotation bool + hasFormAnnotation bool + ) + for _, f := range inputType.Fields { + if proto.HasExtension(f.Desc.Options(), api.E_Query) { + queryAnnos := proto.GetExtension(f.Desc.Options(), api.E_Query) + val := queryAnnos.(string) + clientMethod.QueryParamsCode += fmt.Sprintf("%q: req.Get%s(),\n", val, f.GoName) + } + + if proto.HasExtension(f.Desc.Options(), api.E_Path) { + pathAnnos := proto.GetExtension(f.Desc.Options(), api.E_Path) + val := pathAnnos.(string) + clientMethod.PathParamsCode += fmt.Sprintf("%q: req.Get%s(),\n", val, f.GoName) + } + + if proto.HasExtension(f.Desc.Options(), api.E_Header) { + headerAnnos := proto.GetExtension(f.Desc.Options(), api.E_Header) + val := headerAnnos.(string) + clientMethod.HeaderParamsCode += fmt.Sprintf("%q: req.Get%s(),\n", val, f.GoName) + } + + if proto.HasExtension(f.Desc.Options(), api.E_Form) { + formAnnos := proto.GetExtension(f.Desc.Options(), api.E_Form) + hasFormAnnotation = true + val := formAnnos.(string) + clientMethod.FormValueCode += fmt.Sprintf("%q: req.Get%s(),\n", val, f.GoName) + } + + if proto.HasExtension(f.Desc.Options(), api.E_Body) { + hasBodyAnnotation = true + } + + if proto.HasExtension(f.Desc.Options(), api.E_FileName) { + fileAnnos := proto.GetExtension(f.Desc.Options(), api.E_FileName) + hasFormAnnotation = true + val := fileAnnos.(string) + clientMethod.FormFileCode += fmt.Sprintf("%q: req.Get%s(),\n", val, f.GoName) + } + } + clientMethod.BodyParamsCode = meta.SetBodyParam + if hasBodyAnnotation && hasFormAnnotation { + clientMethod.FormValueCode = "" + clientMethod.FormFileCode = "" + } + if !hasBodyAnnotation && hasFormAnnotation { + clientMethod.BodyParamsCode = "" + } + + return nil +} + +func getMethod(file *protogen.File, m *descriptorpb.MethodDescriptorProto) (*protogen.Method, error) { + for _, f := range file.Services { + for _, method := range f.Methods { + if string(method.Desc.Name()) == m.GetName() { + return method, nil + } + } + } + + return nil, fmt.Errorf("can not find method: %s", m.GetName()) +} + //---------------------------------Model-------------------------------- func astToModel(ast *descriptorpb.FileDescriptorProto, rs *Resolver) (*model.Model, error) { diff --git a/cmd/hz/protobuf/plugin.go b/cmd/hz/protobuf/plugin.go index 7ae60fcbe..4d3234675 100644 --- a/cmd/hz/protobuf/plugin.go +++ b/cmd/hz/protobuf/plugin.go @@ -69,12 +69,14 @@ import ( ) type Plugin struct { - Package string - Recursive bool - OutDir string - ModelDir string - PkgMap map[string]string - logger *logs.StdLogger + *protogen.Plugin + Package string + Recursive bool + OutDir string + ModelDir string + IdlClientDir string + PkgMap map[string]string + logger *logs.StdLogger } func (plugin *Plugin) Run() int { @@ -184,6 +186,7 @@ func (plugin *Plugin) Handle(req *pluginpb.CodeGeneratorRequest, args *config.Ar // new plugin opts := protogen.Options{} gen, err := opts.New(req) + plugin.Plugin = gen if err != nil { return fmt.Errorf("new protoc plugin failed: %s", err.Error()) } @@ -321,6 +324,11 @@ func (plugin *Plugin) GenerateFiles(pluginPb *protogen.Plugin) error { if err != nil { return err } + impt := string(f.GoImportPath) + if strings.HasPrefix(impt, plugin.Package) { + impt = impt[len(plugin.Package):] + } + plugin.IdlClientDir = impt } else if plugin.Recursive { if strings.HasPrefix(f.Proto.GetPackage(), "google.protobuf") { continue @@ -504,7 +512,7 @@ func genMessageField(g *protogen.GeneratedFile, f *fileInfo, m *messageInfo, fie return nil } -func (plugin *Plugin) getIdlInfo(ast *descriptorpb.FileDescriptorProto, deps map[string]*descriptorpb.FileDescriptorProto) (*generator.HttpPackage, error) { +func (plugin *Plugin) getIdlInfo(ast *descriptorpb.FileDescriptorProto, deps map[string]*descriptorpb.FileDescriptorProto, args *config.Argument) (*generator.HttpPackage, error) { if ast == nil { return nil, fmt.Errorf("ast is nil") } @@ -528,7 +536,7 @@ func (plugin *Plugin) getIdlInfo(ast *descriptorpb.FileDescriptorProto, deps map return nil, err } - services, err := astToService(ast, rs) + services, err := astToService(ast, rs, args.CmdType, plugin.Plugin) if err != nil { return nil, err } @@ -547,7 +555,7 @@ func (plugin *Plugin) getIdlInfo(ast *descriptorpb.FileDescriptorProto, deps map func (plugin *Plugin) genHttpPackage(ast *descriptorpb.FileDescriptorProto, deps map[string]*descriptorpb.FileDescriptorProto, args *config.Argument) ([]generator.File, error) { options := CheckTagOption(args) - idl, err := plugin.getIdlInfo(ast, deps) + idl, err := plugin.getIdlInfo(ast, deps, args) if err != nil { return nil, err } @@ -585,6 +593,8 @@ func (plugin *Plugin) genHttpPackage(ast *descriptorpb.FileDescriptorProto, deps ProjPackage: pkg, Options: options, HandlerByMethod: args.HandlerByMethod, + CmdType: args.CmdType, + IdlClientDir: plugin.IdlClientDir, } if args.ModelBackend != "" { diff --git a/cmd/hz/thrift/ast.go b/cmd/hz/thrift/ast.go index 5bd6cfea0..7ef2d172e 100644 --- a/cmd/hz/thrift/ast.go +++ b/cmd/hz/thrift/ast.go @@ -18,11 +18,14 @@ package thrift import ( "fmt" + "strings" "github.com/cloudwego/hertz/cmd/hz/generator" "github.com/cloudwego/hertz/cmd/hz/generator/model" + "github.com/cloudwego/hertz/cmd/hz/meta" "github.com/cloudwego/hertz/cmd/hz/util" "github.com/cloudwego/hertz/cmd/hz/util/logs" + "github.com/cloudwego/thriftgo/generator/golang" "github.com/cloudwego/thriftgo/generator/golang/styles" "github.com/cloudwego/thriftgo/parser" ) @@ -45,7 +48,7 @@ func getGoPackage(ast *parser.Thrift, pkgMap map[string]string) string { /*---------------------------Service-----------------------------*/ -func astToService(ast *parser.Thrift, resolver *Resolver) ([]*generator.Service, error) { +func astToService(ast *parser.Thrift, resolver *Resolver, cmdType string) ([]*generator.Service, error) { ss := ast.GetServices() out := make([]*generator.Service, 0, len(ss)) var models model.Models @@ -55,21 +58,25 @@ func astToService(ast *parser.Thrift, resolver *Resolver) ([]*generator.Service, service := &generator.Service{ Name: s.GetName(), } + service.BaseDomain = "" + domainAnno := getAnnotation(s.Annotations, ApiBaseDomain) + if len(domainAnno) == 1 { + if cmdType == meta.CmdClient { + service.BaseDomain = domainAnno[0] + } + } ms := s.GetFunctions() methods := make([]*generator.HttpMethod, 0, len(ms)) + clientMethods := make([]*generator.ClientMethod, 0, len(ms)) for _, m := range ms { - rs := getAnnotations(m.Annotations, HttpMethodAnnotations) if len(rs) > 1 { - return nil, fmt.Errorf("invalid http router '%v' for %s.%s", rs, s.Name, m.Name) + return nil, fmt.Errorf("too many 'api.XXX' annotations: %s", rs) } if len(rs) == 0 { continue } - if len(rs) > 1 { - return nil, fmt.Errorf("too many 'api.XXX' annotations: %s", rs) - } var handlerOutDir string genPaths := getAnnotation(m.Annotations, ApiGenPath) @@ -88,13 +95,13 @@ func astToService(ast *parser.Thrift, resolver *Resolver) ([]*generator.Service, var reqName string if len(m.Arguments) >= 1 { + if len(m.Arguments) > 1 { + logs.Warnf("function '%s' has more than one argument, but only the first can be used in hertz now", m.GetName()) + } rt, err := resolver.ResolveIdentifier(m.Arguments[0].GetType().GetName()) if err != nil { return nil, err } - // if len(m.Arguments) > 1 { - // *warns = append(*warns, fmt.Sprintf("function '%s' has more than one argument, but only the first can be used in hertz now", m.GetName())) - // } reqName = rt.Expression() } var respName string @@ -120,7 +127,6 @@ func astToService(ast *parser.Thrift, resolver *Resolver) ([]*generator.Service, refs := resolver.ExportReferred(false, true) method.Models = make(map[string]*model.Model, len(refs)) for _, ref := range refs { - // method.Models[ref.Model.PackageName] = ref.Model if v, ok := method.Models[ref.Model.PackageName]; ok && (v.Package != ref.Model.Package) { return nil, fmt.Errorf("Package name: %s redeclared in %s and %s ", ref.Model.PackageName, v.Package, ref.Model.Package) } @@ -128,8 +134,22 @@ func astToService(ast *parser.Thrift, resolver *Resolver) ([]*generator.Service, } models.MergeMap(method.Models) methods = append(methods, method) + if cmdType == meta.CmdClient { + clientMethod := &generator.ClientMethod{} + clientMethod.HttpMethod = method + rt, err := resolver.ResolveIdentifier(m.Arguments[0].GetType().GetName()) + if err != nil { + return nil, err + } + err = parseAnnotationToClient(clientMethod, m.Arguments[0].GetType(), rt) + if err != nil { + return nil, err + } + clientMethods = append(clientMethods, clientMethod) + } } + service.ClientMethods = clientMethods service.Methods = methods service.Models = models out = append(out, service) @@ -137,6 +157,93 @@ func astToService(ast *parser.Thrift, resolver *Resolver) ([]*generator.Service, return out, nil } +func parseAnnotationToClient(clientMethod *generator.ClientMethod, p *parser.Type, symbol ResolvedSymbol) error { + if p == nil { + return fmt.Errorf("get type failed for parse annotatoon to client") + } + typeName := p.GetName() + if strings.Contains(typeName, ".") { + ret := strings.Split(typeName, ".") + typeName = ret[len(ret)-1] + } + scope, err := golang.BuildScope(thriftgoUtil, symbol.Scope) + if err != nil { + fmt.Errorf("can not build scope for %s", p.Name) + } + thriftgoUtil.SetRootScope(scope) + st := scope.StructLike(typeName) + var ( + hasBodyAnnotation bool + hasFormAnnotation bool + ) + for _, field := range st.Fields() { + rwctx, err := thriftgoUtil.MkRWCtx(thriftgoUtil.RootScope(), field) + if err != nil { + fmt.Errorf("can not get field info for %s", field.Name) + } + if anno := getAnnotation(field.Annotations, AnnotationQuery); len(anno) > 0 { + query := anno[0] + if rwctx.IsPointer { + clientMethod.QueryParamsCode += fmt.Sprintf("%q: *req.%v,\n", query, field.GoName().String()) + } else { + clientMethod.QueryParamsCode += fmt.Sprintf("%q: req.%v,\n", query, field.GoName().String()) + } + } + + if anno := getAnnotation(field.Annotations, AnnotationPath); len(anno) > 0 { + path := anno[0] + if rwctx.IsPointer { + clientMethod.PathParamsCode += fmt.Sprintf("%q: *req.%v,\n", path, field.GoName().String()) + } else { + clientMethod.PathParamsCode += fmt.Sprintf("%q: req.%v,\n", path, field.GoName().String()) + } + } + + if anno := getAnnotation(field.Annotations, AnnotationHeader); len(anno) > 0 { + header := anno[0] + if rwctx.IsPointer { + clientMethod.HeaderParamsCode += fmt.Sprintf("%q: *req.%v,\n", header, field.GoName().String()) + } else { + clientMethod.HeaderParamsCode += fmt.Sprintf("%q: req.%v,\n", header, field.GoName().String()) + } + } + + if anno := getAnnotation(field.Annotations, AnnotationForm); len(anno) > 0 { + form := anno[0] + hasFormAnnotation = true + if rwctx.IsPointer { + clientMethod.FormValueCode += fmt.Sprintf("%q: *req.%v,\n", form, field.GoName().String()) + } else { + clientMethod.FormValueCode += fmt.Sprintf("%q: req.%v,\n", form, field.GoName().String()) + } + } + + if anno := getAnnotation(field.Annotations, AnnotationBody); len(anno) > 0 { + hasBodyAnnotation = true + } + + if anno := getAnnotation(field.Annotations, AnnotationFileName); len(anno) > 0 { + fileName := anno[0] + hasFormAnnotation = true + if rwctx.IsPointer { + clientMethod.FormFileCode += fmt.Sprintf("%q: *req.%v,\n", fileName, field.GoName().String()) + } else { + clientMethod.FormFileCode += fmt.Sprintf("%q: req.%v,\n", fileName, field.GoName().String()) + } + } + } + clientMethod.BodyParamsCode = meta.SetBodyParam + if hasBodyAnnotation && hasFormAnnotation { + clientMethod.FormValueCode = "" + clientMethod.FormFileCode = "" + } + if !hasBodyAnnotation && hasFormAnnotation { + clientMethod.BodyParamsCode = "" + } + + return nil +} + /*---------------------------Model-----------------------------*/ var BaseThrift = parser.Thrift{} diff --git a/cmd/hz/thrift/plugin.go b/cmd/hz/thrift/plugin.go index fa328d510..33d159b13 100644 --- a/cmd/hz/thrift/plugin.go +++ b/cmd/hz/thrift/plugin.go @@ -29,6 +29,8 @@ import ( "github.com/cloudwego/hertz/cmd/hz/meta" "github.com/cloudwego/hertz/cmd/hz/util" "github.com/cloudwego/hertz/cmd/hz/util/logs" + "github.com/cloudwego/thriftgo/generator/backend" + "github.com/cloudwego/thriftgo/generator/golang" "github.com/cloudwego/thriftgo/generator/golang/styles" thriftgo_plugin "github.com/cloudwego/thriftgo/plugin" ) @@ -133,6 +135,8 @@ func (plugin *Plugin) Run() int { ProjPackage: pkg, Options: options, HandlerByMethod: args.HandlerByMethod, + CmdType: args.CmdType, + IdlClientDir: util.SubDir(modelDir, pkgInfo.Package), } if args.ModelBackend != "" { sg.Backend = meta.Backend(args.ModelBackend) @@ -195,6 +199,9 @@ func (plugin *Plugin) handleRequest() error { return fmt.Errorf("unmarshal request failed: %s", err.Error()) } plugin.req = req + // init thriftgo utils + thriftgoUtil = golang.NewCodeUtils(backend.DummyLogFunc()) + thriftgoUtil.HandleOptions(req.GeneratorParameters) return nil } @@ -262,7 +269,7 @@ func (plugin *Plugin) getPackageInfo() (*generator.HttpPackage, error) { return nil, fmt.Errorf("go package for '%s' is not defined", ast.GetFilename()) } - services, err := astToService(ast, rs) + services, err := astToService(ast, rs, args.CmdType) if err != nil { return nil, err } diff --git a/cmd/hz/thrift/tags.go b/cmd/hz/thrift/tags.go index 9b8c0cb45..16a06961f 100644 --- a/cmd/hz/thrift/tags.go +++ b/cmd/hz/thrift/tags.go @@ -30,15 +30,16 @@ import ( ) const ( - AnnotationQuery = "api.query" - AnnotationForm = "api.form" - AnnotationPath = "api.path" - AnnotationHeader = "api.header" - AnnotationCookie = "api.cookie" - AnnotationBody = "api.body" - AnnotationRawBody = "api.raw_body" - AnnotationJsConv = "api.js_conv" - AnnotationNone = "api.none" + AnnotationQuery = "api.query" + AnnotationForm = "api.form" + AnnotationPath = "api.path" + AnnotationHeader = "api.header" + AnnotationCookie = "api.cookie" + AnnotationBody = "api.body" + AnnotationRawBody = "api.raw_body" + AnnotationJsConv = "api.js_conv" + AnnotationNone = "api.none" + AnnotationFileName = "api.file_name" AnnotationValidator = "api.vd" @@ -59,6 +60,10 @@ const ( ApiGenPath = "api.handler_path" ) +const ( + ApiBaseDomain = "api.base_domain" +) + var ( HttpMethodAnnotations = map[string]string{ ApiGet: "GET", diff --git a/cmd/hz/thrift/name_style.go b/cmd/hz/thrift/thriftgo_util.go similarity index 81% rename from cmd/hz/thrift/name_style.go rename to cmd/hz/thrift/thriftgo_util.go index ec53f53fb..0be9baa7a 100644 --- a/cmd/hz/thrift/name_style.go +++ b/cmd/hz/thrift/thriftgo_util.go @@ -16,6 +16,11 @@ package thrift -import "github.com/cloudwego/thriftgo/generator/golang/styles" +import ( + "github.com/cloudwego/thriftgo/generator/golang" + "github.com/cloudwego/thriftgo/generator/golang/styles" +) + +var thriftgoUtil *golang.CodeUtils var NameStyle = styles.NewNamingStyle("thriftgo")