From 3f2ae3ba6207c9fc5df05ec45fc48d92f8e0e136 Mon Sep 17 00:00:00 2001 From: jianchenma Date: Mon, 25 Jan 2021 14:54:38 +0800 Subject: [PATCH] add tracing feature for ghttp.Client --- .../client/tracing}/jaeger/main.go | 30 +- net/ghttp/ghttp_client.go | 441 +++++++++++------- net/ghttp/ghttp_client_api.go | 295 ------------ net/ghttp/ghttp_client_middleware.go | 48 -- net/ghttp/ghttp_func.go | 51 +- net/ghttp/ghttp_middleware.go | 16 + net/ghttp/ghttp_unit_client_dump_test.go | 4 +- net/ghttp/ghttp_unit_client_test.go | 10 +- net/ghttp/internal/client/client.go | 237 ++++++++++ .../client/client_bytes.go} | 2 +- .../client/client_chain.go} | 23 +- .../client/client_content.go} | 2 +- .../client/client_dump.go} | 14 +- .../internal/client/client_middleware.go | 48 ++ .../client/client_request.go} | 61 +-- .../client/client_response.go} | 20 +- net/ghttp/internal/client/client_tracing.go | 72 +++ .../internal/client/client_tracing_tracer.go | 171 +++++++ .../client/client_var.go} | 2 +- net/ghttp/internal/httputil/httputils.go | 66 +++ net/gtrace/gtrace_http_client.go | 242 ---------- 21 files changed, 963 insertions(+), 892 deletions(-) rename .example/net/{gtrace => ghttp/client/tracing}/jaeger/main.go (74%) delete mode 100644 net/ghttp/ghttp_client_api.go delete mode 100644 net/ghttp/ghttp_client_middleware.go create mode 100644 net/ghttp/ghttp_middleware.go create mode 100644 net/ghttp/internal/client/client.go rename net/ghttp/{ghttp_client_bytes.go => internal/client/client_bytes.go} (99%) rename net/ghttp/{ghttp_client_chain.go => internal/client/client_chain.go} (91%) rename net/ghttp/{ghttp_client_content.go => internal/client/client_content.go} (99%) rename net/ghttp/{ghttp_client_dump.go => internal/client/client_dump.go} (88%) create mode 100644 net/ghttp/internal/client/client_middleware.go rename net/ghttp/{ghttp_client_request.go => internal/client/client_request.go} (83%) rename net/ghttp/{ghttp_client_response.go => internal/client/client_response.go} (74%) create mode 100644 net/ghttp/internal/client/client_tracing.go create mode 100644 net/ghttp/internal/client/client_tracing_tracer.go rename net/ghttp/{ghttp_client_var.go => internal/client/client_var.go} (99%) create mode 100644 net/ghttp/internal/httputil/httputils.go delete mode 100644 net/gtrace/gtrace_http_client.go diff --git a/.example/net/gtrace/jaeger/main.go b/.example/net/ghttp/client/tracing/jaeger/main.go similarity index 74% rename from .example/net/gtrace/jaeger/main.go rename to .example/net/ghttp/client/tracing/jaeger/main.go index 87d32d26e..c845c8307 100644 --- a/.example/net/gtrace/jaeger/main.go +++ b/.example/net/ghttp/client/tracing/jaeger/main.go @@ -20,6 +20,7 @@ import ( "context" "fmt" "github.com/gogf/gf/frame/g" + "github.com/gogf/gf/net/ghttp" "go.opentelemetry.io/otel/exporters/trace/jaeger" "go.opentelemetry.io/otel/trace" "log" @@ -57,16 +58,27 @@ func main() { flush := initTracer() defer flush() - ctx, span := otel.Tracer("component-main").Start(ctx, "foo") + ctx, span := otel.Tracer("test").Start(ctx, "test") defer span.End() - content := g.Client().Ctx(ctx).Header(g.MapStrStr{ - "test": "123", - "john": "smith", - }).Cookie(g.MapStrStr{ - "cookieKey":"cookieValue", - }).GetContent("http://baidu.com/?q=goframe") - fmt.Println(content) + for i := 0; i < 20; i++ { + g.Client().Use(ghttp.MiddlewareClientTracing).Ctx(ctx).Header(g.MapStrStr{ + "test": "123", + "john": "smith", + }).Cookie(g.MapStrStr{ + "cookieKey": "cookieValue", + }).GetContent(fmt.Sprintf("http://baidu.com/?q=test_%d", i)) + } + foo(ctx) +} + +func foo(ctx context.Context) { + ctx, span := otel.Tracer("test").Start(ctx, "foo") + defer span.End() + span.AddEvent("Nice operation!", trace.WithAttributes(label.Int("bogons", 100))) + span.SetAttributes(label.String("test2", "123")) + time.Sleep(time.Second * 1) + bar(ctx) } func bar(ctx context.Context) { @@ -74,6 +86,6 @@ func bar(ctx context.Context) { defer span.End() span.AddEvent("Nice operation!", trace.WithAttributes(label.Int("bogons", 100))) span.SetAttributes(label.String("test2", "123")) - time.Sleep(time.Second * 2) + time.Sleep(time.Second * 1) // Do bar... } diff --git a/net/ghttp/ghttp_client.go b/net/ghttp/ghttp_client.go index 089efc7c9..c88e0ef3d 100644 --- a/net/ghttp/ghttp_client.go +++ b/net/ghttp/ghttp_client.go @@ -7,224 +7,303 @@ package ghttp import ( - "context" - "crypto/tls" - "fmt" - "github.com/gogf/gf" - "github.com/gogf/gf/text/gstr" - "golang.org/x/net/proxy" - "net" - "net/http" - "net/url" - "strings" - "time" - - "github.com/gogf/gf/text/gregex" + "github.com/gogf/gf/container/gvar" + "github.com/gogf/gf/net/ghttp/internal/client" ) -// Client is the HTTP client for HTTP request management. -type Client struct { - http.Client // Underlying HTTP Client. - ctx context.Context // Context for each request. - agent string // Client agent. - 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 -} - -var ( - defaultClientAgent = fmt.Sprintf(`GoFrameHTTPClient %s`, gf.VERSION) +type ( + Client = client.Client + ClientResponse = client.Response + ClientHandlerFunc = client.HandlerFunc ) -// NewClient creates and returns a new HTTP client object. +// New creates and returns a new HTTP client object. func NewClient() *Client { - return &Client{ - Client: http.Client{ - Transport: &http.Transport{ - // No validation for https certification of the server in default. - TLSClientConfig: &tls.Config{ - InsecureSkipVerify: true, - }, - DisableKeepAlives: true, - }, - }, - header: make(map[string]string), - cookies: make(map[string]string), - agent: defaultClientAgent, - } + return client.New() } -// Clone clones current client and returns a new one. -func (c *Client) Clone() *Client { - newClient := NewClient() - *newClient = *c - newClient.header = make(map[string]string) - newClient.cookies = make(map[string]string) - for k, v := range c.header { - newClient.header[k] = v - } - for k, v := range c.cookies { - newClient.cookies[k] = v - } - return newClient +// Get is a convenience method for sending GET request. +// NOTE that remembers CLOSING the response object when it'll never be used. +// Deprecated, please use g.Client().Get or NewClient().Get instead. +func Get(url string, data ...interface{}) (*ClientResponse, error) { + return DoRequest("GET", url, data...) } -// SetBrowserMode enables browser mode of the client. -// When browser mode is enabled, it automatically saves and sends cookie content -// from and to server. -func (c *Client) SetBrowserMode(enabled bool) *Client { - c.browserMode = enabled - return c +// Put is a convenience method for sending PUT request. +// NOTE that remembers CLOSING the response object when it'll never be used. +// Deprecated, please use g.Client().Put or NewClient().Put instead. +func Put(url string, data ...interface{}) (*ClientResponse, error) { + return DoRequest("PUT", url, data...) } -// SetHeader sets a custom HTTP header pair for the client. -func (c *Client) SetHeader(key, value string) *Client { - c.header[key] = value - return c +// Post is a convenience method for sending POST request. +// NOTE that remembers CLOSING the response object when it'll never be used. +// Deprecated, please use g.Client().Post or NewClient().Post instead. +func Post(url string, data ...interface{}) (*ClientResponse, error) { + return DoRequest("POST", url, data...) } -// SetHeaderMap sets custom HTTP headers with map. -func (c *Client) SetHeaderMap(m map[string]string) *Client { - for k, v := range m { - c.header[k] = v - } - return c +// Delete is a convenience method for sending DELETE request. +// NOTE that remembers CLOSING the response object when it'll never be used. +// Deprecated, please use g.Client().Delete or NewClient().Delete instead. +func Delete(url string, data ...interface{}) (*ClientResponse, error) { + return DoRequest("DELETE", url, data...) } -// SetAgent sets the User-Agent header for client. -func (c *Client) SetAgent(agent string) *Client { - c.header["User-Agent"] = agent - return c +// Head is a convenience method for sending HEAD request. +// NOTE that remembers CLOSING the response object when it'll never be used. +// Deprecated, please use g.Client().Head or NewClient().Head instead. +func Head(url string, data ...interface{}) (*ClientResponse, error) { + return DoRequest("HEAD", url, data...) } -// SetContentType sets HTTP content type for the client. -func (c *Client) SetContentType(contentType string) *Client { - c.header["Content-Type"] = contentType - return c +// Patch is a convenience method for sending PATCH request. +// NOTE that remembers CLOSING the response object when it'll never be used. +// Deprecated, please use g.Client().Patch or NewClient().Patch instead. +func Patch(url string, data ...interface{}) (*ClientResponse, error) { + return DoRequest("PATCH", url, data...) } -// SetHeaderRaw sets custom HTTP header using raw string. -func (c *Client) SetHeaderRaw(headers string) *Client { - for _, line := range gstr.SplitAndTrim(headers, "\n") { - array, _ := gregex.MatchString(`^([\w\-]+):\s*(.+)`, line) - if len(array) >= 3 { - c.header[array[1]] = array[2] - } - } - return c +// Connect is a convenience method for sending CONNECT request. +// NOTE that remembers CLOSING the response object when it'll never be used. +// Deprecated, please use g.Client().Connect or NewClient().Connect instead. +func Connect(url string, data ...interface{}) (*ClientResponse, error) { + return DoRequest("CONNECT", url, data...) } -// SetCookie sets a cookie pair for the client. -func (c *Client) SetCookie(key, value string) *Client { - c.cookies[key] = value - return c +// Options is a convenience method for sending OPTIONS request. +// NOTE that remembers CLOSING the response object when it'll never be used. +// Deprecated, please use g.Client().Options or NewClient().Options instead. +func Options(url string, data ...interface{}) (*ClientResponse, error) { + return DoRequest("OPTIONS", url, data...) } -// SetCookieMap sets cookie items with map. -func (c *Client) SetCookieMap(m map[string]string) *Client { - for k, v := range m { - c.cookies[k] = v - } - return c +// Trace is a convenience method for sending TRACE request. +// NOTE that remembers CLOSING the response object when it'll never be used. +// Deprecated, please use g.Client().Trace or NewClient().Trace instead. +func Trace(url string, data ...interface{}) (*ClientResponse, error) { + return DoRequest("TRACE", url, data...) } -// SetPrefix sets the request server URL prefix. -func (c *Client) SetPrefix(prefix string) *Client { - c.prefix = prefix - return c +// DoRequest is a convenience method for sending custom http method request. +// NOTE that remembers CLOSING the response object when it'll never be used. +// Deprecated, please use g.Client().DoRequest or NewClient().DoRequest instead. +func DoRequest(method, url string, data ...interface{}) (*ClientResponse, error) { + return client.New().DoRequest(method, url, data...) } -// SetTimeOut sets the request timeout for the client. -func (c *Client) SetTimeout(t time.Duration) *Client { - c.Client.Timeout = t - return c +// GetContent is a convenience method for sending GET request, which retrieves and returns +// the result content and automatically closes response object. +// Deprecated, please use g.Client().GetContent or NewClient().GetContent instead. +func GetContent(url string, data ...interface{}) string { + return RequestContent("GET", url, data...) } -// SetBasicAuth sets HTTP basic authentication information for the client. -func (c *Client) SetBasicAuth(user, pass string) *Client { - c.authUser = user - c.authPass = pass - return c +// PutContent is a convenience method for sending PUT request, which retrieves and returns +// the result content and automatically closes response object. +// Deprecated, please use g.Client().PutContent or NewClient().PutContent instead. +func PutContent(url string, data ...interface{}) string { + return RequestContent("PUT", url, data...) } -// SetCtx sets context for each request of this client. -func (c *Client) SetCtx(ctx context.Context) *Client { - c.ctx = ctx - return c +// PostContent is a convenience method for sending POST request, which retrieves and returns +// the result content and automatically closes response object. +// Deprecated, please use g.Client().PostContent or NewClient().PostContent instead. +func PostContent(url string, data ...interface{}) string { + return RequestContent("POST", url, data...) } -// SetRetry sets retry count and interval. -func (c *Client) SetRetry(retryCount int, retryInterval time.Duration) *Client { - c.retryCount = retryCount - c.retryInterval = retryInterval - return c +// DeleteContent is a convenience method for sending DELETE request, which retrieves and returns +// the result content and automatically closes response object. +// Deprecated, please use g.Client().DeleteContent or NewClient().DeleteContent instead. +func DeleteContent(url string, data ...interface{}) string { + return RequestContent("DELETE", url, data...) } -// SetRedirectLimit limit the number of jumps -func (c *Client) SetRedirectLimit(redirectLimit int) *Client { - c.CheckRedirect = func(req *http.Request, via []*http.Request) error { - if len(via) >= redirectLimit { - return http.ErrUseLastResponse - } - return nil - } - return c +// HeadContent is a convenience method for sending HEAD request, which retrieves and returns +// the result content and automatically closes response object. +// Deprecated, please use g.Client().HeadContent or NewClient().HeadContent instead. +func HeadContent(url string, data ...interface{}) string { + return RequestContent("HEAD", url, data...) } -// SetProxy set proxy for the client. -// This func will do nothing when the parameter `proxyURL` is empty or in wrong pattern. -// The correct pattern is like `http://USER:PASSWORD@IP:PORT` or `socks5://USER:PASSWORD@IP:PORT`. -// Only `http` and `socks5` proxies are supported currently. -func (c *Client) SetProxy(proxyURL string) { - if strings.TrimSpace(proxyURL) == "" { - return - } - _proxy, err := url.Parse(proxyURL) +// PatchContent is a convenience method for sending PATCH request, which retrieves and returns +// the result content and automatically closes response object. +// Deprecated, please use g.Client().PatchContent or NewClient().PatchContent instead. +func PatchContent(url string, data ...interface{}) string { + return RequestContent("PATCH", url, data...) +} + +// ConnectContent is a convenience method for sending CONNECT request, which retrieves and returns +// the result content and automatically closes response object. +// Deprecated, please use g.Client().ConnectContent or NewClient().ConnectContent instead. +func ConnectContent(url string, data ...interface{}) string { + return RequestContent("CONNECT", url, data...) +} + +// OptionsContent is a convenience method for sending OPTIONS request, which retrieves and returns +// the result content and automatically closes response object. +// Deprecated, please use g.Client().OptionsContent or NewClient().OptionsContent instead. +func OptionsContent(url string, data ...interface{}) string { + return RequestContent("OPTIONS", url, data...) +} + +// TraceContent is a convenience method for sending TRACE request, which retrieves and returns +// the result content and automatically closes response object. +// Deprecated, please use g.Client().TraceContent or NewClient().TraceContent instead. +func TraceContent(url string, data ...interface{}) string { + return RequestContent("TRACE", url, data...) +} + +// RequestContent is a convenience method for sending custom http method request, which +// retrieves and returns the result content and automatically closes response object. +// Deprecated, please use g.Client().RequestContent or NewClient().RequestContent instead. +func RequestContent(method string, url string, data ...interface{}) string { + return client.New().RequestContent(method, url, data...) +} + +// GetBytes is a convenience method for sending GET request, which retrieves and returns +// the result content as bytes and automatically closes response object. +// Deprecated, please use g.Client().GetBytes or NewClient().GetBytes instead. +func GetBytes(url string, data ...interface{}) []byte { + return RequestBytes("GET", url, data...) +} + +// PutBytes is a convenience method for sending PUT request, which retrieves and returns +// the result content as bytes and automatically closes response object. +// Deprecated, please use g.Client().PutBytes or NewClient().PutBytes instead. +func PutBytes(url string, data ...interface{}) []byte { + return RequestBytes("PUT", url, data...) +} + +// PostBytes is a convenience method for sending POST request, which retrieves and returns +// the result content as bytes and automatically closes response object. +// Deprecated, please use g.Client().PostBytes or NewClient().PostBytes instead. +func PostBytes(url string, data ...interface{}) []byte { + return RequestBytes("POST", url, data...) +} + +// DeleteBytes is a convenience method for sending DELETE request, which retrieves and returns +// the result content as bytes and automatically closes response object. +// Deprecated, please use g.Client().DeleteBytes or NewClient().DeleteBytes instead. +func DeleteBytes(url string, data ...interface{}) []byte { + return RequestBytes("DELETE", url, data...) +} + +// HeadBytes is a convenience method for sending HEAD request, which retrieves and returns +// the result content as bytes and automatically closes response object. +// Deprecated, please use g.Client().HeadBytes or NewClient().HeadBytes instead. +func HeadBytes(url string, data ...interface{}) []byte { + return RequestBytes("HEAD", url, data...) +} + +// PatchBytes is a convenience method for sending PATCH request, which retrieves and returns +// the result content as bytes and automatically closes response object. +// Deprecated, please use g.Client().PatchBytes or NewClient().PatchBytes instead. +func PatchBytes(url string, data ...interface{}) []byte { + return RequestBytes("PATCH", url, data...) +} + +// ConnectBytes is a convenience method for sending CONNECT request, which retrieves and returns +// the result content as bytes and automatically closes response object. +// Deprecated, please use g.Client().ConnectBytes or NewClient().ConnectBytes instead. +func ConnectBytes(url string, data ...interface{}) []byte { + return RequestBytes("CONNECT", url, data...) +} + +// OptionsBytes is a convenience method for sending OPTIONS request, which retrieves and returns +// the result content as bytes and automatically closes response object. +// Deprecated, please use g.Client().OptionsBytes or NewClient().OptionsBytes instead. +func OptionsBytes(url string, data ...interface{}) []byte { + return RequestBytes("OPTIONS", url, data...) +} + +// TraceBytes is a convenience method for sending TRACE request, which retrieves and returns +// the result content as bytes and automatically closes response object. +// Deprecated, please use g.Client().TraceBytes or NewClient().TraceBytes instead. +func TraceBytes(url string, data ...interface{}) []byte { + return RequestBytes("TRACE", url, data...) +} + +// RequestBytes is a convenience method for sending custom http method request, which +// retrieves and returns the result content as bytes and automatically closes response object. +// Deprecated, please use g.Client().RequestBytes or NewClient().RequestBytes instead. +func RequestBytes(method string, url string, data ...interface{}) []byte { + return client.New().RequestBytes(method, url, data...) +} + +// GetVar sends a GET request, retrieves and converts the result content to specified pointer. +// The parameter can be type of: struct/*struct/**struct/[]struct/[]*struct/*[]struct, et +// Deprecated, please use g.Client().GetVar or NewClient().GetVar instead. +func GetVar(url string, data ...interface{}) *gvar.Var { + return RequestVar("GET", url, data...) +} + +// PutVar sends a PUT request, retrieves and converts the result content to specified pointer. +// The parameter can be type of: struct/*struct/**struct/[]struct/[]*struct/*[]struct, et +// Deprecated, please use g.Client().PutVar or NewClient().PutVar instead. +func PutVar(url string, data ...interface{}) *gvar.Var { + return RequestVar("PUT", url, data...) +} + +// PostVar sends a POST request, retrieves and converts the result content to specified pointer. +// The parameter can be type of: struct/*struct/**struct/[]struct/[]*struct/*[]struct, et +// Deprecated, please use g.Client().PostVar or NewClient().PostVar instead. +func PostVar(url string, data ...interface{}) *gvar.Var { + return RequestVar("POST", url, data...) +} + +// DeleteVar sends a DELETE request, retrieves and converts the result content to specified pointer. +// The parameter can be type of: struct/*struct/**struct/[]struct/[]*struct/*[]struct, et +// Deprecated, please use g.Client().DeleteVar or NewClient().DeleteVar instead. +func DeleteVar(url string, data ...interface{}) *gvar.Var { + return RequestVar("DELETE", url, data...) +} + +// HeadVar sends a HEAD request, retrieves and converts the result content to specified pointer. +// The parameter can be type of: struct/*struct/**struct/[]struct/[]*struct/*[]struct, et +// Deprecated, please use g.Client().HeadVar or NewClient().HeadVar instead. +func HeadVar(url string, data ...interface{}) *gvar.Var { + return RequestVar("HEAD", url, data...) +} + +// PatchVar sends a PATCH request, retrieves and converts the result content to specified pointer. +// The parameter can be type of: struct/*struct/**struct/[]struct/[]*struct/*[]struct, et +// Deprecated, please use g.Client().PatchVar or NewClient().PatchVar instead. +func PatchVar(url string, data ...interface{}) *gvar.Var { + return RequestVar("PATCH", url, data...) +} + +// ConnectVar sends a CONNECT request, retrieves and converts the result content to specified pointer. +// The parameter can be type of: struct/*struct/**struct/[]struct/[]*struct/*[]struct, et +// Deprecated, please use g.Client().ConnectVar or NewClient().ConnectVar instead. +func ConnectVar(url string, data ...interface{}) *gvar.Var { + return RequestVar("CONNECT", url, data...) +} + +// OptionsVar sends a OPTIONS request, retrieves and converts the result content to specified pointer. +// The parameter can be type of: struct/*struct/**struct/[]struct/[]*struct/*[]struct, et +// Deprecated, please use g.Client().OptionsVar or NewClient().OptionsVar instead. +func OptionsVar(url string, data ...interface{}) *gvar.Var { + return RequestVar("OPTIONS", url, data...) +} + +// TraceVar sends a TRACE request, retrieves and converts the result content to specified pointer. +// The parameter can be type of: struct/*struct/**struct/[]struct/[]*struct/*[]struct, et +// Deprecated, please use g.Client().TraceVar or NewClient().TraceVar instead. +func TraceVar(url string, data ...interface{}) *gvar.Var { + return RequestVar("TRACE", url, data...) +} + +// RequestVar sends request using given HTTP method and data, retrieves converts the result +// to specified pointer. It reads and closes the response object internally automatically. +// The parameter can be type of: struct/*struct/**struct/[]struct/[]*struct/*[]struct, et +// Deprecated, please use g.Client().RequestVar or NewClient().RequestVar instead. +func RequestVar(method string, url string, data ...interface{}) *gvar.Var { + response, err := DoRequest(method, url, data...) if err != nil { - return - } - if _proxy.Scheme == "http" { - if _, ok := c.Transport.(*http.Transport); ok { - c.Transport.(*http.Transport).Proxy = http.ProxyURL(_proxy) - } - } else { - var auth = &proxy.Auth{} - user := _proxy.User.Username() - - if user != "" { - auth.User = user - password, hasPassword := _proxy.User.Password() - if hasPassword && password != "" { - auth.Password = password - } - } else { - auth = nil - } - // refer to the source code, error is always nil - dialer, err := proxy.SOCKS5( - "tcp", - _proxy.Host, - auth, - &net.Dialer{ - Timeout: c.Client.Timeout, - KeepAlive: c.Client.Timeout, - }, - ) - if err != nil { - return - } - if _, ok := c.Transport.(*http.Transport); ok { - c.Transport.(*http.Transport).DialContext = func(ctx context.Context, network, addr string) (conn net.Conn, e error) { - return dialer.Dial(network, addr) - } - } - //c.SetTimeout(10*time.Second) + return gvar.New(nil) } + defer response.Close() + return gvar.New(response.ReadAll()) } diff --git a/net/ghttp/ghttp_client_api.go b/net/ghttp/ghttp_client_api.go deleted file mode 100644 index 737f02f61..000000000 --- a/net/ghttp/ghttp_client_api.go +++ /dev/null @@ -1,295 +0,0 @@ -// 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 ghttp - -import "github.com/gogf/gf/container/gvar" - -// Get is a convenience method for sending GET request. -// NOTE that remembers CLOSING the response object when it'll never be used. -// Deprecated, please use g.Client().Get or NewClient().Get instead. -func Get(url string, data ...interface{}) (*ClientResponse, error) { - return DoRequest("GET", url, data...) -} - -// Put is a convenience method for sending PUT request. -// NOTE that remembers CLOSING the response object when it'll never be used. -// Deprecated, please use g.Client().Put or NewClient().Put instead. -func Put(url string, data ...interface{}) (*ClientResponse, error) { - return DoRequest("PUT", url, data...) -} - -// Post is a convenience method for sending POST request. -// NOTE that remembers CLOSING the response object when it'll never be used. -// Deprecated, please use g.Client().Post or NewClient().Post instead. -func Post(url string, data ...interface{}) (*ClientResponse, error) { - return DoRequest("POST", url, data...) -} - -// Delete is a convenience method for sending DELETE request. -// NOTE that remembers CLOSING the response object when it'll never be used. -// Deprecated, please use g.Client().Delete or NewClient().Delete instead. -func Delete(url string, data ...interface{}) (*ClientResponse, error) { - return DoRequest("DELETE", url, data...) -} - -// Head is a convenience method for sending HEAD request. -// NOTE that remembers CLOSING the response object when it'll never be used. -// Deprecated, please use g.Client().Head or NewClient().Head instead. -func Head(url string, data ...interface{}) (*ClientResponse, error) { - return DoRequest("HEAD", url, data...) -} - -// Patch is a convenience method for sending PATCH request. -// NOTE that remembers CLOSING the response object when it'll never be used. -// Deprecated, please use g.Client().Patch or NewClient().Patch instead. -func Patch(url string, data ...interface{}) (*ClientResponse, error) { - return DoRequest("PATCH", url, data...) -} - -// Connect is a convenience method for sending CONNECT request. -// NOTE that remembers CLOSING the response object when it'll never be used. -// Deprecated, please use g.Client().Connect or NewClient().Connect instead. -func Connect(url string, data ...interface{}) (*ClientResponse, error) { - return DoRequest("CONNECT", url, data...) -} - -// Options is a convenience method for sending OPTIONS request. -// NOTE that remembers CLOSING the response object when it'll never be used. -// Deprecated, please use g.Client().Options or NewClient().Options instead. -func Options(url string, data ...interface{}) (*ClientResponse, error) { - return DoRequest("OPTIONS", url, data...) -} - -// Trace is a convenience method for sending TRACE request. -// NOTE that remembers CLOSING the response object when it'll never be used. -// Deprecated, please use g.Client().Trace or NewClient().Trace instead. -func Trace(url string, data ...interface{}) (*ClientResponse, error) { - return DoRequest("TRACE", url, data...) -} - -// DoRequest is a convenience method for sending custom http method request. -// NOTE that remembers CLOSING the response object when it'll never be used. -// Deprecated, please use g.Client().DoRequest or NewClient().DoRequest instead. -func DoRequest(method, url string, data ...interface{}) (*ClientResponse, error) { - return NewClient().DoRequest(method, url, data...) -} - -// GetContent is a convenience method for sending GET request, which retrieves and returns -// the result content and automatically closes response object. -// Deprecated, please use g.Client().GetContent or NewClient().GetContent instead. -func GetContent(url string, data ...interface{}) string { - return RequestContent("GET", url, data...) -} - -// PutContent is a convenience method for sending PUT request, which retrieves and returns -// the result content and automatically closes response object. -// Deprecated, please use g.Client().PutContent or NewClient().PutContent instead. -func PutContent(url string, data ...interface{}) string { - return RequestContent("PUT", url, data...) -} - -// PostContent is a convenience method for sending POST request, which retrieves and returns -// the result content and automatically closes response object. -// Deprecated, please use g.Client().PostContent or NewClient().PostContent instead. -func PostContent(url string, data ...interface{}) string { - return RequestContent("POST", url, data...) -} - -// DeleteContent is a convenience method for sending DELETE request, which retrieves and returns -// the result content and automatically closes response object. -// Deprecated, please use g.Client().DeleteContent or NewClient().DeleteContent instead. -func DeleteContent(url string, data ...interface{}) string { - return RequestContent("DELETE", url, data...) -} - -// HeadContent is a convenience method for sending HEAD request, which retrieves and returns -// the result content and automatically closes response object. -// Deprecated, please use g.Client().HeadContent or NewClient().HeadContent instead. -func HeadContent(url string, data ...interface{}) string { - return RequestContent("HEAD", url, data...) -} - -// PatchContent is a convenience method for sending PATCH request, which retrieves and returns -// the result content and automatically closes response object. -// Deprecated, please use g.Client().PatchContent or NewClient().PatchContent instead. -func PatchContent(url string, data ...interface{}) string { - return RequestContent("PATCH", url, data...) -} - -// ConnectContent is a convenience method for sending CONNECT request, which retrieves and returns -// the result content and automatically closes response object. -// Deprecated, please use g.Client().ConnectContent or NewClient().ConnectContent instead. -func ConnectContent(url string, data ...interface{}) string { - return RequestContent("CONNECT", url, data...) -} - -// OptionsContent is a convenience method for sending OPTIONS request, which retrieves and returns -// the result content and automatically closes response object. -// Deprecated, please use g.Client().OptionsContent or NewClient().OptionsContent instead. -func OptionsContent(url string, data ...interface{}) string { - return RequestContent("OPTIONS", url, data...) -} - -// TraceContent is a convenience method for sending TRACE request, which retrieves and returns -// the result content and automatically closes response object. -// Deprecated, please use g.Client().TraceContent or NewClient().TraceContent instead. -func TraceContent(url string, data ...interface{}) string { - return RequestContent("TRACE", url, data...) -} - -// RequestContent is a convenience method for sending custom http method request, which -// retrieves and returns the result content and automatically closes response object. -// Deprecated, please use g.Client().RequestContent or NewClient().RequestContent instead. -func RequestContent(method string, url string, data ...interface{}) string { - return NewClient().RequestContent(method, url, data...) -} - -// GetBytes is a convenience method for sending GET request, which retrieves and returns -// the result content as bytes and automatically closes response object. -// Deprecated, please use g.Client().GetBytes or NewClient().GetBytes instead. -func GetBytes(url string, data ...interface{}) []byte { - return RequestBytes("GET", url, data...) -} - -// PutBytes is a convenience method for sending PUT request, which retrieves and returns -// the result content as bytes and automatically closes response object. -// Deprecated, please use g.Client().PutBytes or NewClient().PutBytes instead. -func PutBytes(url string, data ...interface{}) []byte { - return RequestBytes("PUT", url, data...) -} - -// PostBytes is a convenience method for sending POST request, which retrieves and returns -// the result content as bytes and automatically closes response object. -// Deprecated, please use g.Client().PostBytes or NewClient().PostBytes instead. -func PostBytes(url string, data ...interface{}) []byte { - return RequestBytes("POST", url, data...) -} - -// DeleteBytes is a convenience method for sending DELETE request, which retrieves and returns -// the result content as bytes and automatically closes response object. -// Deprecated, please use g.Client().DeleteBytes or NewClient().DeleteBytes instead. -func DeleteBytes(url string, data ...interface{}) []byte { - return RequestBytes("DELETE", url, data...) -} - -// HeadBytes is a convenience method for sending HEAD request, which retrieves and returns -// the result content as bytes and automatically closes response object. -// Deprecated, please use g.Client().HeadBytes or NewClient().HeadBytes instead. -func HeadBytes(url string, data ...interface{}) []byte { - return RequestBytes("HEAD", url, data...) -} - -// PatchBytes is a convenience method for sending PATCH request, which retrieves and returns -// the result content as bytes and automatically closes response object. -// Deprecated, please use g.Client().PatchBytes or NewClient().PatchBytes instead. -func PatchBytes(url string, data ...interface{}) []byte { - return RequestBytes("PATCH", url, data...) -} - -// ConnectBytes is a convenience method for sending CONNECT request, which retrieves and returns -// the result content as bytes and automatically closes response object. -// Deprecated, please use g.Client().ConnectBytes or NewClient().ConnectBytes instead. -func ConnectBytes(url string, data ...interface{}) []byte { - return RequestBytes("CONNECT", url, data...) -} - -// OptionsBytes is a convenience method for sending OPTIONS request, which retrieves and returns -// the result content as bytes and automatically closes response object. -// Deprecated, please use g.Client().OptionsBytes or NewClient().OptionsBytes instead. -func OptionsBytes(url string, data ...interface{}) []byte { - return RequestBytes("OPTIONS", url, data...) -} - -// TraceBytes is a convenience method for sending TRACE request, which retrieves and returns -// the result content as bytes and automatically closes response object. -// Deprecated, please use g.Client().TraceBytes or NewClient().TraceBytes instead. -func TraceBytes(url string, data ...interface{}) []byte { - return RequestBytes("TRACE", url, data...) -} - -// RequestBytes is a convenience method for sending custom http method request, which -// retrieves and returns the result content as bytes and automatically closes response object. -// Deprecated, please use g.Client().RequestBytes or NewClient().RequestBytes instead. -func RequestBytes(method string, url string, data ...interface{}) []byte { - return NewClient().RequestBytes(method, url, data...) -} - -// GetVar sends a GET request, retrieves and converts the result content to specified pointer. -// The parameter can be type of: struct/*struct/**struct/[]struct/[]*struct/*[]struct, et -// Deprecated, please use g.Client().GetVar or NewClient().GetVar instead. -func GetVar(url string, data ...interface{}) *gvar.Var { - return RequestVar("GET", url, data...) -} - -// PutVar sends a PUT request, retrieves and converts the result content to specified pointer. -// The parameter can be type of: struct/*struct/**struct/[]struct/[]*struct/*[]struct, et -// Deprecated, please use g.Client().PutVar or NewClient().PutVar instead. -func PutVar(url string, data ...interface{}) *gvar.Var { - return RequestVar("PUT", url, data...) -} - -// PostVar sends a POST request, retrieves and converts the result content to specified pointer. -// The parameter can be type of: struct/*struct/**struct/[]struct/[]*struct/*[]struct, et -// Deprecated, please use g.Client().PostVar or NewClient().PostVar instead. -func PostVar(url string, data ...interface{}) *gvar.Var { - return RequestVar("POST", url, data...) -} - -// DeleteVar sends a DELETE request, retrieves and converts the result content to specified pointer. -// The parameter can be type of: struct/*struct/**struct/[]struct/[]*struct/*[]struct, et -// Deprecated, please use g.Client().DeleteVar or NewClient().DeleteVar instead. -func DeleteVar(url string, data ...interface{}) *gvar.Var { - return RequestVar("DELETE", url, data...) -} - -// HeadVar sends a HEAD request, retrieves and converts the result content to specified pointer. -// The parameter can be type of: struct/*struct/**struct/[]struct/[]*struct/*[]struct, et -// Deprecated, please use g.Client().HeadVar or NewClient().HeadVar instead. -func HeadVar(url string, data ...interface{}) *gvar.Var { - return RequestVar("HEAD", url, data...) -} - -// PatchVar sends a PATCH request, retrieves and converts the result content to specified pointer. -// The parameter can be type of: struct/*struct/**struct/[]struct/[]*struct/*[]struct, et -// Deprecated, please use g.Client().PatchVar or NewClient().PatchVar instead. -func PatchVar(url string, data ...interface{}) *gvar.Var { - return RequestVar("PATCH", url, data...) -} - -// ConnectVar sends a CONNECT request, retrieves and converts the result content to specified pointer. -// The parameter can be type of: struct/*struct/**struct/[]struct/[]*struct/*[]struct, et -// Deprecated, please use g.Client().ConnectVar or NewClient().ConnectVar instead. -func ConnectVar(url string, data ...interface{}) *gvar.Var { - return RequestVar("CONNECT", url, data...) -} - -// OptionsVar sends a OPTIONS request, retrieves and converts the result content to specified pointer. -// The parameter can be type of: struct/*struct/**struct/[]struct/[]*struct/*[]struct, et -// Deprecated, please use g.Client().OptionsVar or NewClient().OptionsVar instead. -func OptionsVar(url string, data ...interface{}) *gvar.Var { - return RequestVar("OPTIONS", url, data...) -} - -// TraceVar sends a TRACE request, retrieves and converts the result content to specified pointer. -// The parameter can be type of: struct/*struct/**struct/[]struct/[]*struct/*[]struct, et -// Deprecated, please use g.Client().TraceVar or NewClient().TraceVar instead. -func TraceVar(url string, data ...interface{}) *gvar.Var { - return RequestVar("TRACE", url, data...) -} - -// RequestVar sends request using given HTTP method and data, retrieves converts the result -// to specified pointer. It reads and closes the response object internally automatically. -// The parameter can be type of: struct/*struct/**struct/[]struct/[]*struct/*[]struct, et -// Deprecated, please use g.Client().RequestVar or NewClient().RequestVar instead. -func RequestVar(method string, url string, data ...interface{}) *gvar.Var { - response, err := DoRequest(method, url, data...) - if err != nil { - return gvar.New(nil) - } - defer response.Close() - return gvar.New(response.ReadAll()) -} diff --git a/net/ghttp/ghttp_client_middleware.go b/net/ghttp/ghttp_client_middleware.go deleted file mode 100644 index 505d85bf5..000000000 --- a/net/ghttp/ghttp_client_middleware.go +++ /dev/null @@ -1,48 +0,0 @@ -package ghttp - -import ( - "net/http" -) - -// 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. -} - -const clientMiddlewareKey = "__clientMiddlewareKey" - -// Use adds one or more middleware handlers to client. -func (c *Client) Use(handlers ...ClientHandlerFunc) *Client { - c.middlewareHandler = append(c.middlewareHandler, handlers...) - return c -} - -// MiddlewareNext calls next middleware. -// This is should only be call in ClientHandlerFunc. -func (c *Client) MiddlewareNext(req *http.Request) (*ClientResponse, error) { - if v := req.Context().Value(clientMiddlewareKey); v != nil { - if m, ok := v.(*clientMiddleware); ok { - return m.Next(req) - } - } - return c.callRequest(req) -} - -// Next calls next middleware handler. -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++ - m.resp, m.err = m.handlers[m.handlerIndex](m.client, req) - } - return m.resp, m.err -} diff --git a/net/ghttp/ghttp_func.go b/net/ghttp/ghttp_func.go index 3a03bdfea..7047f0317 100644 --- a/net/ghttp/ghttp_func.go +++ b/net/ghttp/ghttp_func.go @@ -8,15 +8,7 @@ package ghttp import ( "github.com/gogf/gf/errors/gerror" - "github.com/gogf/gf/text/gstr" - "strings" - - "github.com/gogf/gf/encoding/gurl" - "github.com/gogf/gf/util/gconv" -) - -const ( - fileUploadingKey = "@file:" + "github.com/gogf/gf/net/ghttp/internal/httputil" ) // BuildParams builds the request string for the http client. The can be type of: @@ -24,46 +16,7 @@ const ( // // The optional parameter specifies whether ignore the url encoding for the data. func BuildParams(params interface{}, noUrlEncode ...bool) (encodedParamStr string) { - // If given string/[]byte, converts and returns it directly as string. - switch v := params.(type) { - case string, []byte: - return gconv.String(params) - case []interface{}: - if len(v) > 0 { - params = v[0] - } else { - params = nil - } - } - // Else converts it to map and does the url encoding. - m, urlEncode := gconv.Map(params), true - if len(m) == 0 { - return gconv.String(params) - } - if len(noUrlEncode) == 1 { - urlEncode = !noUrlEncode[0] - } - // If there's file uploading, it ignores the url encoding. - if urlEncode { - for k, v := range m { - if gstr.Contains(k, fileUploadingKey) || gstr.Contains(gconv.String(v), fileUploadingKey) { - urlEncode = false - break - } - } - } - s := "" - for k, v := range m { - if len(encodedParamStr) > 0 { - encodedParamStr += "&" - } - s = gconv.String(v) - if urlEncode && len(s) > 6 && strings.Compare(s[0:6], fileUploadingKey) != 0 { - s = gurl.Encode(s) - } - encodedParamStr += k + "=" + s - } - return + return httputil.BuildParams(params, noUrlEncode...) } // niceCallFunc calls function with exception capture logic. diff --git a/net/ghttp/ghttp_middleware.go b/net/ghttp/ghttp_middleware.go new file mode 100644 index 000000000..9531ca378 --- /dev/null +++ b/net/ghttp/ghttp_middleware.go @@ -0,0 +1,16 @@ +// 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 ghttp + +import ( + "github.com/gogf/gf/net/ghttp/internal/client" + "net/http" +) + +func MiddlewareClientTracing(c *Client, r *http.Request) (*ClientResponse, error) { + return client.MiddlewareTracing(c, r) +} diff --git a/net/ghttp/ghttp_unit_client_dump_test.go b/net/ghttp/ghttp_unit_client_dump_test.go index 9852d2c92..e6bd789dc 100644 --- a/net/ghttp/ghttp_unit_client_dump_test.go +++ b/net/ghttp/ghttp_unit_client_dump_test.go @@ -36,7 +36,7 @@ func Test_Client_Request_13_Dump(t *testing.T) { time.Sleep(100 * time.Millisecond) gtest.C(t, func(t *gtest.T) { url := fmt.Sprintf("http://127.0.0.1:%d", p) - client := g.Client().SetPrefix(url).ContentJson() + client := g.Client().SetPrefix(url).ContentJson().SetDump(true) r, err := client.Post("/hello", g.Map{"field": "test_for_request_body"}) t.Assert(err, nil) dumpedText := r.RawRequest() @@ -46,7 +46,7 @@ func Test_Client_Request_13_Dump(t *testing.T) { t.Assert(gstr.Contains(dumpedText2, "test_for_response_body"), true) client2 := g.Client().SetPrefix(url).ContentType("text/html") - r2, err := client2.Post("/hello2", g.Map{"field": "test_for_request_body"}) + r2, err := client2.Dump().Post("/hello2", g.Map{"field": "test_for_request_body"}) t.Assert(err, nil) dumpedText3 := r2.RawRequest() t.Assert(gstr.Contains(dumpedText3, "test_for_request_body"), true) diff --git a/net/ghttp/ghttp_unit_client_test.go b/net/ghttp/ghttp_unit_client_test.go index 468483b2a..5f8c73112 100644 --- a/net/ghttp/ghttp_unit_client_test.go +++ b/net/ghttp/ghttp_unit_client_test.go @@ -360,7 +360,7 @@ func Test_Client_Middleware(t *testing.T) { c := g.Client().SetPrefix(fmt.Sprintf("http://127.0.0.1:%d", p)) c.Use(func(c *ghttp.Client, r *http.Request) (resp *ghttp.ClientResponse, err error) { str1 += "a" - resp, err = c.MiddlewareNext(r) + resp, err = c.Next(r) if err != nil { return nil, err } @@ -369,7 +369,7 @@ func Test_Client_Middleware(t *testing.T) { }) c.Use(func(c *ghttp.Client, r *http.Request) (resp *ghttp.ClientResponse, err error) { str1 += "c" - resp, err = c.MiddlewareNext(r) + resp, err = c.Next(r) if err != nil { return nil, err } @@ -378,7 +378,7 @@ func Test_Client_Middleware(t *testing.T) { }) c.Use(func(c *ghttp.Client, r *http.Request) (resp *ghttp.ClientResponse, err error) { str1 += "e" - resp, err = c.MiddlewareNext(r) + resp, err = c.Next(r) if err != nil { return nil, err } @@ -401,7 +401,7 @@ func Test_Client_Middleware(t *testing.T) { c = g.Client().SetPrefix(fmt.Sprintf("http://127.0.0.1:%d", p)) c.Use(func(c *ghttp.Client, r *http.Request) (resp *ghttp.ClientResponse, err error) { str3 += "a" - resp, err = c.MiddlewareNext(r) + resp, err = c.Next(r) str3 += "b" return }) @@ -411,7 +411,7 @@ func Test_Client_Middleware(t *testing.T) { }) c.Use(func(c *ghttp.Client, r *http.Request) (resp *ghttp.ClientResponse, err error) { str3 += "f" - resp, err = c.MiddlewareNext(r) + resp, err = c.Next(r) str3 += "g" return }) diff --git a/net/ghttp/internal/client/client.go b/net/ghttp/internal/client/client.go new file mode 100644 index 000000000..3aba87d94 --- /dev/null +++ b/net/ghttp/internal/client/client.go @@ -0,0 +1,237 @@ +// 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 client + +import ( + "context" + "crypto/tls" + "fmt" + "github.com/gogf/gf" + "github.com/gogf/gf/text/gstr" + "golang.org/x/net/proxy" + "net" + "net/http" + "net/url" + "strings" + "time" + + "github.com/gogf/gf/text/gregex" +) + +// Client is the HTTP client for HTTP request management. +type Client struct { + http.Client // Underlying HTTP Client. + ctx context.Context // Context for each request. + dump bool // Mark this request will be dumped. + agent string // Client agent. + 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 []HandlerFunc // Interceptor handlers +} + +var ( + defaultClientAgent = fmt.Sprintf(`GoFrameHTTPClient %s`, gf.VERSION) +) + +// New creates and returns a new HTTP client object. +func New() *Client { + return &Client{ + Client: http.Client{ + Transport: &http.Transport{ + // No validation for https certification of the server in default. + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + }, + DisableKeepAlives: true, + }, + }, + header: make(map[string]string), + cookies: make(map[string]string), + agent: defaultClientAgent, + } +} + +// Clone clones current client and returns a new one. +func (c *Client) Clone() *Client { + newClient := New() + *newClient = *c + newClient.header = make(map[string]string) + newClient.cookies = make(map[string]string) + for k, v := range c.header { + newClient.header[k] = v + } + for k, v := range c.cookies { + newClient.cookies[k] = v + } + return newClient +} + +// SetBrowserMode enables browser mode of the client. +// When browser mode is enabled, it automatically saves and sends cookie content +// from and to server. +func (c *Client) SetBrowserMode(enabled bool) *Client { + c.browserMode = enabled + return c +} + +// SetHeader sets a custom HTTP header pair for the client. +func (c *Client) SetHeader(key, value string) *Client { + c.header[key] = value + return c +} + +// SetHeaderMap sets custom HTTP headers with map. +func (c *Client) SetHeaderMap(m map[string]string) *Client { + for k, v := range m { + c.header[k] = v + } + return c +} + +// SetAgent sets the User-Agent header for client. +func (c *Client) SetAgent(agent string) *Client { + c.header["User-Agent"] = agent + return c +} + +// SetContentType sets HTTP content type for the client. +func (c *Client) SetContentType(contentType string) *Client { + c.header["Content-Type"] = contentType + return c +} + +// SetHeaderRaw sets custom HTTP header using raw string. +func (c *Client) SetHeaderRaw(headers string) *Client { + for _, line := range gstr.SplitAndTrim(headers, "\n") { + array, _ := gregex.MatchString(`^([\w\-]+):\s*(.+)`, line) + if len(array) >= 3 { + c.header[array[1]] = array[2] + } + } + return c +} + +// SetCookie sets a cookie pair for the client. +func (c *Client) SetCookie(key, value string) *Client { + c.cookies[key] = value + return c +} + +// SetDump enables/disables dump feature for this request. +func (c *Client) SetDump(dump bool) *Client { + c.dump = dump + return c +} + +// SetCookieMap sets cookie items with map. +func (c *Client) SetCookieMap(m map[string]string) *Client { + for k, v := range m { + c.cookies[k] = v + } + return c +} + +// SetPrefix sets the request server URL prefix. +func (c *Client) SetPrefix(prefix string) *Client { + c.prefix = prefix + return c +} + +// SetTimeOut sets the request timeout for the client. +func (c *Client) SetTimeout(t time.Duration) *Client { + c.Client.Timeout = t + return c +} + +// SetBasicAuth sets HTTP basic authentication information for the client. +func (c *Client) SetBasicAuth(user, pass string) *Client { + c.authUser = user + c.authPass = pass + return c +} + +// SetCtx sets context for each request of this client. +func (c *Client) SetCtx(ctx context.Context) *Client { + c.ctx = ctx + return c +} + +// SetRetry sets retry count and interval. +func (c *Client) SetRetry(retryCount int, retryInterval time.Duration) *Client { + c.retryCount = retryCount + c.retryInterval = retryInterval + return c +} + +// SetRedirectLimit limit the number of jumps +func (c *Client) SetRedirectLimit(redirectLimit int) *Client { + c.CheckRedirect = func(req *http.Request, via []*http.Request) error { + if len(via) >= redirectLimit { + return http.ErrUseLastResponse + } + return nil + } + return c +} + +// SetProxy set proxy for the client. +// This func will do nothing when the parameter `proxyURL` is empty or in wrong pattern. +// The correct pattern is like `http://USER:PASSWORD@IP:PORT` or `socks5://USER:PASSWORD@IP:PORT`. +// Only `http` and `socks5` proxies are supported currently. +func (c *Client) SetProxy(proxyURL string) { + if strings.TrimSpace(proxyURL) == "" { + return + } + _proxy, err := url.Parse(proxyURL) + if err != nil { + return + } + if _proxy.Scheme == "http" { + if _, ok := c.Transport.(*http.Transport); ok { + c.Transport.(*http.Transport).Proxy = http.ProxyURL(_proxy) + } + } else { + var auth = &proxy.Auth{} + user := _proxy.User.Username() + + if user != "" { + auth.User = user + password, hasPassword := _proxy.User.Password() + if hasPassword && password != "" { + auth.Password = password + } + } else { + auth = nil + } + // refer to the source code, error is always nil + dialer, err := proxy.SOCKS5( + "tcp", + _proxy.Host, + auth, + &net.Dialer{ + Timeout: c.Client.Timeout, + KeepAlive: c.Client.Timeout, + }, + ) + if err != nil { + return + } + if _, ok := c.Transport.(*http.Transport); ok { + c.Transport.(*http.Transport).DialContext = func(ctx context.Context, network, addr string) (conn net.Conn, e error) { + return dialer.Dial(network, addr) + } + } + //c.SetTimeout(10*time.Second) + } +} diff --git a/net/ghttp/ghttp_client_bytes.go b/net/ghttp/internal/client/client_bytes.go similarity index 99% rename from net/ghttp/ghttp_client_bytes.go rename to net/ghttp/internal/client/client_bytes.go index a7b6b412c..41794370c 100644 --- a/net/ghttp/ghttp_client_bytes.go +++ b/net/ghttp/internal/client/client_bytes.go @@ -4,7 +4,7 @@ // If a copy of the MIT was not distributed with this file, // You can obtain one at https://github.com/gogf/gf. -package ghttp +package client // GetBytes sends a GET request, retrieves and returns the result content as bytes. func (c *Client) GetBytes(url string, data ...interface{}) []byte { diff --git a/net/ghttp/ghttp_client_chain.go b/net/ghttp/internal/client/client_chain.go similarity index 91% rename from net/ghttp/ghttp_client_chain.go rename to net/ghttp/internal/client/client_chain.go index e27159015..467d614f4 100644 --- a/net/ghttp/ghttp_client_chain.go +++ b/net/ghttp/internal/client/client_chain.go @@ -4,7 +4,7 @@ // If a copy of the MIT was not distributed with this file, // You can obtain one at https://github.com/gogf/gf. -package ghttp +package client import ( "context" @@ -133,7 +133,22 @@ func (c *Client) Retry(retryCount int, retryInterval time.Duration) *Client { newClient = c.Clone() } newClient.SetRetry(retryCount, retryInterval) - return c + return newClient +} + +// Dump is a chaining function, +// which enables/disables dump feature for this request. +func (c *Client) Dump(dump ...bool) *Client { + newClient := c + if c.parent == nil { + newClient = c.Clone() + } + if len(dump) > 0 { + newClient.SetDump(dump[0]) + } else { + newClient.SetDump(true) + } + return newClient } // Proxy is a chaining function, @@ -147,7 +162,7 @@ func (c *Client) Proxy(proxyURL string) *Client { newClient = c.Clone() } newClient.SetProxy(proxyURL) - return c + return newClient } // RedirectLimit is a chaining function, @@ -158,5 +173,5 @@ func (c *Client) RedirectLimit(redirectLimit int) *Client { newClient = c.Clone() } newClient.SetRedirectLimit(redirectLimit) - return c + return newClient } diff --git a/net/ghttp/ghttp_client_content.go b/net/ghttp/internal/client/client_content.go similarity index 99% rename from net/ghttp/ghttp_client_content.go rename to net/ghttp/internal/client/client_content.go index c3cef5a3d..a64ea73ff 100644 --- a/net/ghttp/ghttp_client_content.go +++ b/net/ghttp/internal/client/client_content.go @@ -4,7 +4,7 @@ // If a copy of the MIT was not distributed with this file, // You can obtain one at https://github.com/gogf/gf. -package ghttp +package client // GetContent is a convenience method for sending GET request, which retrieves and returns // the result content and automatically closes response object. diff --git a/net/ghttp/ghttp_client_dump.go b/net/ghttp/internal/client/client_dump.go similarity index 88% rename from net/ghttp/ghttp_client_dump.go rename to net/ghttp/internal/client/client_dump.go index bd8c4f875..ac082d57c 100644 --- a/net/ghttp/ghttp_client_dump.go +++ b/net/ghttp/internal/client/client_dump.go @@ -4,7 +4,7 @@ // If a copy of the MIT was not distributed with this file, // You can obtain one at https://github.com/gogf/gf. -package ghttp +package client import ( "fmt" @@ -35,8 +35,8 @@ func getResponseBody(res *http.Response) string { } // RawRequest returns the raw content of the request. -func (r *ClientResponse) RawRequest() string { - // ClientResponse can be nil. +func (r *Response) RawRequest() string { + // Response can be nil. if r == nil { return "" } @@ -57,8 +57,8 @@ func (r *ClientResponse) RawRequest() string { } // RawResponse returns the raw content of the response. -func (r *ClientResponse) RawResponse() string { - // ClientResponse can be nil. +func (r *Response) RawResponse() string { + // Response can be nil. if r == nil || r.Response == nil { return "" } @@ -76,11 +76,11 @@ func (r *ClientResponse) RawResponse() string { } // Raw returns the raw text of the request and the response. -func (r *ClientResponse) Raw() string { +func (r *Response) Raw() string { return fmt.Sprintf("%s\n%s", r.RawRequest(), r.RawResponse()) } // RawDump outputs the raw text of the request and the response to stdout. -func (r *ClientResponse) RawDump() { +func (r *Response) RawDump() { fmt.Println(r.Raw()) } diff --git a/net/ghttp/internal/client/client_middleware.go b/net/ghttp/internal/client/client_middleware.go new file mode 100644 index 000000000..d483e9df3 --- /dev/null +++ b/net/ghttp/internal/client/client_middleware.go @@ -0,0 +1,48 @@ +package client + +import ( + "net/http" +) + +// HandlerFunc middleware handler func +type HandlerFunc = func(c *Client, r *http.Request) (*Response, error) + +// clientMiddleware is the plugin for http client request workflow management. +type clientMiddleware struct { + client *Client // http client. + handlers []HandlerFunc // mdl handlers. + handlerIndex int // current handler index. + resp *Response // save resp. + err error // save err. +} + +const clientMiddlewareKey = "__clientMiddlewareKey" + +// Use adds one or more middleware handlers to client. +func (c *Client) Use(handlers ...HandlerFunc) *Client { + c.middlewareHandler = append(c.middlewareHandler, handlers...) + return c +} + +// Next calls next middleware. +// This is should only be call in HandlerFunc. +func (c *Client) Next(req *http.Request) (*Response, error) { + if v := req.Context().Value(clientMiddlewareKey); v != nil { + if m, ok := v.(*clientMiddleware); ok { + return m.Next(req) + } + } + return c.callRequest(req) +} + +// Next calls next middleware handler. +func (m *clientMiddleware) Next(req *http.Request) (resp *Response, err error) { + if m.err != nil { + return m.resp, m.err + } + if m.handlerIndex < len(m.handlers) { + m.handlerIndex++ + m.resp, m.err = m.handlers[m.handlerIndex](m.client, req) + } + return m.resp, m.err +} diff --git a/net/ghttp/ghttp_client_request.go b/net/ghttp/internal/client/client_request.go similarity index 83% rename from net/ghttp/ghttp_client_request.go rename to net/ghttp/internal/client/client_request.go index 3d3775922..74f46675f 100644 --- a/net/ghttp/ghttp_client_request.go +++ b/net/ghttp/internal/client/client_request.go @@ -4,20 +4,17 @@ // If a copy of the MIT was not distributed with this file, // You can obtain one at https://github.com/gogf/gf. -package ghttp +package client import ( "bytes" "context" "errors" "fmt" - "github.com/gogf/gf" "github.com/gogf/gf/internal/intlog" "github.com/gogf/gf/internal/json" "github.com/gogf/gf/internal/utils" - "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/label" - "go.opentelemetry.io/otel/trace" + "github.com/gogf/gf/net/ghttp/internal/httputil" "io" "io/ioutil" "mime/multipart" @@ -36,55 +33,55 @@ import ( // Get send GET request and returns the response object. // Note that the response object MUST be closed if it'll be never used. -func (c *Client) Get(url string, data ...interface{}) (*ClientResponse, error) { +func (c *Client) Get(url string, data ...interface{}) (*Response, error) { return c.DoRequest("GET", url, data...) } // Put send PUT request and returns the response object. // Note that the response object MUST be closed if it'll be never used. -func (c *Client) Put(url string, data ...interface{}) (*ClientResponse, error) { +func (c *Client) Put(url string, data ...interface{}) (*Response, error) { return c.DoRequest("PUT", url, data...) } // Post sends request using HTTP method POST and returns the response object. // Note that the response object MUST be closed if it'll be never used. -func (c *Client) Post(url string, data ...interface{}) (*ClientResponse, error) { +func (c *Client) Post(url string, data ...interface{}) (*Response, error) { return c.DoRequest("POST", url, data...) } // Delete send DELETE request and returns the response object. // Note that the response object MUST be closed if it'll be never used. -func (c *Client) Delete(url string, data ...interface{}) (*ClientResponse, error) { +func (c *Client) Delete(url string, data ...interface{}) (*Response, error) { return c.DoRequest("DELETE", url, data...) } // Head send HEAD request and returns the response object. // Note that the response object MUST be closed if it'll be never used. -func (c *Client) Head(url string, data ...interface{}) (*ClientResponse, error) { +func (c *Client) Head(url string, data ...interface{}) (*Response, error) { return c.DoRequest("HEAD", url, data...) } // Patch send PATCH request and returns the response object. // Note that the response object MUST be closed if it'll be never used. -func (c *Client) Patch(url string, data ...interface{}) (*ClientResponse, error) { +func (c *Client) Patch(url string, data ...interface{}) (*Response, error) { return c.DoRequest("PATCH", url, data...) } // Connect send CONNECT request and returns the response object. // Note that the response object MUST be closed if it'll be never used. -func (c *Client) Connect(url string, data ...interface{}) (*ClientResponse, error) { +func (c *Client) Connect(url string, data ...interface{}) (*Response, error) { return c.DoRequest("CONNECT", url, data...) } // Options send OPTIONS request and returns the response object. // Note that the response object MUST be closed if it'll be never used. -func (c *Client) Options(url string, data ...interface{}) (*ClientResponse, error) { +func (c *Client) Options(url string, data ...interface{}) (*Response, error) { return c.DoRequest("OPTIONS", url, data...) } // Trace send TRACE request and returns the response object. // Note that the response object MUST be closed if it'll be never used. -func (c *Client) Trace(url string, data ...interface{}) (*ClientResponse, error) { +func (c *Client) Trace(url string, data ...interface{}) (*Response, error) { return c.DoRequest("TRACE", url, data...) } @@ -95,30 +92,17 @@ func (c *Client) Trace(url string, data ...interface{}) (*ClientResponse, error) // 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) { +func (c *Client) DoRequest(method, url string, data ...interface{}) (resp *Response, err error) { req, err := c.prepareRequest(method, url, data...) if err != nil { return nil, err } - // Tracing. - tr := otel.GetTracerProvider().Tracer( - "github.com/gogf/gf/net/ghttp.client", - trace.WithInstrumentationVersion(fmt.Sprintf(`%s`, gf.VERSION)), - ) - ctx, span := tr.Start(req.Context(), req.URL.String()) - defer span.End() - // Header (Cookie is in it). - if len(req.Header) > 0 { - span.SetAttributes(label.Any(`http.headers`, req.Header)) - } - req = req.WithContext(ctx) - // Client middleware. if len(c.middlewareHandler) > 0 { - mdlHandlers := make([]ClientHandlerFunc, 0, len(c.middlewareHandler)+1) + mdlHandlers := make([]HandlerFunc, 0, len(c.middlewareHandler)+1) mdlHandlers = append(mdlHandlers, c.middlewareHandler...) - mdlHandlers = append(mdlHandlers, func(cli *Client, r *http.Request) (*ClientResponse, error) { + mdlHandlers = append(mdlHandlers, func(cli *Client, r *http.Request) (*Response, error) { return cli.callRequest(r) }) ctx := context.WithValue(req.Context(), clientMiddlewareKey, &clientMiddleware{ @@ -127,7 +111,7 @@ func (c *Client) DoRequest(method, url string, data ...interface{}) (resp *Clien handlerIndex: -1, }) req = req.WithContext(ctx) - resp, err = c.MiddlewareNext(req) + resp, err = c.Next(req) } else { resp, err = c.callRequest(req) } @@ -178,7 +162,7 @@ func (c *Client) prepareRequest(method, url string, data ...interface{}) (req *h } } default: - param = BuildParams(data[0]) + param = httputil.BuildParams(data[0]) } } if method == "GET" { @@ -304,15 +288,18 @@ func (c *Client) prepareRequest(method, url string, data ...interface{}) (req *h // 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{ +func (c *Client) callRequest(req *http.Request) (resp *Response, err error) { + resp = &Response{ request: req, } + // Dump feature. // The request body can be reused for dumping // raw HTTP request-response procedure. - reqBodyContent, _ := ioutil.ReadAll(req.Body) - resp.requestBody = reqBodyContent - req.Body = utils.NewReadCloser(reqBodyContent, false) + if c.dump { + reqBodyContent, _ := ioutil.ReadAll(req.Body) + resp.requestBody = reqBodyContent + req.Body = utils.NewReadCloser(reqBodyContent, false) + } for { if resp.Response, err = c.Do(req); err != nil { // The response might not be nil when err != nil. diff --git a/net/ghttp/ghttp_client_response.go b/net/ghttp/internal/client/client_response.go similarity index 74% rename from net/ghttp/ghttp_client_response.go rename to net/ghttp/internal/client/client_response.go index f7ed30014..893f4f834 100644 --- a/net/ghttp/ghttp_client_response.go +++ b/net/ghttp/internal/client/client_response.go @@ -4,7 +4,7 @@ // If a copy of the MIT was not distributed with this file, // You can obtain one at https://github.com/gogf/gf. -package ghttp +package client import ( "io/ioutil" @@ -13,16 +13,16 @@ import ( "github.com/gogf/gf/util/gconv" ) -// ClientResponse is the struct for client request response. -type ClientResponse struct { +// Response is the struct for client request response. +type Response struct { *http.Response request *http.Request requestBody []byte cookies map[string]string } -// initCookie initializes the cookie map attribute of ClientResponse. -func (r *ClientResponse) initCookie() { +// initCookie initializes the cookie map attribute of Response. +func (r *Response) initCookie() { if r.cookies == nil { r.cookies = make(map[string]string) for _, v := range r.Cookies() { @@ -32,13 +32,13 @@ func (r *ClientResponse) initCookie() { } // GetCookie retrieves and returns the cookie value of specified . -func (r *ClientResponse) GetCookie(key string) string { +func (r *Response) GetCookie(key string) string { r.initCookie() return r.cookies[key] } // GetCookieMap retrieves and returns a copy of current cookie values map. -func (r *ClientResponse) GetCookieMap() map[string]string { +func (r *Response) GetCookieMap() map[string]string { r.initCookie() m := make(map[string]string, len(r.cookies)) for k, v := range r.cookies { @@ -48,7 +48,7 @@ func (r *ClientResponse) GetCookieMap() map[string]string { } // ReadAll retrieves and returns the response content as []byte. -func (r *ClientResponse) ReadAll() []byte { +func (r *Response) ReadAll() []byte { body, err := ioutil.ReadAll(r.Response.Body) if err != nil { return nil @@ -57,12 +57,12 @@ func (r *ClientResponse) ReadAll() []byte { } // ReadAllString retrieves and returns the response content as string. -func (r *ClientResponse) ReadAllString() string { +func (r *Response) ReadAllString() string { return gconv.UnsafeBytesToStr(r.ReadAll()) } // Close closes the response when it will never be used. -func (r *ClientResponse) Close() error { +func (r *Response) Close() error { if r == nil || r.Response == nil || r.Response.Close { return nil } diff --git a/net/ghttp/internal/client/client_tracing.go b/net/ghttp/internal/client/client_tracing.go new file mode 100644 index 000000000..d5c9a91b2 --- /dev/null +++ b/net/ghttp/internal/client/client_tracing.go @@ -0,0 +1,72 @@ +// 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 client + +import ( + "fmt" + "github.com/gogf/gf" + "github.com/gogf/gf/internal/utils" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/label" + "go.opentelemetry.io/otel/trace" + "io/ioutil" + "net/http" + "net/http/httptrace" +) + +const ( + maxContentLogSize = 512 * 1024 // Max log size for request and response body. +) + +// MiddlewareTracing is a client middleware that enables tracing feature using standards of OpenTelemetry. +func MiddlewareTracing(c *Client, r *http.Request) (response *Response, err error) { + tr := otel.GetTracerProvider().Tracer( + "github.com/gogf/gf/net/ghttp.Client", + trace.WithInstrumentationVersion(fmt.Sprintf(`%s`, gf.VERSION)), + ) + ctx, span := tr.Start(r.Context(), r.URL.String()) + defer span.End() + response, err = c.Next( + r.WithContext( + httptrace.WithClientTrace( + ctx, newClientTrace(ctx, span, r), + ), + ), + ) + if err != nil { + span.SetStatus(codes.Error, fmt.Sprintf(`%+v`, err)) + } + var resBodyContent string + if response.ContentLength <= maxContentLogSize { + reqBodyContentBytes, _ := ioutil.ReadAll(response.Body) + resBodyContent = string(reqBodyContentBytes) + response.Body = utils.NewReadCloser(reqBodyContentBytes, false) + } else { + resBodyContent = fmt.Sprintf("[Response Body Too Large For Logging, Max: %d bytes]", maxContentLogSize) + } + if response != nil { + span.AddEvent("http.response", trace.WithAttributes( + label.Any(`http.response.headers`, headerToMap(response.Header)), + label.String(`http.response.body`, resBodyContent), + )) + } + return +} + +// headerToMap coverts request headers to map. +func headerToMap(header http.Header) map[string]interface{} { + m := make(map[string]interface{}) + for k, v := range header { + if len(v) > 1 { + m[k] = v + } else { + m[k] = v[0] + } + } + return m +} diff --git a/net/ghttp/internal/client/client_tracing_tracer.go b/net/ghttp/internal/client/client_tracing_tracer.go new file mode 100644 index 000000000..154eb2473 --- /dev/null +++ b/net/ghttp/internal/client/client_tracing_tracer.go @@ -0,0 +1,171 @@ +// 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 client + +import ( + "context" + "crypto/tls" + "fmt" + "github.com/gogf/gf/internal/utils" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/label" + "go.opentelemetry.io/otel/trace" + "io/ioutil" + "net/http" + "net/http/httptrace" + "net/textproto" + "strings" + "sync" +) + +type clientTracer struct { + context.Context + span trace.Span + request *http.Request + requestBody []byte + headers map[string]interface{} + mtx sync.Mutex +} + +func newClientTrace(ctx context.Context, span trace.Span, request *http.Request) *httptrace.ClientTrace { + ct := &clientTracer{ + Context: ctx, + span: span, + request: request, + headers: make(map[string]interface{}), + } + return &httptrace.ClientTrace{ + GetConn: ct.getConn, + GotConn: ct.gotConn, + PutIdleConn: ct.putIdleConn, + GotFirstResponseByte: ct.gotFirstResponseByte, + Got100Continue: ct.got100Continue, + Got1xxResponse: ct.got1xxResponse, + DNSStart: ct.dnsStart, + DNSDone: ct.dnsDone, + ConnectStart: ct.connectStart, + ConnectDone: ct.connectDone, + TLSHandshakeStart: ct.tlsHandshakeStart, + TLSHandshakeDone: ct.tlsHandshakeDone, + WroteHeaderField: ct.wroteHeaderField, + WroteHeaders: ct.wroteHeaders, + Wait100Continue: ct.wait100Continue, + WroteRequest: ct.wroteRequest, + } +} + +func (ct *clientTracer) getConn(host string) { + +} + +func (ct *clientTracer) gotConn(info httptrace.GotConnInfo) { + ct.span.SetAttributes( + label.String("http.connection.remote", info.Conn.RemoteAddr().String()), + label.String("http.connection.local", info.Conn.LocalAddr().String()), + ) +} + +func (ct *clientTracer) putIdleConn(err error) { + if err != nil { + ct.span.SetStatus(codes.Error, fmt.Sprintf(`%+v`, err)) + } +} + +func (ct *clientTracer) dnsStart(info httptrace.DNSStartInfo) { + ct.span.SetAttributes( + label.String("http.dns.start", info.Host), + ) +} + +func (ct *clientTracer) dnsDone(info httptrace.DNSDoneInfo) { + var buffer strings.Builder + for _, v := range info.Addrs { + if buffer.Len() != 0 { + buffer.WriteString(",") + } + buffer.WriteString(v.String()) + } + if info.Err != nil { + ct.span.SetStatus(codes.Error, fmt.Sprintf(`%+v`, info.Err)) + } + ct.span.SetAttributes( + label.String("http.dns.done", buffer.String()), + ) +} + +func (ct *clientTracer) connectStart(network, addr string) { + ct.span.SetAttributes( + label.String("http.connect.start", network+"@"+addr), + ) +} + +func (ct *clientTracer) connectDone(network, addr string, err error) { + if err != nil { + ct.span.SetStatus(codes.Error, fmt.Sprintf(`%+v`, err)) + } + ct.span.SetAttributes( + label.String("http.connect.done", network+"@"+addr), + ) +} + +func (ct *clientTracer) tlsHandshakeStart() { + +} + +func (ct *clientTracer) tlsHandshakeDone(_ tls.ConnectionState, err error) { + if err != nil { + ct.span.SetStatus(codes.Error, fmt.Sprintf(`%+v`, err)) + } +} + +func (ct *clientTracer) wroteHeaderField(k string, v []string) { + if len(v) > 1 { + ct.headers[k] = v + } else { + ct.headers[k] = v[0] + } +} + +func (ct *clientTracer) wroteHeaders() { + if ct.request.ContentLength <= maxContentLogSize { + reqBodyContent, _ := ioutil.ReadAll(ct.request.Body) + ct.requestBody = reqBodyContent + ct.request.Body = utils.NewReadCloser(reqBodyContent, false) + } +} + +func (ct *clientTracer) wroteRequest(info httptrace.WroteRequestInfo) { + if info.Err != nil { + ct.span.SetStatus(codes.Error, fmt.Sprintf(`%+v`, info.Err)) + } + var bodyContent string + if ct.request.ContentLength <= maxContentLogSize { + bodyContent = string(ct.requestBody) + } else { + bodyContent = fmt.Sprintf("[Request Body Too Large For Logging, Max: %d bytes]", maxContentLogSize) + } + ct.span.AddEvent("http.request", trace.WithAttributes( + label.Any(`http.request.headers`, ct.headers), + label.String(`http.request.body`, bodyContent), + )) +} + +func (ct *clientTracer) got100Continue() { + +} + +func (ct *clientTracer) wait100Continue() { + +} + +func (ct *clientTracer) gotFirstResponseByte() { + ct.span.AddEvent("http.request.receive", trace.WithAttributes()) +} + +func (ct *clientTracer) got1xxResponse(code int, header textproto.MIMEHeader) error { + return nil +} diff --git a/net/ghttp/ghttp_client_var.go b/net/ghttp/internal/client/client_var.go similarity index 99% rename from net/ghttp/ghttp_client_var.go rename to net/ghttp/internal/client/client_var.go index e8b2a00b0..1fe9810e1 100644 --- a/net/ghttp/ghttp_client_var.go +++ b/net/ghttp/internal/client/client_var.go @@ -4,7 +4,7 @@ // If a copy of the MIT was not distributed with this file, // You can obtain one at https://github.com/gogf/gf. -package ghttp +package client import ( "github.com/gogf/gf/container/gvar" diff --git a/net/ghttp/internal/httputil/httputils.go b/net/ghttp/internal/httputil/httputils.go new file mode 100644 index 000000000..209acb782 --- /dev/null +++ b/net/ghttp/internal/httputil/httputils.go @@ -0,0 +1,66 @@ +// 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 httputil + +import ( + "github.com/gogf/gf/text/gstr" + "strings" + + "github.com/gogf/gf/encoding/gurl" + "github.com/gogf/gf/util/gconv" +) + +const ( + fileUploadingKey = "@file:" +) + +// BuildParams builds the request string for the http client. The can be type of: +// string/[]byte/map/struct/*struct. +// +// The optional parameter specifies whether ignore the url encoding for the data. +func BuildParams(params interface{}, noUrlEncode ...bool) (encodedParamStr string) { + // If given string/[]byte, converts and returns it directly as string. + switch v := params.(type) { + case string, []byte: + return gconv.String(params) + case []interface{}: + if len(v) > 0 { + params = v[0] + } else { + params = nil + } + } + // Else converts it to map and does the url encoding. + m, urlEncode := gconv.Map(params), true + if len(m) == 0 { + return gconv.String(params) + } + if len(noUrlEncode) == 1 { + urlEncode = !noUrlEncode[0] + } + // If there's file uploading, it ignores the url encoding. + if urlEncode { + for k, v := range m { + if gstr.Contains(k, fileUploadingKey) || gstr.Contains(gconv.String(v), fileUploadingKey) { + urlEncode = false + break + } + } + } + s := "" + for k, v := range m { + if len(encodedParamStr) > 0 { + encodedParamStr += "&" + } + s = gconv.String(v) + if urlEncode && len(s) > 6 && strings.Compare(s[0:6], fileUploadingKey) != 0 { + s = gurl.Encode(s) + } + encodedParamStr += k + "=" + s + } + return +} diff --git a/net/gtrace/gtrace_http_client.go b/net/gtrace/gtrace_http_client.go deleted file mode 100644 index 7bc100b22..000000000 --- a/net/gtrace/gtrace_http_client.go +++ /dev/null @@ -1,242 +0,0 @@ -package gtrace - -import ( - "context" - "crypto/tls" - "fmt" - "github.com/gogf/gf" - "net/http/httptrace" - "net/textproto" - "strings" - "sync" - - "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/codes" - "go.opentelemetry.io/otel/label" - "go.opentelemetry.io/otel/semconv" - "go.opentelemetry.io/otel/trace" -) - -var ( - HTTPStatus = label.Key("http.status") - HTTPHeaderMIME = label.Key("http.mime") - HTTPRemoteAddr = label.Key("http.remote") - HTTPLocalAddr = label.Key("http.local") -) - -var ( - hookMap = map[string]string{ - "http.dns": "http.getconn", - "http.connect": "http.getconn", - "http.tls": "http.getconn", - } -) - -func parentHook(hook string) string { - if strings.HasPrefix(hook, "http.connect") { - return hookMap["http.connect"] - } - return hookMap[hook] -} - -type clientTracer struct { - context.Context - - tr trace.Tracer - - activeHooks map[string]context.Context - root trace.Span - mtx sync.Mutex -} - -func NewClientTrace(ctx context.Context) *httptrace.ClientTrace { - ct := &clientTracer{ - Context: ctx, - activeHooks: make(map[string]context.Context), - } - - ct.tr = otel.GetTracerProvider().Tracer( - "github.com/gogf/gf/net/ghttp.client", - trace.WithInstrumentationVersion(fmt.Sprintf(`%s`, gf.VERSION)), - ) - - return &httptrace.ClientTrace{ - GetConn: ct.getConn, - GotConn: ct.gotConn, - PutIdleConn: ct.putIdleConn, - GotFirstResponseByte: ct.gotFirstResponseByte, - Got100Continue: ct.got100Continue, - Got1xxResponse: ct.got1xxResponse, - DNSStart: ct.dnsStart, - DNSDone: ct.dnsDone, - ConnectStart: ct.connectStart, - ConnectDone: ct.connectDone, - TLSHandshakeStart: ct.tlsHandshakeStart, - TLSHandshakeDone: ct.tlsHandshakeDone, - WroteHeaderField: ct.wroteHeaderField, - WroteHeaders: ct.wroteHeaders, - Wait100Continue: ct.wait100Continue, - WroteRequest: ct.wroteRequest, - } -} - -func (ct *clientTracer) start(hook, spanName string, attrs ...label.KeyValue) { - ct.mtx.Lock() - defer ct.mtx.Unlock() - - if hookCtx, found := ct.activeHooks[hook]; !found { - var sp trace.Span - ct.activeHooks[hook], sp = ct.tr.Start(ct.getParentContext(hook), spanName, trace.WithAttributes(attrs...), trace.WithSpanKind(trace.SpanKindClient)) - if ct.root == nil { - ct.root = sp - } - } else { - // end was called before start finished, add the start attributes and end the span here - span := trace.SpanFromContext(hookCtx) - span.SetAttributes(attrs...) - span.End() - - delete(ct.activeHooks, hook) - } -} - -func (ct *clientTracer) end(hook string, err error, attrs ...label.KeyValue) { - ct.mtx.Lock() - defer ct.mtx.Unlock() - if ctx, ok := ct.activeHooks[hook]; ok { - span := trace.SpanFromContext(ctx) - if err != nil { - span.SetStatus(codes.Error, err.Error()) - } - span.SetAttributes(attrs...) - span.End() - delete(ct.activeHooks, hook) - } else { - // start is not finished before end is called. - // Start a span here with the ending attributes that will be finished when start finishes. - // Yes, it's backwards. v0v - ctx, span := ct.tr.Start(ct.getParentContext(hook), hook, trace.WithAttributes(attrs...), trace.WithSpanKind(trace.SpanKindClient)) - if err != nil { - span.SetStatus(codes.Error, err.Error()) - } - ct.activeHooks[hook] = ctx - } -} - -func (ct *clientTracer) getParentContext(hook string) context.Context { - ctx, ok := ct.activeHooks[parentHook(hook)] - if !ok { - return ct.Context - } - return ctx -} - -func (ct *clientTracer) span(hook string) trace.Span { - ct.mtx.Lock() - defer ct.mtx.Unlock() - if ctx, ok := ct.activeHooks[hook]; ok { - return trace.SpanFromContext(ctx) - } - return nil -} - -func (ct *clientTracer) getConn(host string) { - ct.start("http.getconn", "http.getconn", semconv.HTTPHostKey.String(host)) -} - -func (ct *clientTracer) gotConn(info httptrace.GotConnInfo) { - ct.end("http.getconn", - nil, - HTTPRemoteAddr.String(info.Conn.RemoteAddr().String()), - HTTPLocalAddr.String(info.Conn.LocalAddr().String()), - ) -} - -func (ct *clientTracer) putIdleConn(err error) { - ct.end("http.receive", err) -} - -func (ct *clientTracer) gotFirstResponseByte() { - ct.start("http.receive", "http.receive") -} - -func (ct *clientTracer) dnsStart(info httptrace.DNSStartInfo) { - ct.start("http.dns", "http.dns", semconv.HTTPHostKey.String(info.Host)) -} - -func (ct *clientTracer) dnsDone(info httptrace.DNSDoneInfo) { - ct.end("http.dns", info.Err) -} - -func (ct *clientTracer) connectStart(network, addr string) { - ct.start("http.connect."+addr, "http.connect", HTTPRemoteAddr.String(addr)) -} - -func (ct *clientTracer) connectDone(network, addr string, err error) { - ct.end("http.connect."+addr, err) -} - -func (ct *clientTracer) tlsHandshakeStart() { - ct.start("http.tls", "http.tls") -} - -func (ct *clientTracer) tlsHandshakeDone(_ tls.ConnectionState, err error) { - ct.end("http.tls", err) -} - -func (ct *clientTracer) wroteHeaderField(k string, v []string) { - if ct.span("http.headers") == nil { - ct.start("http.headers", "http.headers") - } - ct.root.SetAttributes(label.String("http."+strings.ToLower(k), sliceToString(v))) -} - -func (ct *clientTracer) wroteHeaders() { - if ct.span("http.headers") != nil { - ct.end("http.headers", nil) - } - ct.start("http.send", "http.send") -} - -func (ct *clientTracer) wroteRequest(info httptrace.WroteRequestInfo) { - if info.Err != nil { - ct.root.SetStatus(codes.Error, info.Err.Error()) - } - ct.end("http.send", info.Err) -} - -func (ct *clientTracer) got100Continue() { - ct.span("http.receive").AddEvent("GOT 100 - Continue") -} - -func (ct *clientTracer) wait100Continue() { - ct.span("http.receive").AddEvent("GOT 100 - Wait") -} - -func (ct *clientTracer) got1xxResponse(code int, header textproto.MIMEHeader) error { - ct.span("http.receive").AddEvent("GOT 1xx", trace.WithAttributes( - HTTPStatus.Int(code), - HTTPHeaderMIME.String(sm2s(header)), - )) - return nil -} - -func sliceToString(value []string) string { - if len(value) == 0 { - return "undefined" - } - return strings.Join(value, ",") -} - -func sm2s(value map[string][]string) string { - var buf strings.Builder - for k, v := range value { - if buf.Len() != 0 { - buf.WriteString(",") - } - buf.WriteString(k) - buf.WriteString("=") - buf.WriteString(sliceToString(v)) - } - return buf.String() -}