@@ -78,114 +78,102 @@ func TCPProbe(config TCPProbeConfigOptions) error {
7878 return nil
7979}
8080
81- // Returns a transport that uses HTTP/2 if it's known to be supported, and otherwise
82- // spoofs the request & response versions to HTTP/1.1.
83- func autoDowngradingTransport (opt HTTPProbeConfigOptions ) http.RoundTripper {
84- t := pkgnet .NewProberTransport ()
81+ // proberTransport is a reusable transport optimized for health check probes.
82+ // The transport auto-selects between HTTP/1.1 and H2C based on the request's ProtoMajor field.
83+ var proberTransport = pkgnet .NewProberTransport ()
84+
85+ // cleartextProbeTransport returns a RoundTripper that wraps the prober transport
86+ // and sets the appropriate protocol hint for the given protocol version.
87+ // protoMajor should be 1 for HTTP/1.1 or 2 for H2C.
88+ func cleartextProbeTransport (protoMajor int ) http.RoundTripper {
8589 return pkgnet .RoundTripperFunc (func (r * http.Request ) (* http.Response , error ) {
86- // If the user-container can handle HTTP2, we pass through the request as-is.
87- // We have to set r.ProtoMajor to 2, since auto transport relies solely on it
88- // to decide whether to use h2c or http1.1.
89- if opt .MaxProtoMajor == 2 {
90- r .ProtoMajor = 2
91- return t .RoundTrip (r )
92- }
93-
94- // Otherwise, save the request HTTP version and downgrade it
95- // to HTTP1 before sending.
96- version := r .ProtoMajor
97- r .ProtoMajor = 1
98- resp , err := t .RoundTrip (r )
99-
100- // Restore the request & response HTTP version before sending back.
101- r .ProtoMajor = version
102- if resp != nil {
103- resp .ProtoMajor = version
104- }
105- return resp , err
90+ // Set the protocol hint for the auto-selecting prober transport
91+ r .ProtoMajor = protoMajor
92+ return proberTransport .RoundTrip (r )
10693 })
10794}
10895
109- var transport = func () * http.Transport {
110- t := http .DefaultTransport .(* http.Transport ).Clone ()
111- t .TLSClientConfig .InsecureSkipVerify = true
112- return t
113- }()
114-
11596func getURL (config HTTPProbeConfigOptions ) (* url.URL , error ) {
11697 return url .Parse (string (config .Scheme ) + "://" + net .JoinHostPort (config .Host , config .Port .String ()) + "/" + strings .TrimPrefix (config .Path , "/" ))
11798}
11899
119- // http2UpgradeProbe checks that an HTTP with HTTP2 upgrade request
120- // connection can be understood by the address.
121- // Returns: the highest known proto version supported (0 if not ready or error)
122- func http2UpgradeProbe (config HTTPProbeConfigOptions ) (int , error ) {
100+ // detectHTTPProtocolVersion detects the highest HTTP protocol version supported by the server.
101+ // Attempts H2C first, falls back to HTTP/1 if that fails or returns non-ready status.
102+ // Returns 2 (H2C ready), 1 (HTTP/1 ready), or 0 (not ready/error).
103+ func detectHTTPProtocolVersion (config HTTPProbeConfigOptions ) (int , error ) {
104+ // http.Client does not fallback to from h2c to http1, we need to make two requests ourselves
123105 httpClient := & http.Client {
124- Transport : transport ,
125106 Timeout : config .Timeout ,
107+ Transport : cleartextProbeTransport (2 ),
126108 }
109+
127110 url , err := getURL (config )
128111 if err != nil {
129112 return 0 , fmt .Errorf ("error constructing probe url %w" , err )
130113 }
114+
115+ // do a simple GET request as Kubernetes does, avoid non-standard methods like HEAD
131116 //nolint:noctx // timeout is specified on the http.Client above
132- req , err := http .NewRequest (http .MethodOptions , url .String (), nil )
117+ req , err := http .NewRequest (http .MethodGet , url .String (), nil )
133118 if err != nil {
134119 return 0 , fmt .Errorf ("error constructing probe request %w" , err )
135120 }
121+ req .Header .Add (netheader .UserAgentKey , netheader .KubeProbeUAPrefix + config .KubeMajor + "/" + config .KubeMinor )
136122
137- // An upgrade will need to have at least these 3 headers.
138- // This is documented at https://tools.ietf.org/html/rfc7540#section-3.2
139- req .Header .Add ("Connection" , "Upgrade, HTTP2-Settings" )
140- req .Header .Add ("Upgrade" , "h2c" )
141- req .Header .Add ("HTTP2-Settings" , "" )
123+ if res , err := httpClient .Do (req ); err == nil {
124+ defer res .Body .Close ()
142125
143- req .Header .Add (netheader .UserAgentKey , netheader .KubeProbeUAPrefix + config .KubeMajor + "/" + config .KubeMinor )
126+ // ignore non-ready http2 responses and continue with http1, http2 might not be properly supported
127+ if isHTTPProbeReady (res ) {
128+ return 2 , nil
129+ }
130+ }
144131
132+ // fallback to check http1
133+ httpClient .Transport = cleartextProbeTransport (1 )
145134 res , err := httpClient .Do (req )
146135 if err != nil {
147136 return 0 , err
148137 }
149- defer res .Body .Close ()
150138
151- maxProto := 0
139+ defer res . Body . Close ()
152140
153- if isHTTPProbeUpgradingToH2C (res ) {
154- maxProto = 2
155- } else if isHTTPProbeReady (res ) {
156- maxProto = 1
157- } else {
158- return maxProto , fmt .Errorf ("HTTP probe did not respond Ready, got status code: %d" , res .StatusCode )
141+ if isHTTPProbeReady (res ) {
142+ return 1 , nil
159143 }
160144
161- return maxProto , nil
145+ return 0 , fmt . Errorf ( "HTTP probe did not respond Ready, got status code: %d" , res . StatusCode )
162146}
163147
164148// HTTPProbe checks that HTTP connection can be established to the address.
165149func HTTPProbe (config HTTPProbeConfigOptions ) error {
166150 if config .MaxProtoMajor == 0 {
167151 // If we don't know if the connection supports HTTP2, we will try it.
168- // Once we get a non-error response, we won't try again.
152+ // NOTE: the result is not cached right now, every probe attempts http2 detection again
153+
169154 // If maxProto is 0, container is not ready, so we don't know whether http2 is supported.
170155 // If maxProto is 1, we know we're ready, but we also can't upgrade, so just return.
171156 // If maxProto is 2, we know we can upgrade to http2
172- maxProto , err := http2UpgradeProbe (config )
157+ maxProto , err := detectHTTPProtocolVersion (config )
173158 if err != nil {
174- return fmt .Errorf ("failed to run HTTP2 upgrade probe with error: %w" , err )
159+ return fmt .Errorf ("failed to run HTTP protocol probe with error: %w" , err )
175160 }
176161 config .MaxProtoMajor = maxProto
177162 if config .MaxProtoMajor == 1 {
163+ // probe already passed for HTTP/1.1 during auto detection, return early
178164 return nil
179165 }
180166 }
167+
181168 httpClient := & http.Client {
182- Transport : autoDowngradingTransport (config ),
169+ Transport : cleartextProbeTransport (config . MaxProtoMajor ),
183170 Timeout : config .Timeout ,
184171 }
185172 url , err := getURL (config )
186173 if err != nil {
187174 return fmt .Errorf ("error constructing probe url %w" , err )
188175 }
176+
189177 //nolint:noctx // timeout is specified on the http.Client above
190178 req , err := http .NewRequest (http .MethodGet , url .String (), nil )
191179 if err != nil {
@@ -217,11 +205,6 @@ func HTTPProbe(config HTTPProbeConfigOptions) error {
217205 return nil
218206}
219207
220- // isHTTPProbeUpgradingToH2C checks whether the server indicates it's switching to h2c protocol.
221- func isHTTPProbeUpgradingToH2C (res * http.Response ) bool {
222- return res .StatusCode == http .StatusSwitchingProtocols && res .Header .Get ("Upgrade" ) == "h2c"
223- }
224-
225208// isHTTPProbeReady checks whether we received a successful Response
226209func isHTTPProbeReady (res * http.Response ) bool {
227210 // response status code between 200-399 indicates success
0 commit comments