Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 32 additions & 24 deletions net/gclient/gclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
"crypto/rand"
"crypto/tls"
"fmt"
"net"
"net/http"
"os"
"time"
Expand Down Expand Up @@ -58,31 +57,40 @@
defaultClientAgent = fmt.Sprintf(`GClient %s at %s`, gf.VERSION, hostname)
)

// New creates and returns a new HTTP client object.
// New creates and returns a new HTTP client object with a default timeout of 30 seconds.
func New() *Client {
return NewWithTimeout(30 * time.Second)
}
Comment thread
hailaz marked this conversation as resolved.

// NewWithTimeout creates and returns a new HTTP client object with specified timeout.
//
// The transport is cloned from http.DefaultTransport to inherit standard library defaults
// (such as Proxy, HTTP/2 knobs, and future Go defaults), then customized with the project's
// own TLS, keep-alive, and connection pool settings.
func NewWithTimeout(timeout time.Duration) *Client {
// Clone from http.DefaultTransport to inherit standard library defaults,
// then override with project-specific settings.
transport := http.DefaultTransport.(*http.Transport).Clone()
// No validation for https certification of the server in default.
Comment thread
hailaz marked this conversation as resolved.
transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}

Check failure

Code scanning / CodeQL

Disabled TLS certificate check High

InsecureSkipVerify should not be used in production code.
Comment thread
hailaz marked this conversation as resolved.
Dismissed
transport.DisableKeepAlives = true
transport.MaxIdleConnsPerHost = 50
transport.MaxConnsPerHost = 100
defaultClient := http.Client{
Transport: transport,
Timeout: timeout,
}
return NewWithHttpClient(&defaultClient)
}

// NewWithHttpClient creates and returns a new Client with given http.Client.
// It panics if client is nil.
func NewWithHttpClient(client *http.Client) *Client {
if client == nil {
panic(`gclient: client must not be nil`)
}
c := &Client{
Comment thread
hailaz marked this conversation as resolved.
Client: http.Client{
Transport: &http.Transport{
// No validation for https certification of the server in default.
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
DisableKeepAlives: true,
MaxIdleConns: 100,
MaxIdleConnsPerHost: 50,
MaxConnsPerHost: 100,
IdleConnTimeout: 90 * time.Second,
ResponseHeaderTimeout: 30 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ForceAttemptHTTP2: true,
DisableCompression: false,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
},
},
Client: *client,
header: make(map[string]string),
cookies: make(map[string]string),
builder: gsel.GetBuilder(),
Expand Down
63 changes: 63 additions & 0 deletions net/gclient/gclient_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,11 +95,68 @@ func (c *Client) SetPrefix(prefix string) *Client {
}

// SetTimeout sets the request timeout for the client.
// It only updates the client timeout, not transport timeouts.
// Use SetTransportTimeout to configure transport-level timeouts.
//
// Note: If a SOCKS5 proxy has been configured via SetProxy, the proxy dialer
// snapshots the client timeout at setup time. Call SetTimeout before SetProxy
// to ensure the proxy dialer uses the updated timeout value.
func (c *Client) SetTimeout(t time.Duration) *Client {
Comment thread
hailaz marked this conversation as resolved.
c.Client.Timeout = t
return c
}

// SetTransportTimeout sets the transport-level timeouts for the client.
// It configures ResponseHeaderTimeout, TLSHandshakeTimeout, and ExpectContinueTimeout.
// Use this method to set fine-grained timeouts for different phases of the request.
//
// Note: This is a no-op if c.Transport is not a *http.Transport (for example,
// after calling SetTransport with a custom RoundTripper).
func (c *Client) SetTransportTimeout(responseHeaderTimeout, tlsHandshakeTimeout, expectContinueTimeout time.Duration) *Client {
Comment thread
hailaz marked this conversation as resolved.
if transport, ok := c.Transport.(*http.Transport); ok {
transport.ResponseHeaderTimeout = responseHeaderTimeout
transport.TLSHandshakeTimeout = tlsHandshakeTimeout
transport.ExpectContinueTimeout = expectContinueTimeout
}
return c
}

// SetResponseHeaderTimeout sets the timeout for receiving response headers.
// This is the maximum time to wait for the server to send response headers.
//
// Note: This is a no-op if c.Transport is not a *http.Transport (for example,
// after calling SetTransport with a custom RoundTripper).
func (c *Client) SetResponseHeaderTimeout(t time.Duration) *Client {
if transport, ok := c.Transport.(*http.Transport); ok {
transport.ResponseHeaderTimeout = t
}
return c
}

// SetTLSHandshakeTimeout sets the timeout for TLS handshake.
// This is the maximum time to wait for TLS handshake to complete.
//
// Note: This is a no-op if c.Transport is not a *http.Transport (for example,
// after calling SetTransport with a custom RoundTripper).
func (c *Client) SetTLSHandshakeTimeout(t time.Duration) *Client {
if transport, ok := c.Transport.(*http.Transport); ok {
transport.TLSHandshakeTimeout = t
}
return c
}

// SetExpectContinueTimeout sets the timeout for Expect: 100-continue.
// This is the maximum time to wait for the server to respond to Expect: 100-continue header.
//
// Note: This is a no-op if c.Transport is not a *http.Transport (for example,
// after calling SetTransport with a custom RoundTripper).
func (c *Client) SetExpectContinueTimeout(t time.Duration) *Client {
if transport, ok := c.Transport.(*http.Transport); ok {
transport.ExpectContinueTimeout = t
}
return c
}

// SetBasicAuth sets HTTP basic authentication information for the client.
func (c *Client) SetBasicAuth(user, pass string) *Client {
c.authUser = user
Expand Down Expand Up @@ -216,3 +273,9 @@ func (c *Client) SetBuilder(builder gsel.Builder) {
func (c *Client) SetDiscovery(discovery gsvc.Discovery) {
c.discovery = discovery
}

// SetTransport sets the transport for the client.
func (c *Client) SetTransport(transport http.RoundTripper) *Client {
c.Transport = transport
return c
}
194 changes: 194 additions & 0 deletions net/gclient/gclient_z_unit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -719,3 +719,197 @@ func TestClient_NoUrlEncode(t *testing.T) {
t.Assert(c.NoUrlEncode().GetContent(ctx, `/`, params), `path=/data/binlog`)
})
}

func TestClient_NewWithHttpClient_NilPanics(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
defer func() {
if r := recover(); r != nil {
t.Assert(fmt.Sprintf("%v", r), `gclient: client must not be nil`)
}
}()
Comment thread
hailaz marked this conversation as resolved.
Outdated
gclient.NewWithHttpClient(nil)
})
}

func TestClient_SetTransportTimeout_ShouldUpdateTransportTimeouts(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
client := gclient.New()
responseHeaderTimeout := 5 * time.Second
tlsHandshakeTimeout := 3 * time.Second
expectContinueTimeout := 2 * time.Second

// Set transport timeouts
client.SetTransportTimeout(responseHeaderTimeout, tlsHandshakeTimeout, expectContinueTimeout)

// Verify transport timeouts
transport, ok := client.Transport.(*http.Transport)
t.Assert(ok, true)
t.Assert(transport.ResponseHeaderTimeout, responseHeaderTimeout)
t.Assert(transport.TLSHandshakeTimeout, tlsHandshakeTimeout)
t.Assert(transport.ExpectContinueTimeout, expectContinueTimeout)

// Verify that client timeout remains at default (30s from New())
t.Assert(client.Client.Timeout, 30*time.Second)

// Verify that IdleConnTimeout is not changed (should remain default 90s)
t.Assert(transport.IdleConnTimeout, 90*time.Second)
Comment thread
hailaz marked this conversation as resolved.
Outdated
Comment thread
hailaz marked this conversation as resolved.
Outdated
})
}

func TestClient_SetTimeout_ShouldOnlyUpdateClientTimeout(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
client := gclient.New()
timeout := 5 * time.Second

// Set timeout
client.SetTimeout(timeout)

// Verify client timeout
t.Assert(client.Client.Timeout, timeout)

// Verify transport timeouts are not changed (should remain defaults)
transport, ok := client.Transport.(*http.Transport)
t.Assert(ok, true)
// ResponseHeaderTimeout should be 0 (not set by default)
t.Assert(transport.ResponseHeaderTimeout, 0*time.Second)
// TLSHandshakeTimeout should be 10s (default)
t.Assert(transport.TLSHandshakeTimeout, 10*time.Second)
// ExpectContinueTimeout should be 1s (default)
t.Assert(transport.ExpectContinueTimeout, 1*time.Second)
Comment thread
hailaz marked this conversation as resolved.
Outdated
})
}

func TestClient_SetTimeout_WithDifferentValues(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
client := gclient.New()

// Test with various timeout values
testCases := []time.Duration{
1 * time.Second,
10 * time.Second,
30 * time.Second,
1 * time.Minute,
}

for _, timeout := range testCases {
client.SetTimeout(timeout)

// Verify client timeout is set correctly
t.Assert(client.Client.Timeout, timeout)

// Verify transport timeouts remain unchanged (defaults)
transport, ok := client.Transport.(*http.Transport)
t.Assert(ok, true)
// ResponseHeaderTimeout should remain 0 (not set by default)
t.Assert(transport.ResponseHeaderTimeout, 0*time.Second)
}
})
}

func TestClient_Clone_ShouldPreserveTimeoutSettings(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
original := gclient.New()
timeout := 5 * time.Second
original.SetTimeout(timeout)

// Clone the client
cloned := original.Clone()

// Verify cloned client has same timeout
t.Assert(cloned.Client.Timeout, timeout)

// Verify transport timeouts are preserved (should be defaults)
transport, ok := cloned.Transport.(*http.Transport)
t.Assert(ok, true)
// ResponseHeaderTimeout should be 0 (not set by default)
t.Assert(transport.ResponseHeaderTimeout, 0*time.Second)
// TLSHandshakeTimeout should be 10s (default)
t.Assert(transport.TLSHandshakeTimeout, 10*time.Second)
Comment thread
hailaz marked this conversation as resolved.
Outdated

// Modify cloned client's timeout
newTimeout := 10 * time.Second
cloned.SetTimeout(newTimeout)

// Verify original client's timeout is unchanged
t.Assert(original.Client.Timeout, timeout)

// Verify cloned client has new timeout
t.Assert(cloned.Client.Timeout, newTimeout)
})

gtest.C(t, func(t *gtest.T) {
client := gclient.New()
client.SetResponseHeaderTimeout(5 * time.Second)
transport, ok := client.Transport.(*http.Transport)
t.Assert(ok, true)
t.Assert(transport.ResponseHeaderTimeout, 5*time.Second)

client.SetTLSHandshakeTimeout(3 * time.Second)
t.Assert(transport.TLSHandshakeTimeout, 3*time.Second)

client.SetExpectContinueTimeout(2 * time.Second)
t.Assert(transport.ExpectContinueTimeout, 2*time.Second)

// Verify client timeout is still at default (30s from New())
t.Assert(client.Client.Timeout, 30*time.Second)
})
Comment thread
hailaz marked this conversation as resolved.
}

func TestClient_SetTransportTimeout_WithIndividualMethods(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
client := gclient.New()

// Test SetResponseHeaderTimeout
client.SetResponseHeaderTimeout(5 * time.Second)
transport, ok := client.Transport.(*http.Transport)
t.Assert(ok, true)
t.Assert(transport.ResponseHeaderTimeout, 5*time.Second)

// Test SetTLSHandshakeTimeout
client.SetTLSHandshakeTimeout(3 * time.Second)
t.Assert(transport.TLSHandshakeTimeout, 3*time.Second)

// Test SetExpectContinueTimeout
client.SetExpectContinueTimeout(2 * time.Second)
t.Assert(transport.ExpectContinueTimeout, 2*time.Second)

// Verify client timeout is still at default (30s from New())
t.Assert(client.Client.Timeout, 30*time.Second)
})
Comment thread
hailaz marked this conversation as resolved.
}

func TestClient_SetTransportTimeout_ShouldNotAffectClientTimeout(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
client := gclient.New()
clientTimeout := 10 * time.Second
client.SetTimeout(clientTimeout)

// Set transport timeouts
client.SetTransportTimeout(5*time.Second, 3*time.Second, 2*time.Second)

// Verify client timeout is unchanged
t.Assert(client.Client.Timeout, clientTimeout)

// Verify transport timeouts are set
transport, ok := client.Transport.(*http.Transport)
t.Assert(ok, true)
t.Assert(transport.ResponseHeaderTimeout, 5*time.Second)
t.Assert(transport.TLSHandshakeTimeout, 3*time.Second)
t.Assert(transport.ExpectContinueTimeout, 2*time.Second)
})
}

func TestClient_SetTransportTimeout_ZeroValues(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
client := gclient.New()

// Set zero values (effectively disable timeouts)
client.SetTransportTimeout(0, 0, 0)

transport, ok := client.Transport.(*http.Transport)
t.Assert(ok, true)
t.Assert(transport.ResponseHeaderTimeout, 0*time.Second)
t.Assert(transport.TLSHandshakeTimeout, 0*time.Second)
t.Assert(transport.ExpectContinueTimeout, 0*time.Second)
})
}
Loading