diff --git a/net/ghttp/ghttp_client_config.go b/net/ghttp/ghttp_client_config.go index 4a47d4a47..b90d57665 100644 --- a/net/ghttp/ghttp_client_config.go +++ b/net/ghttp/ghttp_client_config.go @@ -22,17 +22,18 @@ import ( // Client is the HTTP client for HTTP request management. type Client struct { - http.Client // Underlying HTTP Client. - ctx context.Context // Context for each request. - parent *Client // Parent http client, this is used for chaining operations. - header map[string]string // Custom header map. - cookies map[string]string // Custom cookie map. - prefix string // Prefix for request. - authUser string // HTTP basic authentication: user. - authPass string // HTTP basic authentication: pass. - browserMode bool // Whether auto saving and sending cookie content. - retryCount int // Retry count when request fails. - retryInterval time.Duration // Retry interval when request fails. + http.Client // Underlying HTTP Client. + ctx context.Context // Context for each request. + parent *Client // Parent http client, this is used for chaining operations. + header map[string]string // Custom header map. + cookies map[string]string // Custom cookie map. + prefix string // Prefix for request. + authUser string // HTTP basic authentication: user. + authPass string // HTTP basic authentication: pass. + browserMode bool // Whether auto saving and sending cookie content. + retryCount int // Retry count when request fails. + retryInterval time.Duration // Retry interval when request fails. + middlewareHandler []ClientHandlerFunc // Interceptor handlers } // NewClient creates and returns a new HTTP client object. diff --git a/net/ghttp/ghttp_client_middleware.go b/net/ghttp/ghttp_client_middleware.go new file mode 100644 index 000000000..c121ef66b --- /dev/null +++ b/net/ghttp/ghttp_client_middleware.go @@ -0,0 +1,55 @@ +package ghttp + +import ( + "net/http" +) + +const gfHTTPClientMiddlewareKey = "__gfHttpClientMiddlewareKey" + +// Use Add middleware to client +func (c *Client) Use(handlers ...ClientHandlerFunc) *Client { + newClient := c + if c.parent == nil { + newClient = c.Clone() + } + + newClient.middlewareHandler = append(newClient.middlewareHandler, handlers...) + return newClient +} + +// MiddlewareNext call next middleware +// this is should only be call in ClientHandlerFunc +func (c *Client) MiddlewareNext(req *http.Request) (*ClientResponse, error) { + m, ok := req.Context().Value(gfHTTPClientMiddlewareKey).(*clientMiddleware) + if ok { + resp, err := m.Next(req) + return resp, err + } + return c.callRequest(req) +} + +// ClientHandlerFunc middleware handler func +type ClientHandlerFunc = func(c *Client, r *http.Request) (*ClientResponse, error) + +// clientMiddleware is the plugin for http client request workflow management. +type clientMiddleware struct { + client *Client // http client + handlers []ClientHandlerFunc // mdl handlers + handlerIndex int // current handler index + resp *ClientResponse // save resp + err error // save err +} + +// Next call next middleware handler, if abort, +func (m *clientMiddleware) Next(req *http.Request) (resp *ClientResponse, err error) { + if m.err != nil { + return m.resp, m.err + } + if m.handlerIndex < len(m.handlers) { + m.handlerIndex++ + resp, err = m.handlers[m.handlerIndex](m.client, req) + m.resp = resp + m.err = err + } + return +} diff --git a/net/ghttp/ghttp_client_request.go b/net/ghttp/ghttp_client_request.go index 5d724cea6..0962c9080 100644 --- a/net/ghttp/ghttp_client_request.go +++ b/net/ghttp/ghttp_client_request.go @@ -8,6 +8,7 @@ package ghttp import ( "bytes" + "context" "errors" "fmt" "github.com/gogf/gf/internal/json" @@ -82,14 +83,8 @@ func (c *Client) Trace(url string, data ...interface{}) (*ClientResponse, error) return c.DoRequest("TRACE", url, data...) } -// DoRequest sends request with given HTTP method and data and returns the response object. -// Note that the response object MUST be closed if it'll be never used. -// -// Note that it uses "multipart/form-data" as its Content-Type if it contains file uploading, -// else it uses "application/x-www-form-urlencoded". It also automatically detects the post -// content for JSON format, and for that it automatically sets the Content-Type as -// "application/json". -func (c *Client) DoRequest(method, url string, data ...interface{}) (resp *ClientResponse, err error) { +// prepareRequest verify params and return http request +func (c *Client) prepareRequest(method, url string, data ...interface{}) (req *http.Request, err error) { method = strings.ToUpper(method) if len(c.prefix) > 0 { url = c.prefix + gstr.Trim(url) @@ -123,7 +118,6 @@ func (c *Client) DoRequest(method, url string, data ...interface{}) (resp *Clien param = BuildParams(data[0]) } } - var req *http.Request if method == "GET" { // It appends the parameters to the url if http method is GET. if param != "" { @@ -203,6 +197,8 @@ func (c *Client) DoRequest(method, url string, data ...interface{}) (resp *Clien // Context. if c.ctx != nil { req = req.WithContext(c.ctx) + } else { + req = req.WithContext(context.Background()) } // Custom header. if len(c.header) > 0 { @@ -232,6 +228,12 @@ func (c *Client) DoRequest(method, url string, data ...interface{}) (resp *Clien if len(c.authUser) > 0 { req.SetBasicAuth(c.authUser, c.authPass) } + return req, nil +} + +// callRequest sends request with give http.Request, and returns the responses object. +// Note that the response object MUST be closed if it'll be never used. +func (c *Client) callRequest(req *http.Request) (resp *ClientResponse, err error) { resp = &ClientResponse{ request: req, } @@ -250,12 +252,49 @@ func (c *Client) DoRequest(method, url string, data ...interface{}) (resp *Clien c.retryCount-- time.Sleep(c.retryInterval) } else { - return resp, err + //return resp, err + break } } else { break } } + return resp, err +} + +// DoRequest sends request with given HTTP method and data and returns the response object. +// Note that the response object MUST be closed if it'll be never used. +// +// Note that it uses "multipart/form-data" as its Content-Type if it contains file uploading, +// else it uses "application/x-www-form-urlencoded". It also automatically detects the post +// content for JSON format, and for that it automatically sets the Content-Type as +// "application/json". +func (c *Client) DoRequest(method, url string, data ...interface{}) (resp *ClientResponse, err error) { + req, err := c.prepareRequest(method, url, data...) + if err != nil { + return nil, err + } + + if len(c.middlewareHandler) > 0 { + mdlHandlers := make([]ClientHandlerFunc, 0, len(c.middlewareHandler)+1) + mdlHandlers = append(mdlHandlers, c.middlewareHandler...) + + // last call internal handler + mdlHandlers = append(mdlHandlers, func(cli *Client, r *http.Request) (*ClientResponse, error) { + return cli.callRequest(r) + }) + + // call middleware + ctx := context.WithValue(req.Context(), gfHTTPClientMiddlewareKey, &clientMiddleware{ + client: c, + handlers: mdlHandlers, + handlerIndex: -1, + }) + req = req.WithContext(ctx) + resp, err = c.MiddlewareNext(req) + } else { + resp, err = c.callRequest(req) + } // Auto saving cookie content. if c.browserMode { @@ -268,5 +307,5 @@ func (c *Client) DoRequest(method, url string, data ...interface{}) (resp *Clien } } } - return resp, nil + return resp, err } diff --git a/net/ghttp/ghttp_unit_client_test.go b/net/ghttp/ghttp_unit_client_test.go index 21a68c568..bead02afb 100644 --- a/net/ghttp/ghttp_unit_client_test.go +++ b/net/ghttp/ghttp_unit_client_test.go @@ -7,14 +7,19 @@ package ghttp_test import ( + "bytes" "context" "fmt" - "github.com/gogf/gf/debug/gdebug" - "github.com/gogf/gf/os/gfile" - "github.com/gogf/gf/util/guid" + "io/ioutil" + "net/http" "testing" "time" + "github.com/gogf/gf/debug/gdebug" + "github.com/gogf/gf/errors/gerror" + "github.com/gogf/gf/os/gfile" + "github.com/gogf/gf/util/guid" + "github.com/gogf/gf/frame/g" "github.com/gogf/gf/net/ghttp" "github.com/gogf/gf/test/gtest" @@ -332,3 +337,68 @@ func Test_Client_File_And_Param(t *testing.T) { t.Assert(c.PostContent("/", data), data["json"].(string)+gfile.GetContents(path)) }) } + +func Test_Client_Middleware(t *testing.T) { + p, _ := ports.PopRand() + s := g.Server(p) + isServerHandler := false + s.BindHandler("/", func(r *ghttp.Request) { + isServerHandler = true + }) + s.SetPort(p) + s.SetDumpRouterMap(false) + s.Start() + defer s.Shutdown() + + time.Sleep(100 * time.Millisecond) + + gtest.C(t, func(t *gtest.T) { + str := "" + str2 := "resp body" + c := ghttp.NewClient().SetPrefix(fmt.Sprintf("http://127.0.0.1:%d", p)).Use(func(c *ghttp.Client, r *http.Request) (resp *ghttp.ClientResponse, err error) { + str += "a" + resp, err = c.MiddlewareNext(r) + str += "b" + return + }).Use(func(c *ghttp.Client, r *http.Request) (resp *ghttp.ClientResponse, err error) { + str += "c" + resp, err = c.MiddlewareNext(r) + str += "d" + return + }).Use(func(c *ghttp.Client, r *http.Request) (resp *ghttp.ClientResponse, err error) { + str += "e" + resp, err = c.MiddlewareNext(r) + resp.Response.Body = ioutil.NopCloser(bytes.NewBufferString(str2)) + str += "f" + return + }) + + resp, err := c.Get("/") + t.Assert(str, "acefdb") + t.Assert(err, nil) + t.Assert(resp.ReadAllString(), str2) + t.Assert(isServerHandler, true) + + // test abort, abort will not send + str3 := "" + abortStr := "abort request" + c = ghttp.NewClient().SetPrefix(fmt.Sprintf("http://127.0.0.1:%d", p)).Use(func(c *ghttp.Client, r *http.Request) (resp *ghttp.ClientResponse, err error) { + str3 += "a" + resp, err = c.MiddlewareNext(r) + str3 += "b" + return + }).Use(func(c *ghttp.Client, r *http.Request) (*ghttp.ClientResponse, error) { + str3 += "c" + return nil, gerror.New(abortStr) + }).Use(func(c *ghttp.Client, r *http.Request) (resp *ghttp.ClientResponse, err error) { + str3 += "f" + resp, err = c.MiddlewareNext(r) + str3 += "g" + return + }) + resp, err = c.Get("/") + t.Assert(str3, "acb") + t.Assert(err.Error(), abortStr) + t.Assert(resp, nil) + }) +}