add http client middleware feature

This commit is contained in:
eyasliu
2021-01-12 18:08:50 +08:00
parent 9fbdb9712b
commit 14536e9abc
4 changed files with 195 additions and 11 deletions

View File

@ -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.

View File

@ -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
}
}

View File

@ -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
}

View File

@ -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)
})
}