Skip to content

net/http: http2 server use with synctest sometimes causes fatal error "select on synctest channel from outside bubble" #75674

@codekitchen

Description

@codekitchen

Go version

go version go1.25.1 darwin/arm64

Output of go env in your module/workspace:

AR='ar'
CC='clang'
CGO_CFLAGS='-O2 -g'
CGO_CPPFLAGS=''
CGO_CXXFLAGS='-O2 -g'
CGO_ENABLED='1'
CGO_FFLAGS='-O2 -g'
CGO_LDFLAGS='-O2 -g'
CXX='clang++'
GCCGO='gccgo'
GO111MODULE=''
GOARCH='arm64'
GOARM64='v8.0'
GOAUTH='netrc'
GOBIN=''
GOCACHE='/Users/brianp/Library/Caches/go-build'
GOCACHEPROG=''
GODEBUG=''
GOENV='/Users/brianp/Library/Application Support/go/env'
GOEXE=''
GOEXPERIMENT=''
GOFIPS140='off'
GOFLAGS=''
GOGCCFLAGS='-fPIC -arch arm64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -ffile-prefix-map=/var/folders/cy/xf7m8_557hs34244c8cbxvqh0000gn/T/go-build2569017976=/tmp/go-build -gno-record-gcc-switches -fno-common'
GOHOSTARCH='arm64'
GOHOSTOS='darwin'
GOINSECURE=''
GOMOD='/dev/null'
GOMODCACHE='/Users/brianp/go/pkg/mod'
GONOPROXY=''
GONOSUMDB=''
GOOS='darwin'
GOPATH='/Users/brianp/go'
GOPRIVATE=''
GOPROXY='https://proxy.golang.org,direct'
GOROOT='/Users/brianp/.cache/tsgo/aa85d1541af0921f830f053f29d91971fa5838f6'
GOSUMDB='sum.golang.org'
GOTELEMETRY='local'
GOTELEMETRYDIR='/Users/brianp/Library/Application Support/go/telemetry'
GOTMPDIR=''
GOTOOLCHAIN='auto'
GOTOOLDIR='/Users/brianp/.cache/tsgo/aa85d1541af0921f830f053f29d91971fa5838f6/pkg/tool/darwin_arm64'
GOVCS=''
GOVERSION='go1.25.1'
GOWORK=''
PKG_CONFIG='pkg-config'

What did you do?

We are adopting synctest for some tests involving http2 servers using an in-memory network, which has been great. But sometimes the tests fail with fatal error: select on synctest channel from outside bubble, especially when run in parallel. I have a repro that fails pretty consistently when I run it with go test -count=50:

package main

import (
	"net/http"
	"net/http/httptest"
	"testing"
	"testing/synctest"

	"tailscale.com/net/memnet"
)

func TestHTTP2Synctest(t *testing.T) {
	synctest.Test(t, func(t *testing.T) {
		nw := new(memnet.Network)
		s := httptest.NewUnstartedServer(http.Handler(http.NotFoundHandler()))
		s.EnableHTTP2 = true
		s.Listener, _ = nw.Listen("tcp", "127.0.0.1:0")
		s.StartTLS()
		t.Cleanup(s.Close)
		c := s.Client()
		c.Transport.(*http.Transport).DialContext = nw.Dial
		if resp, err := c.Get(s.URL); err != nil {
			t.Fatal(err)
		} else if resp.StatusCode != 404 {
			t.Errorf("got status %v; want 404", resp.StatusCode)
		}
	})
}

What did you see happen?

Usually when run with go test -count=50, the test fails with the below. This seems likely to be caused by the global http2errChanPool inside net/http. I see a recent attempt to work around this issue here and here. However, that http2inTests flag is only available from within the net/http package. We probably need a more general solution for application tests. Possibly each server should have its own pool?

fatal error: select on synctest channel from outside bubble

goroutine 103 [running, synctest bubble 4]:
net/http.(*http2serverConn).writeHeaders(0x14000176f00, 0x1400012e420, 0x1400013f490)
        /Users/brianp/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.25.1.darwin-arm64/src/net/http/h2_bundle.go:6420 +0x108
net/http.(*http2responseWriterState).writeChunk(0x14000034480, {0x14000244000, 0x13, 0x1000})
        /Users/brianp/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.25.1.darwin-arm64/src/net/http/h2_bundle.go:6679 +0x714
net/http.http2chunkWriter.Write({0x0?}, {0x14000244000?, 0x0?, 0x140001469c0?})
        /Users/brianp/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.25.1.darwin-arm64/src/net/http/h2_bundle.go:6581 +0x24
bufio.(*Writer).Flush(0x14000011100)
        /Users/brianp/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.25.1.darwin-arm64/src/bufio/bufio.go:645 +0x58
net/http.(*http2responseWriter).FlushError(0x10241e3b8?)
        /Users/brianp/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.25.1.darwin-arm64/src/net/http/h2_bundle.go:6846 +0x38
net/http.(*http2responseWriter).Flush(...)
        /Users/brianp/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.25.1.darwin-arm64/src/net/http/h2_bundle.go:6836
net/http.(*http2responseWriter).handlerDone(0x1400006e178)
        /Users/brianp/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.25.1.darwin-arm64/src/net/http/h2_bundle.go:7011 +0x34
net/http.(*http2serverConn).runHandler.func1()
        /Users/brianp/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.25.1.darwin-arm64/src/net/http/h2_bundle.go:6384 +0x1b4
net/http.(*http2serverConn).runHandler(0x1400011a9c0?, 0x102421970?, 0x140003161e0?, 0x0?)
        /Users/brianp/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.25.1.darwin-arm64/src/net/http/h2_bundle.go:6388 +0xf4
created by net/http.(*http2serverConn).scheduleHandler in goroutine 98
        /Users/brianp/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.25.1.darwin-arm64/src/net/http/h2_bundle.go:6320 +0x1dc
...

What did you expect to see?

This test should consistently pass.

Metadata

Metadata

Assignees

No one assigned

    Labels

    NeedsInvestigationSomeone must examine and confirm this is a valid issue and not a duplicate of an existing one.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions