Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
38 changes: 38 additions & 0 deletions http/connect.go
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)
}
Comment on lines +27 to +35

Copy link
Copy Markdown

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:

#!/bin/bash
set -euo pipefail

# Find existing custom TLS dialers that require timeout coverage.
rg -n -C 4 --type go '\bDialTLSContext\s*:|\bDialTLS\s*:' .

Repository: flanksource/commons

Length of output: 157


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate connect.go =="
fd -a 'connect.go$' . | sed 's#^\./##'

echo "== http/connect.go snippet =="
if [ -f http/connect.go ]; then
  nl -ba http/connect.go | sed -n '1,120p'
fi

echo "== search DialTLSContext usages =="
rg -n --type go 'DialTLSContext|DialTLS|DialContext|ConnectTimeout' . || true

echo "== git diff stat =="
git diff --stat || true

Repository: flanksource/commons

Length of output: 286


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate connect.go =="
fd -a 'connect.go$' . | sed 's#^\./##'

echo "== http/connect.go snippet =="
if [ -f http/connect.go ]; then
  cat -n http/connect.go | sed -n '1,140p'
fi

echo "== search DialTLSContext usages =="
rg -n --type go 'DialTLSContext|DialTLS|DialContext|ConnectTimeout' . || true

echo "== lint availability =='
[ -f Makefile ] && rg -n 'lint|golangci' Makefile || true

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:

#!/bin/bash
set -euo pipefail

echo "== http/connect_test.go =="
cat -n http/connect_test.go | sed -n '1,120p'

echo "== transport dial hook fields =="
go doc net/http.Transport DialTLSContext 2>/tmp/go_doc_err.log || cat /tmp/go_doc_err.log

Repository: flanksource/commons

Length of output: 3395


Apply the timeout to DialTLSContext.

ConnectTimeout copies an *http.Transport but only wraps DialContext. When Transport.DialTLSContext is set, HTTPS connections use it instead of DialContext, so custom TLS dialing can bypass ConnectTimeout. Wrap the cloned DialTLSContext with the same timeout context and add an HTTPS test using a blocking DialTLSContext.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@http/connect.go` around lines 27 - 35, Update ConnectTimeout’s
transport-cloning logic to wrap configured.DialTLSContext with the same timeout
context used by DialContext, preserving its existing fallback behavior as
appropriate. Ensure HTTPS dialing through a blocking custom DialTLSContext
respects ConnectTimeout, and add a test covering that timeout path.

c.httpClient.Transport = configured
return c, nil
}
78 changes: 78 additions & 0 deletions http/connect_test.go
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@http/connect_test.go` around lines 55 - 56, Reduce the upper-bound duration
assertions around the connection timing checks in the affected tests to a
practical limit relative to the configured 25-millisecond timeout, such as 250
milliseconds, while preserving the existing lower-bound assertions and applying
the same change to both assertion pairs.

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