From a1b6d12d7250c0d51df6be6c2b04215d18eb4d01 Mon Sep 17 00:00:00 2001 From: Mootjelh <134108676+Mootjelh@users.noreply.github.com> Date: Mon, 14 Sep 2026 00:46:39 +0200 Subject: [PATCH] fix(http3): run init on the AddConn path AddConn is the second way into getClient, and only roundTripOpt ran init. A transport whose first use is AddConn dialed with a nil QUICConfig, a nil quic.Transport and a nil newClientConn, and the dial runs in its own goroutine, so the nil transport took the process down with it. Fixes #457 --- internal/http3/transport.go | 8 +++++++ internal/http3/transport_test.go | 37 ++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/internal/http3/transport.go b/internal/http3/transport.go index ddb7c6ed..b4cb114b 100644 --- a/internal/http3/transport.go +++ b/internal/http3/transport.go @@ -315,6 +315,14 @@ func (t *Transport) AddConn(ctx context.Context, addr string) error { } func (t *Transport) getClient(ctx context.Context, hostname string, onlyCached bool) (rtc *roundTripperWithCount, isReused bool, err error) { + // AddConn reaches here without going through roundTripOpt, so init has to + // run here as well. Outside the mutex, like roundTripOpt does it, because + // init opens a UDP socket. + t.initOnce.Do(func() { t.initErr = t.init() }) + if t.initErr != nil { + return nil, false, t.initErr + } + t.mutex.Lock() defer t.mutex.Unlock() if t.closed { diff --git a/internal/http3/transport_test.go b/internal/http3/transport_test.go index 9896655a..4846a007 100644 --- a/internal/http3/transport_test.go +++ b/internal/http3/transport_test.go @@ -3,6 +3,7 @@ package http3 import ( "context" "crypto/tls" + "errors" "net/http" "testing" "time" @@ -139,3 +140,39 @@ func TestNewClientConnAndRawClientConn(t *testing.T) { t.Log("Successfully created ClientConn and RawClientConn") } + +// AddConn is the second way into getClient, beside roundTripOpt, and only +// roundTripOpt ran init. A transport whose first use is AddConn therefore +// dialed with a nil QUICConfig, a nil quic.Transport and a nil newClientConn, +// and the dial runs in its own goroutine, so the nil transport took the +// process down with it. +func TestAddConnInitializesTheTransport(t *testing.T) { + dialed := make(chan *quic.Config, 1) + tr := &Transport{ + Dial: func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (*quic.Conn, error) { + dialed <- cfg + return nil, errors.New("this test does not dial") + }, + } + + if err := tr.AddConn(context.Background(), "127.0.0.1:443"); err != nil { + t.Fatalf("AddConn: %v", err) + } + + var cfg *quic.Config + select { + case cfg = <-dialed: + case <-time.After(10 * time.Second): + t.Fatal("the dial never ran") + } + + if cfg == nil { + t.Fatal("dialed with a nil quic.Config, so init did not run before AddConn dialed") + } + if cfg.MaxIncomingStreams != -1 { + t.Errorf("MaxIncomingStreams is %d, want -1", cfg.MaxIncomingStreams) + } + if len(cfg.Versions) != 1 { + t.Errorf("Versions is %v, want exactly one", cfg.Versions) + } +}