#68111 (HTTP DialCancel flake)

/home/claude/golang_go_68111/go/src/net/http · Tue 25 Aug 2026 09:00:27 · 2 messages
ac7b6ac2-ca52-4cd6-8c2d-2d373643832d

You Tue 25 Aug 2026 08:53:03

Triage and debug go.dev/issue/68111.

Assistant Tue 25 Aug 2026 08:53:07

Ran tools (21 Bash, 1 ToolSearch, 6 WebFetch)
Bash ls && git log --oneline -3 2>&1 | head -5
command
ls && git log --oneline -3 2>&1 | head -5
description
List working directory
Result
env.sh
go
fatal: not a git repository (or any of the parent directories): .git
ToolSearch select:WebFetch,WebSearch
query
select:WebFetch,WebSearch
max_results
5
WebFetch https://github.com/golang/go/issues/68111
url
https://github.com/golang/go/issues/68111
prompt
Report the full issue title, body, labels, milestone, and all comments verbatim as much as possible.
Result
# Go Issue #68111: TestTransportDialCancelRace/h1 Failures

## Issue Summary

**Title:** net/http: TestTransportDialCancelRace/h1 failures

**Status:** Open

**Labels:** NeedsInvestigation

**Project:** Test Flakes

## Issue Details

This is an automatically generated issue created by the watchflakes system to track recurring test failures in the Go repository.

**Description:**
The test `TestTransportDialCancelRace/h1` in the net/http package is experiencing intermittent failures. 

**Error Message:**
The test reports an assertion failure where it "expected canceled request error; got <nil>" (transport_test.go:4299).

## Key Information

- **Package:** net/http
- **Test Name:** TestTransportDialCancelRace/h1
- **Failure Pattern:** Race condition-related, as indicated by the test name
- **Current Status:** Unassigned and awaiting investigation
- **Repository:** golang/go (136k stars, 19.3k forks)

## Context

The issue indicates a potential race condition in the HTTP transport layer where request cancellation is not properly propagated in certain scenarios. The test expects an error when a request is canceled, but occasionally receives nil instead.
Bash cat env.sh; ls go | head -30; cd go && git log --oneline -3 2>&1 | head
command
cat env.sh; ls go | head -30; cd go && git log --oneline -3 2>&1 | head
description
Inspect env and go tree
Result
export WORK=/home/claude/golang_go_68111
export GOROOT=$WORK/go
export GOPATH=$WORK
export GOBIN=$WORK/bin
export PATH=$GOBIN:/home/claude/.local/bin:/home/claude/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin
api
bin
codereview.cfg
CONTRIBUTING.md
doc
go.env
lib
LICENSE
misc
PATENTS
pkg
README.md
SECURITY.md
src
test
VERSION.cache
13b477edc7 runtime: fix flaky refill detection in TestScanAllocIssue77573
72755de33c os, internal/poll: use splice for copies involving pipes
3e6ad2cd0a cmd/compile: always do rewriteCondSelectIntoMath for powers of two
Bash sed -n '4250,4330p' src/net/http/transport_test.go
command
sed -n '4250,4330p' src/net/http/transport_test.go
description
Read test source
Result
		mu.Lock()
		gotReq = true
		mu.Unlock()
	})).ts
	c := ts.Client()
	c.Transport.(*Transport).DialTLS = func(netw, addr string) (net.Conn, error) {
		mu.Lock()
		didDial = true
		mu.Unlock()
		c, err := tls.Dial(netw, addr, c.Transport.(*Transport).TLSClientConfig)
		if err != nil {
			return nil, err
		}
		return c, c.Handshake()
	}

	res, err := c.Get(ts.URL)
	if err != nil {
		t.Fatal(err)
	}
	res.Body.Close()
	mu.Lock()
	if !gotReq {
		t.Error("didn't get request")
	}
	if !didDial {
		t.Error("didn't use dial hook")
	}
}

func TestTransportDialContext(t *testing.T) { run(t, testTransportDialContext, http3SkippedMode) }
func testTransportDialContext(t *testing.T, mode testMode) {
	ctxKey := "some-key"
	ctxValue := "some-value"
	var (
		mu          sync.Mutex // guards following
		gotReq      bool
		gotCtxValue any
	)

	ts := newClientServerTest(t, mode, HandlerFunc(func(w ResponseWriter, r *Request) {
		mu.Lock()
		gotReq = true
		mu.Unlock()
	})).ts
	c := ts.Client()
	c.Transport.(*Transport).DialContext = func(ctx context.Context, netw, addr string) (net.Conn, error) {
		mu.Lock()
		gotCtxValue = ctx.Value(ctxKey)
		mu.Unlock()
		return net.Dial(netw, addr)
	}

	req, err := NewRequest("GET", ts.URL, nil)
	if err != nil {
		t.Fatal(err)
	}
	ctx := context.WithValue(context.Background(), ctxKey, ctxValue)
	res, err := c.Do(req.WithContext(ctx))
	if err != nil {
		t.Fatal(err)
	}
	res.Body.Close()
	mu.Lock()
	if !gotReq {
		t.Error("didn't get request")
	}
	if got, want := gotCtxValue, ctxValue; got != want {
		t.Errorf("got context with value %v, want %v", got, want)
	}
}

func TestTransportDialTLSContext(t *testing.T) {
	run(t, testTransportDialTLSContext, []testMode{https1Mode, http2Mode})
}
func testTransportDialTLSContext(t *testing.T, mode testMode) {
	ctxKey := "some-key"
	ctxValue := "some-value"
	var (
		mu          sync.Mutex // guards following
		gotReq      bool
Bash grep -rn "TestTransportDialCancelRace" src/net/http/ | head
command
grep -rn "TestTransportDialCancelRace" src/net/http/ | head
description
Find test
Result
(Bash completed with no output)
Bash grep -rn "expected canceled request error" src/net/http/ | head; grep -rn "CancelRace" src/net/http/*.go | head
command
grep -rn "expected canceled request error" src/net/http/ | head; grep -rn "CancelRace" src/net/http/*.go | head
description
Search for error string
Result
src/net/http/transport_test.go:4665:func TestTransportResponseCancelRace(t *testing.T) { run(t, testTransportResponseCancelRace) }
src/net/http/transport_test.go:4666:func testTransportResponseCancelRace(t *testing.T, mode testMode) {
WebFetch https://api.github.com/repos/golang/go/issues/68111
url
https://api.github.com/repos/golang/go/issues/68111
prompt
Return the title and body fields verbatim.
Result
**Title:**
net/http: TestTransportDialCancelRace/h1 failures

**Body:**
```
#!watchflakes
default <- pkg == "net/http" && test == "TestTransportDialCancelRace/h1"
```

Issue created automatically to collect these failures.

Example (log):

    === RUN   TestTransportDialCancelRace/h1
        transport_test.go:4299: expected canceled request error; got <nil>
    --- FAIL: TestTransportDialCancelRace/h1 (0.00s)


— watchflakes
Bash grep -rni "dialcancel" src/ | head; echo ---; grep -rn "canceled request error" src/net/http/*.go | head
command
grep -rni "dialcancel" src/ | head; echo ---; grep -rn "canceled request error" src/net/http/*.go | head
description
Search dialcancel
Result
src/net/dial_test.go:742:func TestDialCancel(t *testing.T) {
src/net/http/transport_test.go:844:func TestTransportMaxConnsPerHostDialCancellation(t *testing.T) {
src/net/http/transport_test.go:845:	run(t, testTransportMaxConnsPerHostDialCancellation,
src/net/http/transport_test.go:851:func testTransportMaxConnsPerHostDialCancellation(t *testing.T, mode testMode) {
src/net/http/transport.go:1609:	dialCtx, dialCancel := context.WithCancel(context.WithoutCancel(ctx))
src/net/http/transport.go:1615:		cancelCtx:  dialCancel,
---
Bash sed -n '4280,4310p' src/net/http/transport_test.go; echo "=== git log ==="; git log --oneline -S"TestTransportDialCancel...
command
sed -n '4280,4310p' src/net/http/transport_test.go; echo "=== git log ==="; git log --oneline -S"TestTransportDialCancelRace" -- src/net/http/transport_test.go | head
description
Check line 4299 and history
Result
func TestTransportDialContext(t *testing.T) { run(t, testTransportDialContext, http3SkippedMode) }
func testTransportDialContext(t *testing.T, mode testMode) {
	ctxKey := "some-key"
	ctxValue := "some-value"
	var (
		mu          sync.Mutex // guards following
		gotReq      bool
		gotCtxValue any
	)

	ts := newClientServerTest(t, mode, HandlerFunc(func(w ResponseWriter, r *Request) {
		mu.Lock()
		gotReq = true
		mu.Unlock()
	})).ts
	c := ts.Client()
	c.Transport.(*Transport).DialContext = func(ctx context.Context, netw, addr string) (net.Conn, error) {
		mu.Lock()
		gotCtxValue = ctx.Value(ctxKey)
		mu.Unlock()
		return net.Dial(netw, addr)
	}

	req, err := NewRequest("GET", ts.URL, nil)
	if err != nil {
		t.Fatal(err)
	}
	ctx := context.WithValue(context.Background(), ctxKey, ctxValue)
	res, err := c.Do(req.WithContext(ctx))
	if err != nil {
		t.Fatal(err)
=== git log ===
3ed007d754 net/http: remove TestTransportDialCancelRace
723f86537c net/http: fix race between dialing and canceling
Bash git log -1 --format='%H %ad %s' 3ed007d754 723f86537c; echo "=== HEAD ==="; git log -1 --format='%H %ad %s'; echo "=== a...
command
git log -1 --format='%H %ad %s' 3ed007d754 723f86537c; echo "=== HEAD ==="; git log -1 --format='%H %ad %s'; echo "=== ancestors? ==="; git merge-base --is-ancestor 723f86537c HEAD && echo "723 is ancestor"; git merge-base --is-ancestor 3ed007d754 HEAD && echo "3ed is ancestor"
description
Check commit dates and ancestry
Result
3ed007d754b685cd8f6011a8e96a3c9303c785db Thu May 23 09:05:04 2024 -0700 net/http: remove TestTransportDialCancelRace
=== HEAD ===
13b477edc71ef88001783cad8bf0d990712a9204 Thu Aug 20 11:16:25 2026 -0400 runtime: fix flaky refill detection in TestScanAllocIssue77573
=== ancestors? ===
723 is ancestor
3ed is ancestor
Bash git status --short | head -20; echo "=== show 3ed ==="; git show --stat 3ed007d754; echo; git show 3ed007d754 | head -80
command
git status --short | head -20; echo "=== show 3ed ==="; git show --stat 3ed007d754; echo; git show 3ed007d754 | head -80
description
Git status and removal commit
Result
=== show 3ed ===
commit 3ed007d754b685cd8f6011a8e96a3c9303c785db
Author: Damien Neil <dneil@google.com>
Date:   Thu May 23 09:05:04 2024 -0700

    net/http: remove TestTransportDialCancelRace
    
    This test was added to cover a specific race condition
    in request cancellation, applying only to the deprecated
    Transport.CancelRequest cancellation path. The test
    assumes that canceling a request at the moment
    persistConn.RoundTrip begins guarantees that it will
    be canceled before being sent.
    
    This does not apply to the newer forms of canceling
    a request: Request.Cancel and context-based cancellation
    both send the cancel signal on a channel, and do not
    check for cancellation before sending a request.
    
    A recent refactoring unified the implementation
    of cancellation, so the Transport.CancelRequest
    path now translates into context-based cancellation
    internally. This makes this test flaky, since
    sometimes the request completes before we read
    from the context's done channel.
    
    Drop the test entirely. It's verifying the fix
    for a bug in a code path which no longer exists,
    and the property that it's testing for (canceling
    a request at a very specific point in the internal
    request flow) is not interesting.
    
    Fixes #67533
    
    Change-Id: I8d71540f1b44a64e0621d31a1c545c9351ae897c
    Reviewed-on: https://go-review.googlesource.com/c/go/+/587935
    Reviewed-by: Austin Clements <austin@google.com>
    LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
    Auto-Submit: Damien Neil <dneil@google.com>

 src/net/http/transport_test.go | 24 ------------------------
 1 file changed, 24 deletions(-)

commit 3ed007d754b685cd8f6011a8e96a3c9303c785db
Author: Damien Neil <dneil@google.com>
Date:   Thu May 23 09:05:04 2024 -0700

    net/http: remove TestTransportDialCancelRace
    
    This test was added to cover a specific race condition
    in request cancellation, applying only to the deprecated
    Transport.CancelRequest cancellation path. The test
    assumes that canceling a request at the moment
    persistConn.RoundTrip begins guarantees that it will
    be canceled before being sent.
    
    This does not apply to the newer forms of canceling
    a request: Request.Cancel and context-based cancellation
    both send the cancel signal on a channel, and do not
    check for cancellation before sending a request.
    
    A recent refactoring unified the implementation
    of cancellation, so the Transport.CancelRequest
    path now translates into context-based cancellation
    internally. This makes this test flaky, since
    sometimes the request completes before we read
    from the context's done channel.
    
    Drop the test entirely. It's verifying the fix
    for a bug in a code path which no longer exists,
    and the property that it's testing for (canceling
    a request at a very specific point in the internal
    request flow) is not interesting.
    
    Fixes #67533
    
    Change-Id: I8d71540f1b44a64e0621d31a1c545c9351ae897c
    Reviewed-on: https://go-review.googlesource.com/c/go/+/587935
    Reviewed-by: Austin Clements <austin@google.com>
    LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
    Auto-Submit: Damien Neil <dneil@google.com>

diff --git a/src/net/http/transport_test.go b/src/net/http/transport_test.go
index 25876e8d16..aa877b57c7 100644
--- a/src/net/http/transport_test.go
+++ b/src/net/http/transport_test.go
@@ -4279,30 +4279,6 @@ func testTransportContentEncodingCaseInsensitive(t *testing.T, mode testMode) {
 	}
 }
 
-func TestTransportDialCancelRace(t *testing.T) {
-	run(t, testTransportDialCancelRace, testNotParallel, []testMode{http1Mode})
-}
-func testTransportDialCancelRace(t *testing.T, mode testMode) {
-	ts := newClientServerTest(t, mode, HandlerFunc(func(w ResponseWriter, r *Request) {})).ts
-	tr := ts.Client().Transport.(*Transport)
-
-	req, err := NewRequest("GET", ts.URL, nil)
-	if err != nil {
-		t.Fatal(err)
-	}
-	SetEnterRoundTripHook(func() {
-		tr.CancelRequest(req)
-	})
-	defer SetEnterRoundTripHook(nil)
-	res, err := tr.RoundTrip(req)
-	if err != ExportErrRequestCanceled {
-		t.Errorf("expected canceled request error; got %v", err)
-		if err == nil {
-			res.Body.Close()
-		}
-	}
-}
-
 // https://go.dev/issue/49621
 func TestConnClosedBeforeRequestIsWritten(t *testing.T) {
 	run(t, testConnClosedBeforeRequestIsWritten, testNotParallel, []testMode{http1Mode})
WebFetch https://api.github.com/repos/golang/go/issues/68111/comments
url
https://api.github.com/repos/golang/go/issues/68111/comments
prompt
List every comment: author, date, and full body text verbatim.
Result
# Comments Summary

**Comment 1**
- **Author:** gopherbot
- **Date:** 2024-06-21T19:16:30Z
- **Body:** Report of test flakes for `net/http.TestTransportDialCancelRace/h1`. Notes a failure from 2024-05-20 where "expected canceled request error; got <nil>" occurred in transport_test.go at line 4299.

**Comment 2**
- **Author:** gabyhelp
- **Date:** 2024-06-21T19:17:46Z
- **Body:** Lists ten related issues in the net/http package, including "TestTransportDialCancelRace/h1 failures #67533" (highest similarity score of 0.99998) and other test failure issues. Invites emoji reactions and detailed feedback via discussion link.
WebFetch https://api.github.com/repos/golang/go/issues/68111/comments?per_page=5
url
https://api.github.com/repos/golang/go/issues/68111/comments?per_page=5
prompt
Show the raw "body" field of the first comment (author gopherbot) exactly, including every failure line, builder name, date, and URL.
Result
# Raw Body Field from First Comment

Here is the exact body content from gopherbot's comment:

```
Found new dashboard test flakes for:

    #!watchflakes
    default <- pkg == "net/http" && test == "TestTransportDialCancelRace/h1"

<details><summary>2024-05-20 15:47 gotip-linux-amd64-boringcrypto go@7ffa8d0a net/http.TestTransportDialCancelRace/h1 (<a href="https://ci.chromium.org/b/8747406538770871361">log</a>)</summary>

    === RUN   TestTransportDialCancelRace/h1
        transport_test.go:4299: expected canceled request error; got <nil>
    --- FAIL: TestTransportDialCancelRace/h1 (0.00s)
</details>


— [watchflakes](https://go.dev/wiki/Watchflakes)
```

**Key details:**
- **Builder:** gotip-linux-amd64-boringcrypto
- **Commit:** go@7ffa8d0a
- **Date/Time:** 2024-05-20 15:47
- **Test:** TestTransportDialCancelRace/h1
- **Failure:** Expected canceled request error but received nil
- **Log URL:** https://ci.chromium.org/b/8747406538770871361
Bash git log --oneline --since=2024-05-01 --until=2024-06-30 -- src/net/http/transport_test.go | head -20; echo "=== when did...
command
git log --oneline --since=2024-05-01 --until=2024-06-30 -- src/net/http/transport_test.go | head -20; echo "=== when did 723 land ==="; git show -s --format='%H %ad%n%s%n%b' 723f86537c
description
Related commits timeline
Result
cf501e05e1 net/http: send body or close connection on expect-100-continue requests
3ed007d754 net/http: remove TestTransportDialCancelRace
a61729b880 net/http: return correct error when reading from a canceled request body
=== when did 723 land ===
723f86537c362f2f86c82ae827f689c23b866f00 Mon Apr 20 23:02:07 2015 +0100
net/http: fix race between dialing and canceling
In the brief window between getConn and persistConn.roundTrip,
a cancel could end up going missing.

Fix by making it possible to inspect if a cancel function was cleared
and checking if we were canceled before entering roundTrip.

Fixes #10511

Change-Id: If6513e63fbc2edb703e36d6356ccc95a1dc33144
Reviewed-on: https://go-review.googlesource.com/9181
Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
Bash git show -s --format='%H %ad %s' 7ffa8d0a 2>/dev/null; echo "=== hook still present? ==="; grep -rn "EnterRoundTripHook\...
command
git show -s --format='%H %ad %s' 7ffa8d0a 2>/dev/null; echo "=== hook still present? ==="; grep -rn "EnterRoundTripHook\|testHookEnterRoundTrip" src/net/http/ | head; echo "=== ExportErrRequestCanceled ==="; grep -rn "ExportErrRequestCanceled" src/net/http/ | head
description
Check flake commit date and leftover hooks
Result
7ffa8d0a5b286add7b618b8177c8ce39414cb60a Sat May 18 15:33:57 2024 -0700 cmd/go: accept -mcmodel and -mlarge-data-threshold compiler flags
=== hook still present? ===
src/net/http/export_test.go:70:	SetEnterRoundTripHook = hookSetter(&testHookEnterRoundTrip)
src/net/http/transport.go:2969:	testHookEnterRoundTrip   = nop
src/net/http/transport.go:2991:	testHookEnterRoundTrip()
src/net/http/transport_test.go:4759:	SetEnterRoundTripHook(func() {
src/net/http/transport_test.go:4762:	defer SetEnterRoundTripHook(nil)
=== ExportErrRequestCanceled ===
src/net/http/export_test.go:27:	ExportErrRequestCanceled          = errRequestCanceled
src/net/http/export_test.go:28:	ExportErrRequestCanceledConn      = errRequestCanceledConn
src/net/http/clientserver_test.go:910:	if err != ExportErrRequestCanceled {
src/net/http/clientserver_test.go:911:		t.Errorf("ReadAll error = %v; want %v", err, ExportErrRequestCanceled)
src/net/http/client_test.go:1369:	if err != ExportErrRequestCanceled {
src/net/http/transport_test.go:2802:				if !errors.Is(err, ExportErrRequestCanceled) && !errors.Is(err, ExportErrRequestCanceledConn) {
src/net/http/transport_test.go:2824:			if !errors.Is(err, ExportErrRequestCanceled) && !errors.Is(err, ExportErrRequestCanceledConn) {
src/net/http/transport_test.go:5161:	if ue, ok := err.(*url.Error); !ok || ue.Err != ExportErrRequestCanceledConn {
WebFetch https://api.github.com/repos/golang/go/issues/67533
url
https://api.github.com/repos/golang/go/issues/67533
prompt
Give title, state, state_reason, closed_at, and body verbatim.
Result
**Title:** net/http: TestTransportDialCancelRace/h1 failures

**State:** closed

**State_reason:** completed

**Closed_at:** 2024-05-23T16:36:22Z

**Body:**
```
#!watchflakes
default <- pkg == "net/http" && test == "TestTransportDialCancelRace/h1"
```

Issue created automatically to collect these failures.

Example (log):

    === RUN   TestTransportDialCancelRace/h1
        transport_test.go:4299: expected canceled request error; got <nil>
    --- FAIL: TestTransportDialCancelRace/h1 (0.00s)


— watchflakes
Bash sed -n '2960,3010p' src/net/http/transport.go; echo "=== CancelRequest ==="; grep -n "func (t \*Transport) CancelRequest...
command
sed -n '2960,3010p' src/net/http/transport.go; echo "=== CancelRequest ==="; grep -n "func (t \*Transport) CancelRequest" -A 25 src/net/http/transport.go
description
Inspect roundTrip hook and CancelRequest
Result
// errRequestDone is used to cancel the round trip Context after a request is successfully done.
// It should not be seen by the user.
var errRequestDone = errors.New("net/http: request completed")

func nop() {}

// testHooks. Always non-nil.
var (
	testHookEnterRoundTrip   = nop
	testHookWaitResLoop      = nop
	testHookRoundTripRetried = nop
	testHookPrePendingDial   = nop
	testHookPostPendingDial  = nop

	testHookMu                     sync.Locker = fakeLocker{} // guards following
	testHookReadLoopBeforeNextRead             = nop
)

func (pc *persistConn) waitForAvailability(ctx context.Context) error {
	select {
	case <-pc.availch:
		return nil
	case <-pc.closech:
		return pc.closed
	case <-ctx.Done():
		return ctx.Err()
	}
}

func (pc *persistConn) roundTrip(req *transportRequest) (resp *Response, err error) {
	testHookEnterRoundTrip()

	pc.mu.Lock()
	if pc.isClientConn {
		if !pc.reserved {
			pc.mu.Unlock()
			if err := pc.waitForAvailability(req.ctx); err != nil {
				return nil, err
			}
			pc.mu.Lock()
		}
		pc.reserved = false
		pc.inFlight = true
	}
	pc.numExpectedResponses++
	headerFn := pc.mutateHeaderFunc
	pc.mu.Unlock()

	if headerFn != nil {
		headerFn(req.extraHeaders())
=== CancelRequest ===
1031:func (t *Transport) CancelRequest(req *Request) {
1032-	t.reqMu.Lock()
1033-	cancel := t.reqCanceler[req]
1034-	t.reqMu.Unlock()
1035-	if cancel != nil {
1036-		cancel(errRequestCanceled)
1037-	}
1038-}
1039-
1040-//
1041-// Private implementation past this point.
1042-//
1043-
1044-var (
1045-	envProxyOnce      sync.Once
1046-	envProxyFuncValue func(*url.URL) (*url.URL, error)
1047-)
1048-
1049-// envProxyFunc returns a function that reads the
1050-// environment variable to determine the proxy address.
1051-func envProxyFunc() func(*url.URL) (*url.URL, error) {
1052-	envProxyOnce.Do(func() {
1053-		envProxyFuncValue = httpproxy.FromEnvironment().ProxyFunc()
1054-	})
1055-	return envProxyFuncValue
1056-}
Bash cd /home/claude/golang_go_68111 && . ./env.sh && which go && go version
command
cd /home/claude/golang_go_68111 && . ./env.sh && which go && go version
description
Check go toolchain
Result
/usr/bin/go
go version go1.22.2 linux/amd64
WebFetch https://api.github.com/repos/golang/go/issues/68111?x=1
url
https://api.github.com/repos/golang/go/issues/68111?x=1
prompt
Report state, comments count, created_at, updated_at, labels, and assignees.
Result
# GitHub Issue Summary

**State:** Open

**Comments:** 2

**Created:** June 21, 2024 at 19:15:51 UTC

**Updated:** June 21, 2024 at 19:17:48 UTC

**Labels:** NeedsInvestigation

**Assignees:** None

---

This issue tracks flaky test failures in `TestTransportDialCancelRace/h1` within the net/http package. The test failure shows "expected canceled request error; got <nil>", suggesting a race condition in transport dial cancellation logic.
Bash ls bin go/bin 2>&1 | head; ls go/pkg/tool/linux_amd64 2>/dev/null | head -3
command
ls bin go/bin 2>&1 | head; ls go/pkg/tool/linux_amd64 2>/dev/null | head -3
description
Check for built toolchain
Result
ls: cannot access 'bin': No such file or directory
go/bin:
go
gofmt
asm
cgo
compile
Bash cd /home/claude/golang_go_68111/go/src/net/http && cat > zz_issue68111_test.go <<'EOF' ...
command
cd /home/claude/golang_go_68111/go/src/net/http && cat > zz_issue68111_test.go <<'EOF'
package http_test

import (
	"testing"

	. "net/http"
)

func TestIssue68111DialCancelRace(t *testing.T) {
	run(t, testIssue68111DialCancelRace, testNotParallel, []testMode{http1Mode})
}
func testIssue68111DialCancelRace(t *testing.T, mode testMode) {
	ts := newClientServerTest(t, mode, HandlerFunc(func(w ResponseWriter, r *Request) {})).ts
	tr := ts.Client().Transport.(*Transport)

	req, err := NewRequest("GET", ts.URL, nil)
	if err != nil {
		t.Fatal(err)
	}
	SetEnterRoundTripHook(func() {
		tr.CancelRequest(req)
	})
	defer SetEnterRoundTripHook(nil)
	res, err := tr.RoundTrip(req)
	if err != ExportErrRequestCanceled {
		t.Errorf("expected canceled request error; got %v", err)
		if err == nil {
			res.Body.Close()
		}
	}
}
EOF
GOROOT=/home/claude/golang_go_68111/go /home/claude/golang_go_68111/go/bin/go test -run 'TestIssue68111DialCancelRace' -count=200 net/http 2>&1 | tail -25
description
Reconstruct and stress the removed test
Result
ok  	net/http	0.107s
Bash export GOROOT=/home/claude/golang_go_68111/go && for i in 1 2 3 4 5 6; do (/home/claude/golang_go_68111/go/bin/go test -...
command
export GOROOT=/home/claude/golang_go_68111/go && for i in 1 2 3 4 5 6; do (/home/claude/golang_go_68111/go/bin/go test -run 'TestIssue68111DialCancelRace' -count=3000 -cpu=1,2,4 net/http > /tmp/claude-1003/-home-claude-golang-go-68111/ac7b6ac2-ca52-4cd6-8c2d-2d373643832d/scratchpad/stress$i.txt 2>&1) & done; wait; grep -l FAIL /tmp/claude-1003/-home-claude-golang-go-68111/ac7b6ac2-ca52-4cd6-8c2d-2d373643832d/scratchpad/stress*.txt; grep -h -m2 "expected canceled" /tmp/claude-1003/-home-claude-golang-go-68111/ac7b6ac2-ca52-4cd6-8c2d-2d373643832d/scratchpad/stress*.txt | head; tail -3 /tmp/claude-1003/-home-claude-golang-go-68111/ac7b6ac2-ca52-4cd6-8c2d-2d373643832d/scratchpad/stress1.txt
description
Stress test under load
Result
[1]   Done                    ( /home/claude/golang_go_68111/go/bin/go test -run 'TestIssue68111DialCancelRace' -count=3000 -cpu=1,2,4 net/http > /tmp/claude-1003/-home-claude-golang-go-68111/ac7b6ac2-ca52-4cd6-8c2d-2d373643832d/scratchpad/stress$i.txt 2>&1 )
[2]   Done                    ( /home/claude/golang_go_68111/go/bin/go test -run 'TestIssue68111DialCancelRace' -count=3000 -cpu=1,2,4 net/http > /tmp/claude-1003/-home-claude-golang-go-68111/ac7b6ac2-ca52-4cd6-8c2d-2d373643832d/scratchpad/stress$i.txt 2>&1 )
[5]-  Done                    ( /home/claude/golang_go_68111/go/bin/go test -run 'TestIssue68111DialCancelRace' -count=3000 -cpu=1,2,4 net/http > /tmp/claude-1003/-home-claude-golang-go-68111/ac7b6ac2-ca52-4cd6-8c2d-2d373643832d/scratchpad/stress$i.txt 2>&1 )
[6]+  Done                    ( /home/claude/golang_go_68111/go/bin/go test -run 'TestIssue68111DialCancelRace' -count=3000 -cpu=1,2,4 net/http > /tmp/claude-1003/-home-claude-golang-go-68111/ac7b6ac2-ca52-4cd6-8c2d-2d373643832d/scratchpad/stress$i.txt 2>&1 )
[3]-  Done                    ( /home/claude/golang_go_68111/go/bin/go test -run 'TestIssue68111DialCancelRace' -count=3000 -cpu=1,2,4 net/http > /tmp/claude-1003/-home-claude-golang-go-68111/ac7b6ac2-ca52-4cd6-8c2d-2d373643832d/scratchpad/stress$i.txt 2>&1 )
[4]+  Done                    ( /home/claude/golang_go_68111/go/bin/go test -run 'TestIssue68111DialCancelRace' -count=3000 -cpu=1,2,4 net/http > /tmp/claude-1003/-home-claude-golang-go-68111/ac7b6ac2-ca52-4cd6-8c2d-2d373643832d/scratchpad/stress$i.txt 2>&1 )
ok  	net/http	9.088s
Bash cd /home/claude/golang_go_68111/go && sed -n '2989,3130p' src/net/http/transport.go
command
cd /home/claude/golang_go_68111/go && sed -n '2989,3130p' src/net/http/transport.go
description
Read persistConn.roundTrip
Result
func (pc *persistConn) roundTrip(req *transportRequest) (resp *Response, err error) {
	testHookEnterRoundTrip()

	pc.mu.Lock()
	if pc.isClientConn {
		if !pc.reserved {
			pc.mu.Unlock()
			if err := pc.waitForAvailability(req.ctx); err != nil {
				return nil, err
			}
			pc.mu.Lock()
		}
		pc.reserved = false
		pc.inFlight = true
	}
	pc.numExpectedResponses++
	headerFn := pc.mutateHeaderFunc
	pc.mu.Unlock()

	if headerFn != nil {
		headerFn(req.extraHeaders())
	}

	// Ask for a compressed version if the caller didn't set their
	// own value for Accept-Encoding. We only attempt to
	// uncompress the gzip stream if we were the layer that
	// requested it.
	requestedGzip := false
	if !pc.t.DisableCompression &&
		req.Header.Get("Accept-Encoding") == "" &&
		req.Header.Get("Range") == "" &&
		req.Method != "HEAD" {
		// Request gzip only, not deflate. Deflate is ambiguous and
		// not as universally supported anyway.
		// See: https://zlib.net/zlib_faq.html#faq39
		//
		// Note that we don't request this for HEAD requests,
		// due to a bug in nginx:
		//   https://trac.nginx.org/nginx/ticket/358
		//   https://golang.org/issue/5522
		//
		// We don't request gzip if the request is for a range, since
		// auto-decoding a portion of a gzipped document will just fail
		// anyway. See https://golang.org/issue/8923
		requestedGzip = true
		req.extraHeaders().Set("Accept-Encoding", "gzip")
	}

	var continueCh chan struct{}
	if req.ProtoAtLeast(1, 1) && req.Body != nil && req.expectsContinue() {
		continueCh = make(chan struct{}, 1)
	}

	if pc.t.DisableKeepAlives &&
		!req.wantsClose() &&
		!isProtocolSwitchHeader(req.Header) {
		req.extraHeaders().Set("Connection", "close")
	}

	gone := make(chan struct{})
	defer close(gone)

	const debugRoundTrip = false

	// Write the request concurrently with waiting for a response,
	// in case the server decides to reply before reading our full
	// request body.
	startBytesWritten := pc.nwrite
	writeErrCh := make(chan error, 1)
	pc.writech <- writeRequest{req, writeErrCh, continueCh}

	resc := make(chan responseAndError)
	pc.reqch <- requestAndChan{
		treq:       req,
		ch:         resc,
		addedGzip:  requestedGzip,
		continueCh: continueCh,
		callerGone: gone,
	}

	handleResponse := func(re responseAndError) (*Response, error) {
		if (re.res == nil) == (re.err == nil) {
			panic(fmt.Sprintf("internal error: exactly one of res or err should be set; nil=%v", re.res == nil))
		}
		if debugRoundTrip {
			req.logf("resc recv: %p, %T/%#v", re.res, re.err, re.err)
		}
		if re.err != nil {
			return nil, pc.mapRoundTripError(req, startBytesWritten, re.err)
		}
		return re.res, nil
	}

	var respHeaderTimer <-chan time.Time
	ctxDoneChan := req.ctx.Done()
	pcClosed := pc.closech
	for {
		testHookWaitResLoop()
		select {
		case err := <-writeErrCh:
			if debugRoundTrip {
				req.logf("writeErrCh recv: %T/%#v", err, err)
			}
			if err != nil {
				pc.close(fmt.Errorf("write error: %w", err))
				return nil, pc.mapRoundTripError(req, startBytesWritten, err)
			}
			if d := pc.t.ResponseHeaderTimeout; d > 0 {
				if debugRoundTrip {
					req.logf("starting timer for %v", d)
				}
				timer := time.NewTimer(d)
				defer timer.Stop() // prevent leaks
				respHeaderTimer = timer.C
			}
		case <-pcClosed:
			select {
			case re := <-resc:
				// The pconn closing raced with the response to the request,
				// probably after the server wrote a response and immediately
				// closed the connection. Use the response.
				return handleResponse(re)
			default:
			}
			if debugRoundTrip {
				req.logf("closech recv: %T %#v", pc.closed, pc.closed)
			}
			return nil, pc.mapRoundTripError(req, startBytesWritten, pc.closed)
		case <-respHeaderTimer:
			if debugRoundTrip {
				req.logf("timeout waiting for response headers.")
			}
			pc.close(errTimeout)
			return nil, errTimeout
		case re := <-resc:
			return handleResponse(re)
		case <-ctxDoneChan:
			select {
			case re := <-resc:
				// readLoop is responsible for canceling req.ctx after
				// it reads the response body. Check for a response racing
Bash sed -n '3130,3160p' src/net/http/transport.go
command
sed -n '3130,3160p' src/net/http/transport.go
description
Rest of select loop
Result
				// it reads the response body. Check for a response racing
				// the context close, and use the response if available.
				return handleResponse(re)
			default:
			}
			pc.cancelRequest(context.Cause(req.ctx))
		}
	}
}

// tLogKey is a context WithValue key for test debugging contexts containing
// a t.Logf func. See export_test.go's Request.WithT method.
type tLogKey struct{}

func (tr *transportRequest) logf(format string, args ...any) {
	if logf, ok := tr.Request.Context().Value(tLogKey{}).(func(string, ...any)); ok {
		logf(time.Now().Format(time.RFC3339Nano)+": "+format, args...)
	}
}

// markReused marks this connection as having been successfully used for a
// request and response.
func (pc *persistConn) markReused() {
	pc.mu.Lock()
	pc.reused = true
	pc.mu.Unlock()
}

// close closes the underlying TCP connection and closes
// the pc.closech channel.
//
Bash cd /home/claude/golang_go_68111/go/src/net/http && grep -n "SetEnterRoundTripHook\|WaitResLoop\|hookSetter" export_test....
command
cd /home/claude/golang_go_68111/go/src/net/http && grep -n "SetEnterRoundTripHook\|WaitResLoop\|hookSetter" export_test.go | head
description
Check exported hooks
Result
70:	SetEnterRoundTripHook = hookSetter(&testHookEnterRoundTrip)
71:	SetRoundTripRetried   = hookSetter(&testHookRoundTripRetried)
238:func hookSetter(dst *func()) func(func()) {
Bash cat > zz_issue68111_export_test.go <<'EOF' ...
command
cat > zz_issue68111_export_test.go <<'EOF'
package http

var SetWaitResLoopHook = hookSetter(&testHookWaitResLoop)
EOF
cat > zz_issue68111_test.go <<'EOF'
package http_test

import (
	"sync/atomic"
	"testing"
	"time"

	. "net/http"
)

// Reconstruction of the test deleted in CL 587935 (issue #67533 / #68111),
// with an artificial scheduling delay between handing the request to the
// persistConn and entering the response-wait select loop. That delay makes
// the response arrive on resc before the loop starts, so the select at the
// top of persistConn.roundTrip has BOTH resc and ctx.Done() ready and picks
// uniformly at random -- which is exactly the flake.
func TestIssue68111DialCancelRace(t *testing.T) {
	run(t, testIssue68111DialCancelRace, testNotParallel, []testMode{http1Mode})
}
func testIssue68111DialCancelRace(t *testing.T, mode testMode) {
	ts := newClientServerTest(t, mode, HandlerFunc(func(w ResponseWriter, r *Request) {})).ts
	tr := ts.Client().Transport.(*Transport)

	req, err := NewRequest("GET", ts.URL, nil)
	if err != nil {
		t.Fatal(err)
	}
	SetEnterRoundTripHook(func() {
		tr.CancelRequest(req)
	})
	defer SetEnterRoundTripHook(nil)
	var once atomic.Bool
	SetWaitResLoopHook(func() {
		if once.CompareAndSwap(false, true) {
			time.Sleep(50 * time.Millisecond) // let the response land in resc first
		}
	})
	defer SetWaitResLoopHook(nil)
	res, err := tr.RoundTrip(req)
	if err != ExportErrRequestCanceled {
		t.Errorf("expected canceled request error; got %v", err)
		if err == nil {
			res.Body.Close()
		}
	}
}
EOF
GOROOT=/home/claude/golang_go_68111/go /home/claude/golang_go_68111/go/bin/go test -run 'TestIssue68111DialCancelRace' -count=30 net/http 2>&1 | grep -c "expected canceled request error"; echo "---(count of failures out of 30)---"
description
Deterministic race demo
Result
30
---(count of failures out of 30)---
Bash rm -f zz_issue68111_test.go zz_issue68111_export_test.go && git status --short
command
rm -f zz_issue68111_test.go zz_issue68111_export_test.go && git status --short
description
Clean up temp test files
Result
(Bash completed with no output)

Triage verdict: duplicate of #67533, already fixed. Close as a stale watchflakes report — no code change needed.

What the issue is

#68111 is a watchflakes issue for net/http.TestTransportDialCancelRace/h1 failing with expected canceled request error; got <nil> at transport_test.go:4299.

Why it needs no fix

The timeline settles it:

When What
2024-05-18 go@7ffa8d0a (the commit in the flake report)
2024-05-20 15:47 The one and only failure, gotip-linux-amd64-boringcrypto
2024-05-23 16:36 #67533 closed by 3ed007d754 "net/http: remove TestTransportDialCancelRace" (CL 587935, Damien Neil)
2024-06-21 19:15 watchflakes files #68111 — from that stale 2024-05-20 log
2024-06-21 19:17 → today zero further activity, 26 months

watchflakes opened a fresh issue because #67533 was already closed, but the failure it cited predates the fix by three days — the test had been deleted before #68111 ever existed. gabyhelp even flagged #67533 at similarity 0.99998. Confirmed against the local tree at 13b477edc7 (Aug 2026): TestTransportDialCancelRace exists nowhere in src/.

The underlying bug (why deleting the test was right)

The test cancelled the request from testHookEnterRoundTrip — the first statement of persistConn.roundTrip — and demanded ErrRequestCanceled back. Once Transport.CancelRequest was unified onto context cancellation (transport.go:1031-1038: cancel(errRequestCanceled)), that premise stopped holding. The response-wait loop at transport.go:3081-3136 selects over both resc and ctxDoneChan, and the ctxDoneChan arm deliberately prefers a racing response:

case <-ctxDoneChan:
    select {
    case re := <-resc:
        // readLoop is responsible for canceling req.ctx after
        // it reads the response body. Check for a response racing
        // the context close, and use the response if available.
        return handleResponse(re)
    default:
    }
    pc.cancelRequest(context.Cause(req.ctx))

So if the response lands before the loop is entered, RoundTrip returns it and err == nil — by design, not by defect.

I verified both halves of that on this tree:

  • Unloaded: 54,000 runs of the reconstructed test (6 parallel × -count=3000 × -cpu=1,2,4) — zero failures. The cancel normally wins because the request hasn't even been written when the hook fires.
  • With the race window forced open: injecting a 50 ms delay at testHookWaitResLoop (between pc.reqch <- and the select loop) so the response is already queued — 30/30 failures, deterministic, and deterministic precisely because of the prefer-the-response default arm above.

That is exactly the "scheduling hiccup on a loaded builder" the single boringcrypto failure represents. The transport is behaving correctly; the test was asserting a guarantee the API never made. (Scratch test files removed; tree is clean.)

Suggested action

Close #68111 as a duplicate of #67533, fixed by CL 587935. Nothing to backport — the flake can't recur on any branch that has the deletion, and no builder has reported it in 26 months.