From 8e075fba530c05e7a9c72d4e9478fb375f7e951c Mon Sep 17 00:00:00 2001 From: jswxstw <385920199@qq.com> Date: Mon, 29 Apr 2024 20:37:25 +0800 Subject: [PATCH] contrib/sdk/httpclient: add custom response handler support, fixe #3539 (#3540) --- .../consts/consts_gen_ctrl_template_sdk.go | 6 - contrib/sdk/httpclient/httpclient.go | 53 +++------ contrib/sdk/httpclient/httpclient_config.go | 1 + contrib/sdk/httpclient/httpclient_handler.go | 68 +++++++++++ .../httpclient_z_unit_feature_handler_test.go | 109 ++++++++++++++++++ 5 files changed, 195 insertions(+), 42 deletions(-) create mode 100644 contrib/sdk/httpclient/httpclient_handler.go create mode 100644 contrib/sdk/httpclient/httpclient_z_unit_feature_handler_test.go diff --git a/cmd/gf/internal/consts/consts_gen_ctrl_template_sdk.go b/cmd/gf/internal/consts/consts_gen_ctrl_template_sdk.go index 92177fbf2fc..09f3795e393 100644 --- a/cmd/gf/internal/consts/consts_gen_ctrl_template_sdk.go +++ b/cmd/gf/internal/consts/consts_gen_ctrl_template_sdk.go @@ -26,12 +26,6 @@ type implementer struct { } func New(config httpclient.Config) IClient { - if !gstr.HasPrefix(config.URL, "http") { - config.URL = fmt.Sprintf("http://%s", config.URL) - } - if config.Logger == nil { - config.Logger = g.Log() - } return &implementer{ config: config, } diff --git a/contrib/sdk/httpclient/httpclient.go b/contrib/sdk/httpclient/httpclient.go index 3aa02add2e9..272f3a6369e 100644 --- a/contrib/sdk/httpclient/httpclient.go +++ b/contrib/sdk/httpclient/httpclient.go @@ -9,11 +9,10 @@ package httpclient import ( "context" - "encoding/json" + "fmt" "net/http" "github.com/gogf/gf/v2/encoding/gurl" - "github.com/gogf/gf/v2/errors/gcode" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/net/gclient" "github.com/gogf/gf/v2/net/ghttp" @@ -24,48 +23,29 @@ import ( "github.com/gogf/gf/v2/util/gtag" ) -// Client is an http client for SDK. +// Client is a http client for SDK. type Client struct { *gclient.Client - config Config + Handler } -// New creates and returns an http client for SDK. +// New creates and returns a http client for SDK. func New(config Config) *Client { client := config.Client if client == nil { client = gclient.New() } - return &Client{ - Client: client, - config: config, - } -} - -func (c *Client) handleResponse(ctx context.Context, res *gclient.Response, out interface{}) error { - if c.config.RawDump { - c.config.Logger.Debugf(ctx, "raw request&response:\n%s", res.Raw()) - } - - var ( - responseBytes = res.ReadAll() - result = ghttp.DefaultHandlerResponse{ - Data: out, - } - ) - if !json.Valid(responseBytes) { - return gerror.Newf(`invalid response content: %s`, responseBytes) + handler := config.Handler + if handler == nil { + handler = NewDefaultHandler(config.Logger, config.RawDump) } - if err := json.Unmarshal(responseBytes, &result); err != nil { - return gerror.Wrapf(err, `json.Unmarshal failed with content:%s`, responseBytes) + if !gstr.HasPrefix(config.URL, "http") { + config.URL = fmt.Sprintf("http://%s", config.URL) } - if result.Code != gcode.CodeOK.Code() { - return gerror.NewCode( - gcode.New(result.Code, result.Message, nil), - result.Message, - ) + return &Client{ + Client: client.Prefix(config.URL), + Handler: handler, } - return nil } // Request sends request to service by struct object `req`, and receives response to struct object `res`. @@ -83,20 +63,21 @@ func (c *Client) Request(ctx context.Context, req, res interface{}) error { if err != nil { return err } - return c.handleResponse(ctx, result, res) + return c.HandleResponse(ctx, result, res) } } // Get sends a request using GET method. func (c *Client) Get(ctx context.Context, path string, in, out interface{}) error { - if urlParams := ghttp.BuildParams(in); urlParams != "" { - path += "?" + ghttp.BuildParams(in) + // TODO: Path params will also be built in urlParams, not graceful now. + if urlParams := ghttp.BuildParams(in); urlParams != "" && urlParams != "{}" { + path += "?" + urlParams } res, err := c.ContentJson().Get(ctx, c.handlePath(path, in)) if err != nil { return gerror.Wrap(err, `http request failed`) } - return c.handleResponse(ctx, res, out) + return c.HandleResponse(ctx, res, out) } func (c *Client) handlePath(path string, in interface{}) string { diff --git a/contrib/sdk/httpclient/httpclient_config.go b/contrib/sdk/httpclient/httpclient_config.go index 7a9c0feb7d9..eccb45323fb 100644 --- a/contrib/sdk/httpclient/httpclient_config.go +++ b/contrib/sdk/httpclient/httpclient_config.go @@ -15,6 +15,7 @@ import ( type Config struct { URL string `v:"required"` // Service address. Eg: user.svc.local, http://user.svc.local Client *gclient.Client // Custom underlying client. + Handler Handler // Custom response handler. Logger *glog.Logger // Custom logger. RawDump bool // Whether auto dump request&response in stdout. } diff --git a/contrib/sdk/httpclient/httpclient_handler.go b/contrib/sdk/httpclient/httpclient_handler.go new file mode 100644 index 00000000000..eca6eed9e0d --- /dev/null +++ b/contrib/sdk/httpclient/httpclient_handler.go @@ -0,0 +1,68 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package httpclient + +import ( + "context" + "encoding/json" + + "github.com/gogf/gf/v2/errors/gcode" + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/net/gclient" + "github.com/gogf/gf/v2/net/ghttp" + "github.com/gogf/gf/v2/os/glog" +) + +// Handler is the interface for http response handling. +type Handler interface { + // HandleResponse handles the http response and transforms its body to the specified object. + // The parameter `out` specifies the object that the response body is transformed to. + HandleResponse(ctx context.Context, res *gclient.Response, out interface{}) error +} + +// DefaultHandler handle ghttp.DefaultHandlerResponse of json format. +type DefaultHandler struct { + Logger *glog.Logger + RawDump bool +} + +func NewDefaultHandler(logger *glog.Logger, rawRump bool) *DefaultHandler { + if rawRump && logger == nil { + logger = g.Log() + } + return &DefaultHandler{ + Logger: logger, + RawDump: rawRump, + } +} + +func (h DefaultHandler) HandleResponse(ctx context.Context, res *gclient.Response, out interface{}) error { + defer res.Close() + if h.RawDump { + h.Logger.Debugf(ctx, "raw request&response:\n%s", res.Raw()) + } + var ( + responseBytes = res.ReadAll() + result = ghttp.DefaultHandlerResponse{ + Data: out, + } + ) + if !json.Valid(responseBytes) { + return gerror.Newf(`invalid response content: %s`, responseBytes) + } + if err := json.Unmarshal(responseBytes, &result); err != nil { + return gerror.Wrapf(err, `json.Unmarshal failed with content:%s`, responseBytes) + } + if result.Code != gcode.CodeOK.Code() { + return gerror.NewCode( + gcode.New(result.Code, result.Message, nil), + result.Message, + ) + } + return nil +} diff --git a/contrib/sdk/httpclient/httpclient_z_unit_feature_handler_test.go b/contrib/sdk/httpclient/httpclient_z_unit_feature_handler_test.go new file mode 100644 index 00000000000..6561f9e79b7 --- /dev/null +++ b/contrib/sdk/httpclient/httpclient_z_unit_feature_handler_test.go @@ -0,0 +1,109 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package httpclient_test + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/gogf/gf/contrib/sdk/httpclient/v2" + "github.com/gogf/gf/v2/errors/gcode" + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/net/gclient" + "github.com/gogf/gf/v2/net/ghttp" + "github.com/gogf/gf/v2/os/gctx" + "github.com/gogf/gf/v2/test/gtest" + "github.com/gogf/gf/v2/util/guid" +) + +func Test_HttpClient_With_Default_Handler(t *testing.T) { + type Req struct { + g.Meta `path:"/get" method:"get"` + } + type Res struct { + Uid int + Name string + } + + s := g.Server(guid.S()) + s.BindHandler("/get", func(r *ghttp.Request) { + res := ghttp.DefaultHandlerResponse{ + Data: Res{ + Uid: 1, + Name: "test", + }, + } + r.Response.WriteJson(res) + }) + s.SetDumpRouterMap(false) + s.Start() + defer s.Shutdown() + + time.Sleep(100 * time.Millisecond) + + gtest.C(t, func(t *gtest.T) { + client := httpclient.New(httpclient.Config{ + URL: fmt.Sprintf("http://127.0.0.1:%d", s.GetListenedPort()), + }) + var ( + req = &Req{} + res = &Res{} + ) + err := client.Request(gctx.New(), req, res) + t.AssertNil(err) + t.AssertEQ(res.Uid, 1) + t.AssertEQ(res.Name, "test") + }) +} + +type CustomHandler struct{} + +func (c CustomHandler) HandleResponse(ctx context.Context, res *gclient.Response, out interface{}) error { + defer res.Close() + if pointer, ok := out.(*string); ok { + *pointer = res.ReadAllString() + } else { + return gerror.NewCodef(gcode.CodeInvalidParameter, "[CustomHandler] expectedType:'*string', but realType:'%T'", out) + } + return nil +} + +func Test_HttpClient_With_Custom_Handler(t *testing.T) { + type Req struct { + g.Meta `path:"/get" method:"get"` + } + + s := g.Server(guid.S()) + s.BindHandler("/get", func(r *ghttp.Request) { + r.Response.WriteExit("It is a test.") + }) + s.SetDumpRouterMap(false) + s.Start() + defer s.Shutdown() + + time.Sleep(100 * time.Millisecond) + + client := httpclient.New(httpclient.Config{ + URL: fmt.Sprintf("127.0.0.1:%d", s.GetListenedPort()), + Handler: CustomHandler{}, + }) + req := &Req{} + gtest.C(t, func(t *gtest.T) { + var res = new(string) + err := client.Request(gctx.New(), req, res) + t.AssertNil(err) + t.AssertEQ(*res, "It is a test.") + }) + gtest.C(t, func(t *gtest.T) { + var res string + err := client.Request(gctx.New(), req, res) + t.AssertEQ(err, gerror.NewCodef(gcode.CodeInvalidParameter, "[CustomHandler] expectedType:'*string', but realType:'%T'", res)) + }) +}