Skip to content

net/http: http client changes context cancel error behaviour in 1.23+ #72898

@phanhuyn

Description

@phanhuyn

Go version

1.23.7

Output of go env in your module/workspace:

GO111MODULE=''
GOARCH='arm64'
GOBIN=''
GOCACHE='/Users/phanhuy1502/Library/Caches/go-build'
GOENV='/Users/phanhuy1502/Library/Application Support/go/env'
GOEXE=''
GOEXPERIMENT=''
GOFLAGS=''
GOHOSTARCH='arm64'
GOHOSTOS='darwin'
GOINSECURE=''
GOMODCACHE='/Users/phanhuy1502/dev/go/pkg/mod'
GONOPROXY=''
GONOSUMDB=''
GOOS='darwin'
GOPATH='/Users/phanhuy1502/dev/go'
GOPRIVATE=''
GOPROXY='https://proxy.golang.org,direct'
GOROOT='/Users/phanhuy1502/dev/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.23.7.darwin-arm64'
GOSUMDB='sum.golang.org'
GOTMPDIR=''
GOTOOLCHAIN='auto'
GOTOOLDIR='/Users/phanhuy1502/dev/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.23.7.darwin-arm64/pkg/tool/darwin_arm64'
GOVCS=''
GOVERSION='go1.23.7'
GODEBUG=''
GOTELEMETRY='local'
GOTELEMETRYDIR='/Users/phanhuy1502/Library/Application Support/go/telemetry'
GCCGO='gccgo'
GOARM64='v8.0'
AR='ar'
CC='clang'
CXX='clang++'
CGO_ENABLED='1'
GOMOD='/Users/phanhuy1502/dev/src/playground/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/y3/rvr96zrj7wlcjhp3dsxv_wch0000gn/T/go-build3316156720=/tmp/go-build -gno-record-gcc-switches -fno-common'

What did you do?

The following code on a http client returns different error in golang 1.22 and 1.23+.

In 1.22, when context is canceled with a cause, client.Do returns context.Cancelled.

In 1.23, when context is canceled with a cause, client.Do returns context.Caused.

package main

import (
	"context"
	"errors"
	"log"
	"net/http"
)

func main() {
	client := &http.Client{}

	ctx := context.Background()
	ctx, cancel := context.WithCancelCause(ctx)
	context.Canceled

	req, err := http.NewRequestWithContext(ctx, "GET", "https://www.github.com", nil)
	if err != nil {
		log.Fatalf("Error creating request: %v\n", err)
	}

	// cancel the request with a custom cause
	cancel(errors.New("test error"))

	resp, err := client.Do(req)
	if err != nil {
		// 1.22: return context.Canceled
		// 1.23+: return "test error" (the cause)
		log.Fatalf("Error making request: %v\n", err)
	}

	resp.Body.Close()
}

What did you see happen?

In 1.22, when context is canceled with a cause, client.Do returns context.Canceled.
In 1.23, when context is canceled with a cause, client.Do returns the cause of the cancelation (context.Cause(ctx))

I'm suspecting this is related to this code change release in 1.23+.
505000b#diff-f2f92ffe0abe8dd3c833d435c2d859d54380e8e4160af8becab6945395563cfeL594

Same issue with 1.24

What did you expect to see?

The "correct" behaviour is debatable, but this change in behaviour may cause existing code to malfunction. So i'm expecting the http client should return the context.Canceled error if the context is cancelled (or at least, the returning err should satisfy errors.Is(err, context.Canceled) == true)

Perhaps, a errors.Join(context.Canceled, context.Cause(ctx) should be returned here: 505000b#diff-f2f92ffe0abe8dd3c833d435c2d859d54380e8e4160af8becab6945395563cfeL594

In my case, I rely on the err returned by http client to be context.Canceled to implement circuit breaker (if context errors.Is(err, context.Canceled) == true, don't count as an error). The upgrade to 1.23 breaks this behaviour.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions