k8s.io/apiserver@v0.31.1/pkg/server/read_write_deadline_test.go (about)

     1  /*
     2  Copyright 2024 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package server
    18  
    19  import (
    20  	"context"
    21  	"errors"
    22  	"fmt"
    23  	"io"
    24  	"net/http"
    25  	"net/http/httptest"
    26  	"strings"
    27  	"testing"
    28  	"time"
    29  
    30  	"k8s.io/apimachinery/pkg/util/wait"
    31  	"k8s.io/apiserver/pkg/endpoints/request"
    32  )
    33  
    34  func TestRequestTimeoutBehavior(t *testing.T) {
    35  	type setup struct {
    36  		name                        string
    37  		clientTimeout               time.Duration
    38  		serverReqTimeout            time.Duration
    39  		handlerWritesBeforeTimeout  bool
    40  		handlerFlushesBeforeTimeout bool
    41  		waiter                      waiter
    42  	}
    43  	type expectation struct {
    44  		clientErr                verifier
    45  		clientStatusCodeExpected int
    46  		clientRespBodyReadErr    verifier
    47  		handlerWriteErr          verifier
    48  	}
    49  
    50  	tests := []struct {
    51  		setup        setup
    52  		expectations map[string]expectation
    53  	}{
    54  		// scenario:
    55  		//   a) timeout filter enabled: Yes
    56  		//   b) client specifies timeout in the request URI: No
    57  		//   c) the handler writes to the ResponseWriter object before request times out: No
    58  		//   d) the handler flushes the ResponseWriter object before request times out: No
    59  		// observation:
    60  		//  the timeout filter detects that the context of the request has exceeded its
    61  		//  deadline, since the ResponseWriter object has not been written to yet,
    62  		//  the following takes place:
    63  		//   - it marks the ResponseWriter object as timeout=true, so any further
    64  		//   attempt to write to it will yield an 'http: Handler timeout' error
    65  		//   - it sends 504 status code to the client
    66  		// expectation (same behavior for both http/1x and http/2.0):
    67  		//  client:
    68  		//   - client receives a '504 GatewayTimeout' status code
    69  		//   - reading the Body of the Response object yields an error
    70  		//  server:
    71  		//   - Write to the ResponseWriter yields an 'http: Handler timeout'
    72  		//     error immediately.
    73  		{
    74  			setup: setup{
    75  				name:                        "timeout occurs before the handler writes to or flushes the ResponseWriter",
    76  				clientTimeout:               0,     // b
    77  				handlerWritesBeforeTimeout:  false, // c
    78  				handlerFlushesBeforeTimeout: false, // d
    79  				serverReqTimeout:            time.Second,
    80  			},
    81  			expectations: map[string]expectation{
    82  				"HTTP/2.0": {
    83  					clientErr:                wantNoError{},
    84  					clientStatusCodeExpected: http.StatusGatewayTimeout,
    85  					clientRespBodyReadErr:    wantNoError{},
    86  					handlerWriteErr:          wantError{http.ErrHandlerTimeout},
    87  				},
    88  				"HTTP/1.1": {
    89  					clientErr:                wantNoError{},
    90  					clientStatusCodeExpected: http.StatusGatewayTimeout,
    91  					clientRespBodyReadErr:    wantNoError{},
    92  					handlerWriteErr:          wantError{http.ErrHandlerTimeout},
    93  				},
    94  			},
    95  		},
    96  
    97  		// scenario:
    98  		//   a) timeout filter enabled: Yes
    99  		//   b) client specifies timeout in the request URI: No
   100  		//   c) the handler writes to the ResponseWriter object before request times out: Yes
   101  		//   d) the handler flushes the ResponseWriter object before request times out: No
   102  		// observation:
   103  		//  the timeout filter detects that the context of the request has exceeded its
   104  		//  deadline, since the ResponseWriter object has already been written to,
   105  		//  the following takes place:
   106  		//   - it marks the ResponseWriter object as timeout=true, so any further attempt
   107  		//   to write to it will yield an 'http: Handler timeout' error
   108  		//   - it can't send '504 GatewayTimeout' to the client since the ResponseWriter
   109  		//   object has already been written to, so it panics with 'net/http: abort Handler' error
   110  		{
   111  			setup: setup{
   112  				name:                        "timeout occurs after the handler writes to the ResponseWriter",
   113  				clientTimeout:               0,     // b
   114  				handlerWritesBeforeTimeout:  true,  // c
   115  				handlerFlushesBeforeTimeout: false, // d
   116  				serverReqTimeout:            time.Second,
   117  			},
   118  			expectations: map[string]expectation{
   119  				// expectation:
   120  				//  - client: receives a stream reset error, no 'Response' from the server
   121  				//  - server: Write to the ResponseWriter yields an 'http: Handler timeout' error
   122  				"HTTP/2.0": {
   123  					clientErr:                wantContains{"stream error: stream ID 1; INTERNAL_ERROR; received from peer"},
   124  					clientStatusCodeExpected: 0,
   125  					clientRespBodyReadErr:    wantNoError{},
   126  					handlerWriteErr:          wantError{http.ErrHandlerTimeout},
   127  				},
   128  				// expectation:
   129  				//  - client: receives an 'io.EOF' error, no 'Response' from the server
   130  				//  - server: Write to the ResponseWriter yields an 'http: Handler timeout' error
   131  				"HTTP/1.1": {
   132  					clientErr:                wantError{io.EOF},
   133  					clientStatusCodeExpected: 0,
   134  					clientRespBodyReadErr:    wantNoError{},
   135  					handlerWriteErr:          wantError{http.ErrHandlerTimeout},
   136  				},
   137  			},
   138  		},
   139  
   140  		// scenario:
   141  		//   a) timeout filter enabled: Yes
   142  		//   b) client specifies timeout in the request URI: No
   143  		//   c) the handler writes to the ResponseWriter object before request times out: Yes
   144  		//   d) the handler flushes the ResponseWriter object before request times out: Yes
   145  		// observation:
   146  		//  the timeout filter detects that the context of the request has exceeded its
   147  		//  deadline, since the ResponseWriter object has already been written to,
   148  		//  the following takes place:
   149  		//   - it marks the ResponseWriter object as timeout=true, so any further attempt
   150  		//   to write to it will yield an 'http: Handler timeout' error
   151  		//   - it can't send '504 GatewayTimeout' to the client since the ResponseWriter
   152  		//   object has already been written to, so it panics with 'net/http: abort Handler' error
   153  		{
   154  			setup: setup{
   155  				name:                        "timeout occurs after the handler writes to and flushes the ResponseWriter",
   156  				clientTimeout:               0,    // b
   157  				handlerWritesBeforeTimeout:  true, // c
   158  				handlerFlushesBeforeTimeout: true, // d
   159  				serverReqTimeout:            time.Second,
   160  			},
   161  			expectations: map[string]expectation{
   162  				// expectation:
   163  				//  - client: since the ResponseWriter has been flushed the client
   164  				//  receives a response from the server, but reading the response body
   165  				//  is expected to yield a stream reset error.
   166  				//  - server: Write to the ResponseWriter yields an 'http: Handler timeout' error
   167  				"HTTP/2.0": {
   168  					clientErr:                wantNoError{},
   169  					clientStatusCodeExpected: 200,
   170  					clientRespBodyReadErr:    wantContains{"stream error: stream ID 1; INTERNAL_ERROR; received from peer"},
   171  					handlerWriteErr:          wantError{http.ErrHandlerTimeout},
   172  				},
   173  				// expectation:
   174  				//  - client: since the ResponseWriter has been flushed the client
   175  				//  receives a response from the server, but reading the response body
   176  				//  will yield an 'unexpected EOF' error.
   177  				//  - server: Write to the ResponseWriter yields an 'http: Handler timeout' error
   178  				"HTTP/1.1": {
   179  					clientErr:                wantNoError{},
   180  					clientStatusCodeExpected: 200,
   181  					clientRespBodyReadErr:    wantError{io.ErrUnexpectedEOF},
   182  					handlerWriteErr:          wantError{http.ErrHandlerTimeout},
   183  				},
   184  			},
   185  		},
   186  
   187  		// scenario:
   188  		//   a) timeout filter enabled: Yes
   189  		//   b) client specifies timeout in the request URI: Yes
   190  		//   c) the handler writes to the ResponseWriter object before request times out: Yes
   191  		//   d) the handler flushes the ResponseWriter object before request times out: No
   192  		// observation:
   193  		//  the timeout filter detects that the context of the request has exceeded its
   194  		//  deadline, and the the following takes place:
   195  		//   - it marks the ResponseWriter object as timeout=true, so any further attempt
   196  		//   to write to it will yield an 'http: Handler timeout' error
   197  		//   - it can't send '504 GatewayTimeout' to the client since the ResponseWriter
   198  		//   object has already been written to, so it panics with 'net/http: abort Handler' error
   199  		//  at the same time, the net/http client also detects that the context of the
   200  		//  client-side request has exceeded its deadline, and so it aborts with a
   201  		//  'context deadline exceeded' error.
   202  		// NOTE: although the client is most likely to receive the context deadline error
   203  		// first due to the roundtrip time added to the arrival of the error from
   204  		// the server, nevertheless it could cause flakes in CI due to overload, so we
   205  		// need to check for either error to be flake free.
   206  		{
   207  			setup: setup{
   208  				name:                        "client specifies a timeout",
   209  				clientTimeout:               time.Second,                 // b
   210  				handlerWritesBeforeTimeout:  true,                        // c
   211  				handlerFlushesBeforeTimeout: false,                       // d
   212  				serverReqTimeout:            wait.ForeverTestTimeout * 2, // this should not be in effect
   213  
   214  				// twice the request timeout so it can withstand flakes in CI
   215  				waiter: &waitWithDuration{after: 2 * time.Second},
   216  			},
   217  			expectations: map[string]expectation{
   218  				// expectation:
   219  				//  - client: receives either a context.DeadlineExceeded error from its transport
   220  				//    or it receives the error from the server
   221  				//  - server: Write to the ResponseWriter will yields an 'http: Handler timeout'
   222  				"HTTP/2.0": {
   223  					clientErr: wantEitherOr{
   224  						err:      context.DeadlineExceeded,
   225  						contains: "stream error: stream ID 1; INTERNAL_ERROR; received from peer",
   226  					},
   227  					clientStatusCodeExpected: 0,
   228  					clientRespBodyReadErr:    wantNoError{},
   229  					handlerWriteErr:          wantError{http.ErrHandlerTimeout},
   230  				},
   231  				// expectation:
   232  				//  - client: receives either a context.DeadlineExceeded error from its
   233  				//    transport, or it receives the error from the server
   234  				//  - server: Write to the ResponseWriter will yields an 'http: Handler timeout'
   235  				"HTTP/1.1": {
   236  					clientErr: wantEitherOr{
   237  						err:      context.DeadlineExceeded,
   238  						contains: "EOF",
   239  					},
   240  					clientStatusCodeExpected: 0,
   241  					clientRespBodyReadErr:    wantNoError{},
   242  					handlerWriteErr:          wantError{http.ErrHandlerTimeout},
   243  				},
   244  			},
   245  		},
   246  	}
   247  	for _, test := range tests {
   248  		for _, proto := range []string{"HTTP/1.1", "HTTP/2.0"} { // every test is run for both http/1x and http/2.0
   249  			t.Run(fmt.Sprintf("%s/%s", test.setup.name, proto), func(t *testing.T) {
   250  				setup := test.setup
   251  				want, ok := test.expectations[proto]
   252  				if !ok {
   253  					t.Fatalf("wrong test setup - no expectation for %s", proto)
   254  				}
   255  
   256  				fakeAudit := &fakeAudit{}
   257  				config, _ := setUp(t)
   258  				config.AuditPolicyRuleEvaluator = fakeAudit
   259  				config.AuditBackend = fakeAudit
   260  
   261  				// setup server run option --request-timeout
   262  				config.RequestTimeout = setup.serverReqTimeout
   263  
   264  				s, err := config.Complete(nil).New("test", NewEmptyDelegate())
   265  				if err != nil {
   266  					t.Fatalf("Error in setting up a GenericAPIServer object: %v", err)
   267  				}
   268  
   269  				// using this, the handler blocks until the timeout occurs
   270  				waiter := setup.waiter
   271  				if waiter == nil {
   272  					waiter = &waitWithChannelClose{after: make(chan time.Time)}
   273  				}
   274  
   275  				// this is the timeout we expect the context of a request
   276  				// on the server to have.
   277  				//  - if the client does not specify a timeout parameter in
   278  				//  the request URI then it should default to --request-timeout
   279  				//  - otherwise, it should be the timeout specified by the client
   280  				reqCtxTimeoutWant := config.RequestTimeout
   281  				if setup.clientTimeout > 0 {
   282  					reqCtxTimeoutWant = setup.clientTimeout
   283  				}
   284  
   285  				handlerDoneCh := make(chan struct{})
   286  				handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   287  					defer close(handlerDoneCh)
   288  
   289  					ctx := r.Context()
   290  					if r.Proto != proto {
   291  						t.Errorf("expected protocol: %q, but got: %q", proto, r.Proto)
   292  						return
   293  					}
   294  
   295  					// TODO: we don't support `FlushError` yet
   296  					flusher, ok := w.(http.Flusher)
   297  					if !ok {
   298  						t.Errorf("expected ResponseWriter object to implement FlushError")
   299  						return
   300  					}
   301  
   302  					// make sure that we have the right request
   303  					//  - it must be a non long-running request
   304  					//  - it must have a received-at timestamp so we
   305  					//    can calculate the request deadline accurately.
   306  					//  - the context of the request must have the
   307  					//    expected deadline
   308  					reqInfo, ok := request.RequestInfoFrom(ctx)
   309  					if !ok {
   310  						t.Errorf("expected the request context to have a RequestInfo associated")
   311  						return
   312  					}
   313  					if config.LongRunningFunc(r, reqInfo) {
   314  						t.Errorf("wrong test setup, wanted a non long-running request, but got: %#v", reqInfo)
   315  						return
   316  					}
   317  					receivedAt, ok := request.ReceivedTimestampFrom(ctx)
   318  					if !ok {
   319  						t.Errorf("expected the request context to have a received-at timestamp")
   320  						return
   321  					}
   322  					deadline, ok := ctx.Deadline()
   323  					if !ok {
   324  						t.Errorf("expected the request context to have a deadline")
   325  						return
   326  					}
   327  					if want, got := reqCtxTimeoutWant, deadline.Sub(receivedAt); want != got {
   328  						t.Errorf("expected the request context to have a deadline of: %s, but got: %s", want, got)
   329  						return
   330  					}
   331  
   332  					// does the handler write to or flush the
   333  					// ResponseWriter object before timeout occurs?
   334  					if setup.handlerWritesBeforeTimeout {
   335  						if _, err := w.Write([]byte("hello")); err != nil {
   336  							t.Errorf("unexpected error from Write: %v", err)
   337  							return
   338  						}
   339  					}
   340  					if setup.handlerFlushesBeforeTimeout {
   341  						flusher.Flush()
   342  					}
   343  
   344  					// wait for the request context deadline to elapse
   345  					<-waiter.wait()
   346  
   347  					// write to the ResponseWriter object after timeout happens
   348  					_, err := w.Write([]byte("a"))
   349  					want.handlerWriteErr.verify(t, err)
   350  
   351  					// flush the ResponseWriter object after timeout happens
   352  					// http.Flusher does not return an error
   353  					flusher.Flush()
   354  				})
   355  				s.Handler.NonGoRestfulMux.Handle("/ping", handler)
   356  
   357  				server := httptest.NewUnstartedServer(s.Handler)
   358  				defer server.Close()
   359  				if proto == "HTTP/2.0" {
   360  					server.EnableHTTP2 = true
   361  				}
   362  				server.StartTLS()
   363  
   364  				func() {
   365  					defer waiter.close()
   366  
   367  					client := server.Client()
   368  
   369  					url := fmt.Sprintf("%s/ping", server.URL)
   370  					// if the user has specified a timeout then add
   371  					// it to the request URI
   372  					if setup.clientTimeout > 0 {
   373  						url = fmt.Sprintf("%s?timeout=%s", url, setup.clientTimeout)
   374  					}
   375  					// if the client has specified a timeout then we
   376  					// must wire the request context with the same
   377  					// deadline, this is how client-go behaves today.
   378  					ctx := context.Background()
   379  					if setup.clientTimeout > 0 {
   380  						var cancel context.CancelFunc
   381  						ctx, cancel = context.WithTimeout(ctx, setup.clientTimeout)
   382  						defer cancel()
   383  					}
   384  
   385  					req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
   386  					if err != nil {
   387  						t.Errorf("failed to create a new http request - %v", err)
   388  						return
   389  					}
   390  
   391  					resp, err := client.Do(req)
   392  					want.clientErr.verify(t, err)
   393  
   394  					// do we expect a valid http status code?
   395  					switch {
   396  					case want.clientStatusCodeExpected > 0:
   397  						if resp == nil {
   398  							t.Errorf("expected a response from the server: %v", err)
   399  							return
   400  						}
   401  						if resp.StatusCode != want.clientStatusCodeExpected {
   402  							t.Errorf("expected a status code: %d, but got: %#v", want.clientStatusCodeExpected, resp)
   403  						}
   404  
   405  						// read off the body of the response, and verify what we expect
   406  						_, err = io.ReadAll(resp.Body)
   407  						want.clientRespBodyReadErr.verify(t, err)
   408  
   409  						if err := resp.Body.Close(); err != nil {
   410  							t.Errorf("unexpected error while closing the Body of the Response: %v", err)
   411  						}
   412  					default:
   413  						if resp != nil {
   414  							t.Errorf("did not expect a Response from the server, but got: %#v", resp)
   415  						}
   416  						return
   417  					}
   418  				}()
   419  
   420  				select {
   421  				case <-handlerDoneCh:
   422  				case <-time.After(wait.ForeverTestTimeout):
   423  					t.Errorf("expected the request handler to have terminated")
   424  				}
   425  			})
   426  		}
   427  	}
   428  }
   429  
   430  type verifier interface {
   431  	verify(t *testing.T, got error)
   432  }
   433  
   434  type wantNoError struct{}
   435  
   436  func (v wantNoError) verify(t *testing.T, got error) {
   437  	t.Helper()
   438  	if got != nil {
   439  		t.Errorf("unexpected error: %v", got)
   440  	}
   441  }
   442  
   443  type wantContains struct {
   444  	contains string
   445  }
   446  
   447  func (v wantContains) verify(t *testing.T, got error) {
   448  	t.Helper()
   449  
   450  	switch {
   451  	case got != nil:
   452  		if !strings.Contains(got.Error(), v.contains) {
   453  			t.Errorf("expected the error to contain: %q, but got: %v", v.contains, got)
   454  		}
   455  	default:
   456  		t.Errorf("expected an error that contains %q, but got none", v.contains)
   457  	}
   458  }
   459  
   460  type wantError struct {
   461  	err error
   462  }
   463  
   464  func (v wantError) verify(t *testing.T, got error) {
   465  	t.Helper()
   466  
   467  	switch {
   468  	case got != nil:
   469  		if !errors.Is(got, v.err) {
   470  			t.Errorf("expected error: %v, but got: %v", v.err, got)
   471  		}
   472  	default:
   473  		t.Errorf("expected an error %v, but got none", v.err)
   474  	}
   475  }
   476  
   477  type wantEitherOr struct {
   478  	err      error
   479  	contains string
   480  }
   481  
   482  func (v wantEitherOr) verify(t *testing.T, got error) {
   483  	t.Helper()
   484  
   485  	switch {
   486  	case got != nil:
   487  		if !(errors.Is(got, v.err) || strings.Contains(got.Error(), v.contains)) {
   488  			t.Errorf("expected the error to contain: %q or be: %v, but got: %v", v.contains, v.err, got)
   489  		}
   490  	default:
   491  		t.Errorf("expected an error to contain: %q or be: %v, but got none", v.contains, v.err)
   492  	}
   493  }
   494  
   495  type waiter interface {
   496  	wait() <-chan time.Time
   497  	close()
   498  }
   499  
   500  type waitWithDuration struct {
   501  	after time.Duration
   502  }
   503  
   504  func (w waitWithDuration) wait() <-chan time.Time { return time.After(w.after) }
   505  func (w waitWithDuration) close()                 {}
   506  
   507  type waitWithChannelClose struct {
   508  	after chan time.Time
   509  }
   510  
   511  func (w waitWithChannelClose) wait() <-chan time.Time {
   512  	// for http/2, we do the following:
   513  	// a) let the handler block indefinitely
   514  	// b) this forces the write timeout to occur on the server side
   515  	// c) the http2 client receives a stream reset error immediately
   516  	//    after the write timeout occurs.
   517  	// d) the client then closes the channel by calling close
   518  	// e) the handler unblocks and terminates
   519  	return w.after
   520  }
   521  
   522  func (w waitWithChannelClose) close() { close(w.after) }