Skip to content

net/http: client timeout error message is ambiguous #74625

@ChiyeungChan

Description

@ChiyeungChan

Go version

go version go1.23.2 darwin/arm64

Output of go env in your module/workspace:

GO111MODULE=''
GOARCH='arm64'
GOBIN=''
GOCACHE='/Users/zy.chen/Library/Caches/go-build'
GOENV='/Users/zy.chen/Library/Application Support/go/env'
GOEXE=''
GOEXPERIMENT=''
GOFLAGS=''
GOHOSTARCH='arm64'
GOHOSTOS='darwin'
GOINSECURE=''
GOMODCACHE='/Users/zy.chen/go/pkg/mod'
GONOPROXY='github.com/AfterShip'
GONOSUMDB='github.com/AfterShip'
GOOS='darwin'
GOPATH='/Users/zy.chen/go'
GOPRIVATE='github.com/AfterShip'
GOPROXY='direct'
GOROOT='/opt/homebrew/Cellar/go/1.23.2/libexec'
GOSUMDB='sum.golang.org'
GOTMPDIR=''
GOTOOLCHAIN='local'
GOTOOLDIR='/opt/homebrew/Cellar/go/1.23.2/libexec/pkg/tool/darwin_arm64'
GOVCS=''
GOVERSION='go1.23.2'
GODEBUG=''
GOTELEMETRY='local'
GOTELEMETRYDIR='/Users/zy.chen/Library/Application Support/go/telemetry'
GCCGO='gccgo'
GOARM64='v8.0'
AR='ar'
CC='cc'
CXX='c++'
CGO_ENABLED='1'
GOMOD='/opt/homebrew/opt/go/libexec/src/go.mod'
GOWORK=''
CGO_CFLAGS='-O2 -g'
CGO_CPPFLAGS=''
CGO_CXXFLAGS='-O2 -g'
CGO_FFLAGS='-O2 -g'
CGO_LDFLAGS='-O2 -g'
PKG_CONFIG='pkg-config'
GOGCCFLAGS='-fPIC -arch arm64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -ffile-prefix-map=/var/folders/fm/spjh4bvs1hzfsg9w8ybmc1zm0000gn/T/go-build2972010941=/tmp/go-build -gno-record-gcc-switches -fno-common'

What did you do?

Found an inconsistency in error message in net/http/client.go.

What did you see happen?

The current error message says:

if b.reqDidTimeout() {
	err = &timeoutError{err.Error() + " (Client.Timeout or context cancellation while reading body)"}
}

However, when reqDidTimeout() returns true in cancelTimerBody.Read(), it should only be due to Client.Timeout expiration, so the error message should only mention "Client.Timeout".

What did you expect to see?

The error message should only mention "Client.Timeout" as below:

err = &timeoutError{err.Error() + " (Client.Timeout exceeded while reading body)"}

Analysis

After analyzing the code in setRequestCancel(), reqDidTimeout() only returns true when:

  1. fast path: return true when client timeout exceeds
if req.Cancel == nil && knownTransport {
	// If they already had a Request.Context that's
	// expiring sooner, do nothing:
	if !timeBeforeContextDeadline(deadline, oldCtx) {
		return nop, alwaysFalse
	}

	var cancelCtx func()
	req.ctx, cancelCtx = context.WithDeadline(oldCtx, deadline)
	return cancelCtx, func() bool { return time.Now().After(deadline) }
}
  1. normal path: return true when the timer created with deadline fires (<-timer.C)
timer := time.NewTimer(time.Until(deadline))
var timedOut atomic.Bool

go func() {
	select {
	case <-initialReqCancel:
		doCancel()
		timer.Stop()
	case <-timer.C:
		timedOut.Store(true)
		doCancel()
	case <-stopTimerCh:
		timer.Stop()
	}
}()

return stopTimer, timedOut.Load

Context cancellation or timeout does not trigger timedOut.Store(true), so when reqDidTimeout() returns true, it's always due to client timeout.

Metadata

Metadata

Assignees

No one assigned

    Labels

    BugReportIssues describing a possible bug in the Go implementation.NeedsInvestigationSomeone must examine and confirm this is a valid issue and not a duplicate of an existing one.

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions