diff --git a/net/ghttp/ghttp_client_config.go b/net/ghttp/ghttp_client_config.go index 207cfe868..2710ecb01 100644 --- a/net/ghttp/ghttp_client_config.go +++ b/net/ghttp/ghttp_client_config.go @@ -33,6 +33,7 @@ type Client struct { 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..b8f7ede3b --- /dev/null +++ b/net/ghttp/ghttp_client_middleware.go @@ -0,0 +1,72 @@ +package ghttp + +import ( + "github.com/gogf/gf/errors/gerror" + "net/http" +) + +const gfHttpClientMiddlewareKey = "__gfHttpClientMiddlewareKey" + +var gfHttpClientMiddlewareAbort = gerror.New("http request abort") + +// 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(c, req) + return resp, err + } + return c.callRequest(req) +} + +// MiddlewareAbort stop call after all middleware, so it will not send http request +// this is should only be call in ClientHandlerFunc +func (c *Client) MiddlewareAbort(req *http.Request) (*ClientResponse, error) { + m := req.Context().Value(gfHttpClientMiddlewareKey).(*clientMiddleware) + m.Abort() + return m.resp, m.err +} + +type ClientHandlerFunc = func(c *Client, r *http.Request) (*ClientResponse, error) + +// clientMiddleware is the plugin for http client request workflow management. +type clientMiddleware struct { + handlers []ClientHandlerFunc // mdl handlers + handlerIndex int // current handler index + abort bool // abort call after handlers + resp *ClientResponse // save resp + err error // save err +} + +// Next call next middleware handler, if abort, +func (m *clientMiddleware) Next(c *Client, req *http.Request) (resp *ClientResponse, err error) { + if m.abort { + return m.resp, m.err + } + if m.handlerIndex < len(m.handlers) { + m.handlerIndex++ + resp, err = m.handlers[m.handlerIndex](c, req) + m.resp = resp + m.err = err + } + return +} + +func (m *clientMiddleware) Abort() { + m.abort = true + if m.err == nil { + m.err = gfHttpClientMiddlewareAbort + } +} diff --git a/net/ghttp/ghttp_client_request.go b/net/ghttp/ghttp_client_request.go index ceb6007ef..02c3849b3 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,11 @@ 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 send http request, return *ClientResponse and error +func (c *Client) callRequest(req *http.Request) (resp *ClientResponse, err error) { resp = &ClientResponse{ request: req, } @@ -250,12 +251,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{ + 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 +306,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 e48340209..d8137eac0 100644 --- a/net/ghttp/ghttp_unit_client_test.go +++ b/net/ghttp/ghttp_unit_client_test.go @@ -7,11 +7,14 @@ 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" @@ -332,3 +335,73 @@ 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 + respStr := "test resp str" + s.BindHandler("/", func(r *ghttp.Request) { + isServerHandler = true + r.Response.Write(respStr) + }) + 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 := "" + 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" + resp, err := c.MiddlewareAbort(r) + str3 += "d" + resp, err = c.MiddlewareNext(r) + str3 += "e" + return resp, err + }).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, "acdeb") + t.Assert(err.Error(), "http request abort") + t.Assert(resp, nil) + }) +} \ No newline at end of file