-
Notifications
You must be signed in to change notification settings - Fork 17
feat(http): Add connection timeout #339
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| package http | ||
|
|
||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "net" | ||
| stdhttp "net/http" | ||
| "time" | ||
| ) | ||
|
|
||
| // ConnectTimeout limits the time spent establishing a network connection. | ||
| func (c *Client) ConnectTimeout(timeout time.Duration) (*Client, error) { | ||
| if timeout <= 0 { | ||
| return nil, fmt.Errorf("connect timeout must be greater than zero, got %s", timeout) | ||
| } | ||
|
|
||
| base := c.httpClient.Transport | ||
| if base == nil { | ||
| base = stdhttp.DefaultTransport | ||
| } | ||
| transport, ok := base.(*stdhttp.Transport) | ||
| if !ok { | ||
| return nil, fmt.Errorf("configure connect timeout: transport %T is not an *http.Transport", base) | ||
| } | ||
|
|
||
| configured := transport.Clone() | ||
| dialContext := configured.DialContext | ||
| if dialContext == nil { | ||
| dialContext = (&net.Dialer{}).DialContext | ||
| } | ||
| configured.DialContext = func(ctx context.Context, network, address string) (net.Conn, error) { | ||
| connectCtx, cancel := context.WithTimeout(ctx, timeout) | ||
| defer cancel() | ||
| return dialContext(connectCtx, network, address) | ||
| } | ||
| c.httpClient.Transport = configured | ||
| return c, nil | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| package http | ||
|
|
||
| import ( | ||
| "context" | ||
| "errors" | ||
| "net" | ||
| stdhttp "net/http" | ||
| "sync/atomic" | ||
| "testing" | ||
| "time" | ||
|
|
||
| . "github.com/onsi/ginkgo/v2" | ||
| . "github.com/onsi/gomega" | ||
| ) | ||
|
|
||
| func TestConnectTimeout(t *testing.T) { | ||
| RegisterFailHandler(Fail) | ||
| RunSpecs(t, "HTTP Connect Timeout Suite") | ||
| } | ||
|
|
||
| var _ = Describe("Client connect timeout", func() { | ||
| It("rejects non-positive durations", func() { | ||
| for _, timeout := range []time.Duration{0, -time.Nanosecond} { | ||
| _, err := NewClient().ConnectTimeout(timeout) | ||
| Expect(err).To(MatchError(ContainSubstring("connect timeout must be greater than zero")), timeout) | ||
| } | ||
| }) | ||
|
|
||
| It("bounds the configured dialer and preserves client settings", func() { | ||
| var dialAttempts atomic.Int32 | ||
| originalTransport := &stdhttp.Transport{ | ||
| DisableKeepAlives: true, | ||
| MaxIdleConns: 17, | ||
| DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) { | ||
| dialAttempts.Add(1) | ||
| <-ctx.Done() | ||
| return nil, ctx.Err() | ||
| }, | ||
| } | ||
| client := NewClient().Timeout(time.Second) | ||
| client.httpClient.Transport = originalTransport | ||
|
|
||
| configured, err := client.ConnectTimeout(25 * time.Millisecond) | ||
| Expect(err).ToNot(HaveOccurred()) | ||
| Expect(configured).To(BeIdenticalTo(client)) | ||
| transport := client.httpClient.Transport.(*stdhttp.Transport) | ||
| Expect(transport).ToNot(BeIdenticalTo(originalTransport)) | ||
| Expect(transport.DisableKeepAlives).To(BeTrue()) | ||
| Expect(transport.MaxIdleConns).To(Equal(17)) | ||
| Expect(client.httpClient.Timeout).To(Equal(time.Second)) | ||
|
|
||
| started := time.Now() | ||
| _, err = client.httpClient.Get("http://example.test") | ||
| Expect(errors.Is(err, context.DeadlineExceeded)).To(BeTrue()) | ||
| Expect(time.Since(started)).To(BeNumerically(">=", 25*time.Millisecond)) | ||
| Expect(time.Since(started)).To(BeNumerically("<", time.Second)) | ||
|
Comment on lines
+55
to
+56
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Tighten the timeout upper bounds. Both assertions only require completion before one second. An implementation that delays dialing far beyond the configured 25 milliseconds can pass these tests. Use a practical upper bound relative to the 25-millisecond timeout, such as 250 milliseconds. Proposed test change- Expect(time.Since(started)).To(BeNumerically("<", time.Second))
+ Expect(time.Since(started)).To(BeNumerically("<", 250*time.Millisecond))Also applies to: 75-76 🤖 Prompt for AI Agents |
||
| Expect(dialAttempts.Load()).To(Equal(int32(1))) | ||
| }) | ||
|
|
||
| It("honors an earlier request deadline", func() { | ||
| client := NewClient() | ||
| client.httpClient.Transport = &stdhttp.Transport{DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) { | ||
| <-ctx.Done() | ||
| return nil, ctx.Err() | ||
| }} | ||
| _, err := client.ConnectTimeout(time.Second) | ||
| Expect(err).ToNot(HaveOccurred()) | ||
|
|
||
| ctx, cancel := context.WithTimeout(context.Background(), 25*time.Millisecond) | ||
| defer cancel() | ||
| request, err := stdhttp.NewRequestWithContext(ctx, stdhttp.MethodGet, "http://example.test", nil) | ||
| Expect(err).ToNot(HaveOccurred()) | ||
| started := time.Now() | ||
| _, err = client.httpClient.Do(request) | ||
| Expect(errors.Is(err, context.DeadlineExceeded)).To(BeTrue()) | ||
| Expect(time.Since(started)).To(BeNumerically("<", time.Second)) | ||
| }) | ||
| }) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: flanksource/commons
Length of output: 157
🏁 Script executed:
Repository: flanksource/commons
Length of output: 286
🏁 Script executed:
Repository: flanksource/commons
Length of output: 2605
🌐 Web query:
Go net/http.Transport DialTLSContext used for HTTPS connections when non-nil documentation💡 Result:
When net/http.Transport.DialTLSContext is set (non-nil), it is used to create TLS connections for non-proxied HTTPS requests [1][2]. Its behavior and interaction with other Transport settings are as follows: 1. Priority: If DialTLSContext is set, it takes precedence over the deprecated DialTLS hook [1][2]. 2. Bypassing Defaults: When DialTLSContext is provided, the Transport will not use the standard Dial or DialContext hooks for HTTPS requests [1][2]. Additionally, the TLSClientConfig and TLSHandshakeTimeout fields are ignored by the Transport, as the returned net.Conn is assumed to have already completed the TLS handshake [1][2]. 3. Expectation of State: The net.Conn returned by DialTLSContext is expected to be fully established and past the TLS handshake stage [1][2]. For proper ALPN (Application-Layer Protocol Negotiation) support, the returned connection should be a *tls.Conn or otherwise implement the ConnectionState method consistent with *tls.Conn [2]. 4. Fallback: If DialTLSContext is nil, the Transport checks for the deprecated DialTLS [1][2]. If both are nil, the Transport defaults to using DialContext (or Dial) combined with the TLSClientConfig to manage the connection and perform the handshake [1][2]. By using DialTLSContext, developers gain the ability to use the provided context.Context to cancel the dial operation when it is no longer needed, as well as the flexibility to perform custom TLS configuration or validation on a per-request basis [2][3].
Citations:
🏁 Script executed:
Repository: flanksource/commons
Length of output: 3395
Apply the timeout to
DialTLSContext.ConnectTimeoutcopies an*http.Transportbut only wrapsDialContext. WhenTransport.DialTLSContextis set, HTTPS connections use it instead ofDialContext, so custom TLS dialing can bypassConnectTimeout. Wrap the clonedDialTLSContextwith the same timeout context and add an HTTPS test using a blockingDialTLSContext.🤖 Prompt for AI Agents