Skip to content

net/http: install cookies into jar when following redirects #74877

@work-robot

Description

@work-robot

Go version

go version go1.24.4 linux/amd64

Output of go env in your module/workspace:

AR='ar'
CC='gcc'
CGO_CFLAGS='-O2 -g'
CGO_CPPFLAGS=''
CGO_CXXFLAGS='-O2 -g'
CGO_ENABLED='1'
CGO_FFLAGS='-O2 -g'
CGO_LDFLAGS='-O2 -g'
CXX='g++'
GCCGO='gccgo'
GO111MODULE=''
GOAMD64='v1'
GOARCH='amd64'
GOAUTH='netrc'
GOBIN=''
GOCACHE='/home/<name>/.cache/go-build'
GOCACHEPROG=''
GODEBUG=''
GOENV='/home/<name>/.config/go/env'
GOEXE=''
GOEXPERIMENT=''
GOFIPS140='off'
GOFLAGS=''
GOGCCFLAGS='-fPIC -m64 -pthread -Wl,--no-gc-sections -fmessage-length=0 -ffile-prefix-map=/tmp/go-build456588333=/tmp/go-build -gno-record-gcc-switches'
GOHOSTARCH='amd64'
GOHOSTOS='linux'
GOINSECURE=''
GOMOD='/dev/null'
GOMODCACHE='/home/<name>/go/pkg/mod'
GONOPROXY=''
GONOSUMDB=''
GOOS='linux'
GOPATH='/home/<name>/go'
GOPRIVATE=''
GOPROXY='https://proxy.golang.org,direct'
GOROOT='/home/<name>/goinstall'
GOSUMDB='sum.golang.org'
GOTELEMETRY='local'
GOTELEMETRYDIR='/home/<name>/.config/go/telemetry'
GOTMPDIR=''
GOTOOLCHAIN='auto'
GOTOOLDIR='/home/<name>/goinstall/pkg/tool/linux_amd64'
GOVCS=''
GOVERSION='go1.24.4'
GOWORK=''
PKG_CONFIG='pkg-config'

What did you do?

I'm not sure if I should file this as a bug or a change request.

The http.Client does not (always) forward cookies set during redirects. If there is a chain of redirects, the server can sent cookies during any of those. If I switch domains in a redirect chain, e.g.

  1. http://127.0.0.1/first -> response sets cookie
  2. http://127.0.1.1/second
  3. http://127.0.0.1/third -> does not receive the cookie
    then the cookie won't be forwarded from response 1 to request 3.

This behaviour differs from Chrome, Firefox and curl (with --cookie-jar <file>), who all forward the cookie from response 1 to request 3.

The Go net/http docs say something about this case, but I can't wrap my head around it. Might not work as expected when going back and forth between domains.

when forwarding the "Cookie" header with a non-nil cookie Jar. Since each redirect may mutate the state of the cookie jar, a redirect may possibly alter a cookie set in the initial request. When forwarding the "Cookie" header, any mutated cookies will be omitted, with the expectation that the Jar will insert those mutated cookies with the updated values (assuming the origin matches). If Jar is nil, the initial cookies are forwarded without change.

jar, _ := cookiejar.New(nil)

initCookie := &http.Cookie{
	Name:  "client-init",
	Value: "hello",
}
initURL, _ := url.Parse("http://localhost:8080/")
jar.SetCookies(initURL, []*http.Cookie{initCookie})

client := &http.Client{Jar: jar}
resp, err := client.Get("http://localhost:8080/first")

#4800 mentions RFC 9110 but I don't see any obvious mandate of either behaviour. I'm not familiar with web RFCs though.

Sample webserver to try out different clients:

package main

import (
	"fmt"
	"log"
	"net/http"
	"os"
)

func printRequestInfo(r *http.Request) {
	fmt.Printf("Request URL: %s; Cookies: %v\n", r.URL.String(), r.Cookies())
}

func main() {
	hostname, err := os.Hostname()
	if err != nil {
		log.Fatalf("Error getting hostname: %v", err)
	}

	first := func(w http.ResponseWriter, r *http.Request) {
		printRequestInfo(r)

		cookie := &http.Cookie{
			Name:   "server-test",
			Value:  "set",
			SameSite: http.SameSiteStrictMode,
			Path:   "/",
			Secure: true,
			HttpOnly: true,
		}
		http.SetCookie(w, cookie)
		fmt.Printf("Setting cookie: %s=%s\n", cookie.Name, cookie.Value)

		url := "http://" + hostname + ":8080/second"
		http.Redirect(w, r, url, http.StatusFound)
	}

	second := func(w http.ResponseWriter, r *http.Request) {
		printRequestInfo(r)
		url := "http://localhost:8080/third"
		http.Redirect(w, r, url, http.StatusFound)
	}

	third := func(w http.ResponseWriter, r *http.Request) {
		printRequestInfo(r)
		w.WriteHeader(http.StatusOK)
	}

	http.HandleFunc("/first", first)
	http.HandleFunc("/second", second)
	http.HandleFunc("/third", third)

	listenAddr := "0.0.0.0:8080"
	fmt.Printf("Listening on %s\n", listenAddr)
	if err := http.ListenAndServe(listenAddr, nil); err != nil {
		log.Fatal(err)
	}
}

What did you see happen?

Server log of Go client:

Request URL: /first; Cookies: [client-init=hello]
Setting cookie: server-test=set
Request URL: /second; Cookies: []
Request URL: /third; Cookies: [client-init=hello]

What did you expect to see?

Chrome:

Request URL: /first; Cookies: []
Setting cookie: server-test=set
Request URL: /second; Cookies: []
Request URL: /third; Cookies: [server-test=set]

curl --location --cookie-jar ./jar localhost:8080/first:

Request URL: /first; Cookies: []
Setting cookie: server-test=set
Request URL: /second; Cookies: []
Request URL: /third; Cookies: [server-test=set]

Metadata

Metadata

Assignees

No one assigned

    Labels

    BugReportIssues describing a possible bug in the Go implementation.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions