@@ -50,6 +50,8 @@ const (
5050 resolverRetryCount = 3
5151 resolverRetryInitialBackoff = 150 * time .Millisecond
5252 resolverRetryMaxBackoff = 1 * time .Second
53+
54+ DefaultHTTP3NegativeCacheTTL = 30 * time .Minute
5355)
5456
5557type CachedIPItem struct {
@@ -63,9 +65,18 @@ type CachedIPs struct {
6365 cache map [string ]* CachedIPItem
6466}
6567
68+ // AltSupportEntry records what we know about a host's HTTP/3 support.
69+ // A positive port means HTTP/3 works on that port. A zero port means HTTP/3 is
70+ // suppressed: temporarily until negativeUntil when that is set, or permanently
71+ // (an explicit Alt-Svc: clear) when negativeUntil is the zero value.
72+ type AltSupportEntry struct {
73+ port uint16
74+ negativeUntil time.Time
75+ }
76+
6677type AltSupport struct {
6778 sync.RWMutex
68- cache map [string ]uint16
79+ cache map [string ]AltSupportEntry
6980}
7081
7182type XTransport struct {
@@ -98,7 +109,7 @@ func NewXTransport() *XTransport {
98109 }
99110 xTransport := XTransport {
100111 cachedIPs : CachedIPs {cache : make (map [string ]* CachedIPItem )},
101- altSupport : AltSupport {cache : make (map [string ]uint16 )},
112+ altSupport : AltSupport {cache : make (map [string ]AltSupportEntry )},
102113 keepAlive : DefaultKeepAlive ,
103114 timeout : DefaultTimeout ,
104115 bootstrapResolvers : []string {DefaultBootstrapResolver },
@@ -114,6 +125,54 @@ func NewXTransport() *XTransport {
114125 return & xTransport
115126}
116127
128+ // loadAltSupport reports the cached HTTP/3 state for a host. An expired
129+ // temporary negative entry is reported as absent (found=false) so that HTTP/3
130+ // can be probed again and a fresh Alt-Svc header can be parsed.
131+ func (xTransport * XTransport ) loadAltSupport (host string ) (port uint16 , found bool , negative bool ) {
132+ xTransport .altSupport .RLock ()
133+ entry , found := xTransport .altSupport .cache [host ]
134+ xTransport .altSupport .RUnlock ()
135+ if ! found {
136+ return 0 , false , false
137+ }
138+ if entry .port > 0 {
139+ return entry .port , true , false
140+ }
141+ if entry .negativeUntil .IsZero () || time .Now ().Before (entry .negativeUntil ) {
142+ return 0 , true , true
143+ }
144+ return 0 , false , false
145+ }
146+
147+ // saveAltSupport stores a host's HTTP/3 state with no expiry. A positive port
148+ // records HTTP/3 support; a zero port records an explicit, permanent negative,
149+ // used for Alt-Svc: clear and for hosts that turned out not to support HTTP/3.
150+ func (xTransport * XTransport ) saveAltSupport (host string , port uint16 ) {
151+ xTransport .altSupport .Lock ()
152+ xTransport .altSupport .cache [host ] = AltSupportEntry {port : port }
153+ xTransport .altSupport .Unlock ()
154+ }
155+
156+ // saveAltSupportNegative suppresses HTTP/3 for a host for the given duration,
157+ // after which it is retried.
158+ func (xTransport * XTransport ) saveAltSupportNegative (host string , ttl time.Duration ) {
159+ xTransport .altSupport .Lock ()
160+ xTransport .altSupport .cache [host ] = AltSupportEntry {negativeUntil : time .Now ().Add (ttl )}
161+ xTransport .altSupport .Unlock ()
162+ }
163+
164+ // recordHTTP3ProbeFailure caches the outcome of a failed HTTP/3 probe once the
165+ // HTTP/2 fallback has revealed the server's Alt-Svc header. A host that still
166+ // advertises h3 is suppressed only temporarily, since the probe failure was
167+ // likely transient; any other host is suppressed permanently.
168+ func (xTransport * XTransport ) recordHTTP3ProbeFailure (host string , advertisesH3 bool ) {
169+ if advertisesH3 {
170+ xTransport .saveAltSupportNegative (host , DefaultHTTP3NegativeCacheTTL )
171+ } else {
172+ xTransport .saveAltSupport (host , 0 )
173+ }
174+ }
175+
117176func ParseIP (ipStr string ) net.IP {
118177 return net .ParseIP (strings .TrimRight (strings .TrimLeft (ipStr , "[" ), "]" ))
119178}
@@ -660,30 +719,24 @@ func (xTransport *XTransport) Fetch(
660719 }
661720 host , port := ExtractHostAndPort (url .Host , 443 )
662721 hasAltSupport := false
722+ http3Suppressed := false
663723
664724 if xTransport .h3Transport != nil {
725+ altPort , found , negative := xTransport .loadAltSupport (url .Host )
726+ http3Suppressed = negative
665727 if xTransport .http3Probe {
666- xTransport .altSupport .RLock ()
667- altPort , inNegativeCache := xTransport .altSupport .cache [url .Host ]
668- inNegativeCache = inNegativeCache && altPort == 0
669- xTransport .altSupport .RUnlock ()
670- if ! inNegativeCache {
728+ if ! negative {
671729 client .Transport = xTransport .h3Transport
672730 dlog .Debugf ("Probing HTTP/3 transport for [%s]" , url .Host )
673731 } else {
674732 dlog .Debugf ("Skipping HTTP/3 probe for [%s] - previously failed" , url .Host )
675733 }
676734 } else {
677735 // Otherwise use traditional Alt-Svc detection
678- xTransport .altSupport .RLock ()
679- var altPort uint16
680- altPort , hasAltSupport = xTransport .altSupport .cache [url .Host ]
681- xTransport .altSupport .RUnlock ()
682- if hasAltSupport && altPort > 0 { // altPort > 0 ensures we're not in the negative cache
683- if int (altPort ) == port {
684- client .Transport = xTransport .h3Transport
685- dlog .Debugf ("Using HTTP/3 transport for [%s]" , url .Host )
686- }
736+ hasAltSupport = found && ! negative
737+ if hasAltSupport && altPort > 0 && int (altPort ) == port {
738+ client .Transport = xTransport .h3Transport
739+ dlog .Debugf ("Using HTTP/3 transport for [%s]" , url .Host )
687740 }
688741 }
689742 }
@@ -731,18 +784,21 @@ func (xTransport *XTransport) Fetch(
731784 rtt := time .Since (start )
732785
733786 // Handle HTTP/3 error case - fallback to HTTP/2 when HTTP/3 fails
787+ h3ProbeFailed := false
734788 if err != nil && client .Transport == xTransport .h3Transport {
735789 if xTransport .http3Probe {
790+ // A probe is only an optimistic guess at HTTP/3 support, so the
791+ // negative-cache decision is deferred until the HTTP/2 fallback below
792+ // lets us inspect the server's Alt-Svc header.
736793 dlog .Debugf ("HTTP/3 probe failed for [%s]: [%s] - falling back to HTTP/2" , url .Host , err )
794+ h3ProbeFailed = true
737795 } else {
796+ // Here the host explicitly advertised Alt-Svc: h3, so the failure is
797+ // more likely transient. Suppress HTTP/3 only until the TTL expires.
738798 dlog .Debugf ("HTTP/3 connection failed for [%s]: [%s] - falling back to HTTP/2" , url .Host , err )
799+ xTransport .saveAltSupportNegative (url .Host , DefaultHTTP3NegativeCacheTTL )
739800 }
740801
741- // Add server to negative cache when HTTP/3 fails
742- xTransport .altSupport .Lock ()
743- xTransport .altSupport .cache [url .Host ] = 0 // 0 port means HTTP/3 failed and should not be tried again
744- xTransport .altSupport .Unlock ()
745-
746802 // Retry with HTTP/2
747803 client .Transport = xTransport .transport
748804 if body != nil {
@@ -773,27 +829,24 @@ func (xTransport *XTransport) Fetch(
773829 return nil , statusCode , nil , rtt , err
774830 }
775831 if xTransport .h3Transport != nil && ! hasAltSupport {
776- // Check if there's entry in negative cache when using http3_probe
777- skipAltSvcParsing := false
778- if xTransport .http3Probe {
779- xTransport .altSupport .RLock ()
780- altPort , inCache := xTransport .altSupport .cache [url .Host ]
781- xTransport .altSupport .RUnlock ()
782- // If server is in negative cache (altPort == 0), don't attempt to parse Alt-Svc header
783- if inCache && altPort == 0 {
784- dlog .Debugf ("Skipping Alt-Svc parsing for [%s] - previously failed HTTP/3 probe" , url .Host )
785- skipAltSvcParsing = true
786- }
832+ // In probe mode nothing rewrites the entry between the negative-cache read
833+ // above and here, so reuse it rather than locking and looking it up again.
834+ skipAltSvcParsing := xTransport .http3Probe && http3Suppressed
835+ if skipAltSvcParsing {
836+ dlog .Debugf ("Skipping Alt-Svc parsing for [%s] - previously failed HTTP/3 probe" , url .Host )
787837 }
788838
789839 if ! skipAltSvcParsing {
790- if alt , found := resp .Header ["Alt-Svc" ]; found {
840+ alt , found := resp .Header ["Alt-Svc" ]
841+ altPort := uint16 (port & 0xffff )
842+ advertisesH3 := false
843+ if found {
791844 dlog .Debugf ("Alt-Svc [%s]: [%s]" , url .Host , alt )
792- altPort := uint16 (port & 0xffff )
793845 for i , xalt := range alt {
794846 if strings .TrimSpace (xalt ) == "clear" {
795847 dlog .Debugf ("Alt-Svc clear for [%s] - HTTP/3 not available" , url .Host )
796848 altPort = 0
849+ advertisesH3 = false
797850 break
798851 }
799852 for j , v := range strings .Split (xalt , ";" ) {
@@ -806,16 +859,20 @@ func (xTransport *XTransport) Fetch(
806859 v = strings .TrimSuffix (v , "\" " )
807860 if xAltPort , err := strconv .ParseUint (v , 10 , 16 ); err == nil && xAltPort <= 65535 {
808861 altPort = uint16 (xAltPort )
862+ advertisesH3 = true
809863 dlog .Debugf ("Using HTTP/3 for [%s]" , url .Host )
810864 break
811865 }
812866 }
813867 }
814868 }
815- xTransport .altSupport .Lock ()
816- xTransport .altSupport .cache [url .Host ] = altPort
869+ }
870+ switch {
871+ case h3ProbeFailed :
872+ xTransport .recordHTTP3ProbeFailure (url .Host , advertisesH3 )
873+ case found :
874+ xTransport .saveAltSupport (url .Host , altPort )
817875 dlog .Debugf ("Caching altPort for [%v]" , url .Host )
818- xTransport .altSupport .Unlock ()
819876 }
820877 }
821878 }
0 commit comments