Triage and debug go.dev/issue/68111.
#68111 (HTTP DialCancel flake)
/home/claude/golang_go_68111/go/src/net/http · Tue 25 Aug 2026 09:00:27 · 2 messagesac7b6ac2-ca52-4cd6-8c2d-2d373643832d
You Tue 25 Aug 2026 08:53:03
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
ls && git log --oneline -3 2>&1 | head -5
List working directory
env.sh go fatal: not a git repository (or any of the parent directories): .git
ToolSearch select:WebFetch,WebSearch
select:WebFetch,WebSearch
5
WebFetch https://github.com/golang/go/issues/68111
https://github.com/golang/go/issues/68111
Report the full issue title, body, labels, milestone, and all comments verbatim as much as possible.
# 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
cat env.sh; ls go | head -30; cd go && git log --oneline -3 2>&1 | head
Inspect env and go tree
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
sed -n '4250,4330p' src/net/http/transport_test.go
Read test source
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 boolBash grep -rn "TestTransportDialCancelRace" src/net/http/ | head
grep -rn "TestTransportDialCancelRace" src/net/http/ | head
Find test
(Bash completed with no output)
Bash grep -rn "expected canceled request error" src/net/http/ | head; grep -rn "CancelRace" src/net/http/*.go | head
grep -rn "expected canceled request error" src/net/http/ | head; grep -rn "CancelRace" src/net/http/*.go | head
Search for error string
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
https://api.github.com/repos/golang/go/issues/68111
Return the title and body fields verbatim.
**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)
— watchflakesBash grep -rni "dialcancel" src/ | head; echo ---; grep -rn "canceled request error" src/net/http/*.go | head
grep -rni "dialcancel" src/ | head; echo ---; grep -rn "canceled request error" src/net/http/*.go | head
Search dialcancel
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...
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
Check line 4299 and history
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 cancelingBash git log -1 --format='%H %ad %s' 3ed007d754 723f86537c; echo "=== HEAD ==="; git log -1 --format='%H %ad %s'; echo "=== a...
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"
Check commit dates and ancestry
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
git status --short | head -20; echo "=== show 3ed ==="; git show --stat 3ed007d754; echo; git show 3ed007d754 | head -80
Git status and removal commit
=== 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
https://api.github.com/repos/golang/go/issues/68111/comments
List every comment: author, date, and full body text verbatim.
# 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
https://api.github.com/repos/golang/go/issues/68111/comments?per_page=5
Show the raw "body" field of the first comment (author gopherbot) exactly, including every failure line, builder name, date, and URL.
# 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/8747406538770871361Bash git log --oneline --since=2024-05-01 --until=2024-06-30 -- src/net/http/transport_test.go | head -20; echo "=== when did...
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
Related commits timeline
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\...
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
Check flake commit date and leftover hooks
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
https://api.github.com/repos/golang/go/issues/67533
Give title, state, state_reason, closed_at, and body verbatim.
**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)
— watchflakesBash sed -n '2960,3010p' src/net/http/transport.go; echo "=== CancelRequest ==="; grep -n "func (t \*Transport) CancelRequest...
sed -n '2960,3010p' src/net/http/transport.go; echo "=== CancelRequest ==="; grep -n "func (t \*Transport) CancelRequest" -A 25 src/net/http/transport.go
Inspect roundTrip hook and CancelRequest
// 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
cd /home/claude/golang_go_68111 && . ./env.sh && which go && go version
Check go toolchain
/usr/bin/go go version go1.22.2 linux/amd64
WebFetch https://api.github.com/repos/golang/go/issues/68111?x=1
https://api.github.com/repos/golang/go/issues/68111?x=1
Report state, comments count, created_at, updated_at, labels, and assignees.
# 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
ls bin go/bin 2>&1 | head; ls go/pkg/tool/linux_amd64 2>/dev/null | head -3
Check for built toolchain
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' ...
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 -25Reconstruct and stress the removed test
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 -...
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
Stress test under load
[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
cd /home/claude/golang_go_68111/go && sed -n '2989,3130p' src/net/http/transport.go
Read persistConn.roundTrip
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 racingBash sed -n '3130,3160p' src/net/http/transport.go
sed -n '3130,3160p' src/net/http/transport.go
Rest of select loop
// 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....
cd /home/claude/golang_go_68111/go/src/net/http && grep -n "SetEnterRoundTripHook\|WaitResLoop\|hookSetter" export_test.go | head
Check exported hooks
70: SetEnterRoundTripHook = hookSetter(&testHookEnterRoundTrip)
71: SetRoundTripRetried = hookSetter(&testHookRoundTripRetried)
238:func hookSetter(dst *func()) func(func()) {Bash cat > zz_issue68111_export_test.go <<'EOF' ...
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)---"Deterministic race demo
30 ---(count of failures out of 30)---
Bash rm -f zz_issue68111_test.go zz_issue68111_export_test.go && git status --short
rm -f zz_issue68111_test.go zz_issue68111_export_test.go && git status --short
Clean up temp test files
(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(betweenpc.reqch <-and the select loop) so the response is already queued — 30/30 failures, deterministic, and deterministic precisely because of the prefer-the-responsedefaultarm 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.