github.com/Lephar/snapd@v0.0.0-20210825215435-c7fba9cef4d2/httputil/retry_test.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2016-2017 Canonical Ltd
     5   *
     6   * This program is free software: you can redistribute it and/or modify
     7   * it under the terms of the GNU General Public License version 3 as
     8   * published by the Free Software Foundation.
     9   *
    10   * This program is distributed in the hope that it will be useful,
    11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
    12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13   * GNU General Public License for more details.
    14   *
    15   * You should have received a copy of the GNU General Public License
    16   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17   *
    18   */
    19  
    20  package httputil_test
    21  
    22  import (
    23  	"encoding/json"
    24  	"fmt"
    25  	"io"
    26  	"net"
    27  	"net/http"
    28  	"net/http/httptest"
    29  	"net/url"
    30  	"sync"
    31  	"time"
    32  
    33  	. "gopkg.in/check.v1"
    34  	"gopkg.in/retry.v1"
    35  
    36  	"github.com/snapcore/snapd/httputil"
    37  )
    38  
    39  type retrySuite struct{}
    40  
    41  var _ = Suite(&retrySuite{})
    42  
    43  func (s *retrySuite) SetUpTest(c *C) {
    44  }
    45  
    46  func (s *retrySuite) TearDownTest(c *C) {
    47  }
    48  
    49  var testRetryStrategy = retry.LimitCount(5, retry.LimitTime(5*time.Second,
    50  	retry.Exponential{
    51  		Initial: 1 * time.Millisecond,
    52  		Factor:  1,
    53  	},
    54  ))
    55  
    56  type counter struct {
    57  	n  int
    58  	mu sync.Mutex
    59  }
    60  
    61  func (cnt *counter) Inc() int {
    62  	cnt.mu.Lock()
    63  	defer cnt.mu.Unlock()
    64  	cnt.n++
    65  	return cnt.n
    66  }
    67  
    68  func (cnt *counter) Count() int {
    69  	cnt.mu.Lock()
    70  	defer cnt.mu.Unlock()
    71  	return cnt.n
    72  }
    73  
    74  func (s *retrySuite) TestRetryRequestOnEOF(c *C) {
    75  	n := 0
    76  	var mockServer *httptest.Server
    77  	mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    78  		n++
    79  		if n < 4 {
    80  			io.WriteString(w, "{")
    81  			mockServer.CloseClientConnections()
    82  			return
    83  		}
    84  		io.WriteString(w, `{"ok": true}`)
    85  	}))
    86  
    87  	c.Assert(mockServer, NotNil)
    88  	defer mockServer.Close()
    89  
    90  	cli := httputil.NewHTTPClient(nil)
    91  
    92  	doRequest := func() (*http.Response, error) {
    93  		return cli.Get(mockServer.URL)
    94  	}
    95  
    96  	failure := false
    97  	var got interface{}
    98  	readResponseBody := func(resp *http.Response) error {
    99  		failure = false
   100  		if resp.StatusCode != 200 {
   101  			failure = true
   102  			return nil
   103  		}
   104  		return json.NewDecoder(resp.Body).Decode(&got)
   105  	}
   106  
   107  	_, err := httputil.RetryRequest("endp", doRequest, readResponseBody, testRetryStrategy)
   108  	c.Assert(err, IsNil)
   109  
   110  	c.Assert(failure, Equals, false)
   111  	c.Check(got, DeepEquals, map[string]interface{}{"ok": true})
   112  	c.Assert(n, Equals, 4)
   113  }
   114  
   115  func (s *retrySuite) TestRetryRequestFailWithEOF(c *C) {
   116  	n := new(counter)
   117  	var mockServer *httptest.Server
   118  	mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   119  		n.Inc()
   120  		io.WriteString(w, "{")
   121  		mockServer.CloseClientConnections()
   122  		return
   123  	}))
   124  
   125  	c.Assert(mockServer, NotNil)
   126  	defer mockServer.Close()
   127  
   128  	cli := httputil.NewHTTPClient(nil)
   129  
   130  	doRequest := func() (*http.Response, error) {
   131  		return cli.Get(mockServer.URL)
   132  	}
   133  
   134  	failure := false
   135  	var got interface{}
   136  	readResponseBody := func(resp *http.Response) error {
   137  		failure = false
   138  		if resp.StatusCode != 200 {
   139  			failure = true
   140  			return nil
   141  		}
   142  		return json.NewDecoder(resp.Body).Decode(&got)
   143  	}
   144  
   145  	_, err := httputil.RetryRequest("endp", doRequest, readResponseBody, testRetryStrategy)
   146  	c.Assert(err, NotNil)
   147  	c.Check(err, ErrorMatches, `^Get \"?http://127.0.0.1:.*?\"?: EOF$`)
   148  
   149  	c.Check(failure, Equals, false)
   150  	c.Assert(n.Count(), Equals, 5)
   151  }
   152  
   153  func (s *retrySuite) TestRetryRequestOn500(c *C) {
   154  	n := 0
   155  	var mockServer *httptest.Server
   156  	mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   157  		n++
   158  		if n < 4 {
   159  			w.WriteHeader(500)
   160  			return
   161  		}
   162  		io.WriteString(w, `{"ok": true}`)
   163  	}))
   164  
   165  	c.Assert(mockServer, NotNil)
   166  	defer mockServer.Close()
   167  
   168  	cli := httputil.NewHTTPClient(nil)
   169  
   170  	doRequest := func() (*http.Response, error) {
   171  		return cli.Get(mockServer.URL)
   172  	}
   173  
   174  	failure := false
   175  	var got interface{}
   176  	readResponseBody := func(resp *http.Response) error {
   177  		failure = false
   178  		if resp.StatusCode != 200 {
   179  			failure = true
   180  			return nil
   181  		}
   182  		return json.NewDecoder(resp.Body).Decode(&got)
   183  	}
   184  
   185  	_, err := httputil.RetryRequest("endp", doRequest, readResponseBody, testRetryStrategy)
   186  	c.Assert(err, IsNil)
   187  
   188  	c.Assert(failure, Equals, false)
   189  	c.Check(got, DeepEquals, map[string]interface{}{"ok": true})
   190  	c.Assert(n, Equals, 4)
   191  }
   192  
   193  func (s *retrySuite) TestRetryRequestFailOn500(c *C) {
   194  	n := 0
   195  	var mockServer *httptest.Server
   196  	mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   197  		n++
   198  		w.WriteHeader(500)
   199  		return
   200  	}))
   201  
   202  	c.Assert(mockServer, NotNil)
   203  	defer mockServer.Close()
   204  
   205  	cli := httputil.NewHTTPClient(nil)
   206  
   207  	doRequest := func() (*http.Response, error) {
   208  		return cli.Get(mockServer.URL)
   209  	}
   210  
   211  	failure := false
   212  	var got interface{}
   213  	readResponseBody := func(resp *http.Response) error {
   214  		failure = false
   215  		if resp.StatusCode != 200 {
   216  			failure = true
   217  			return nil
   218  		}
   219  		return json.NewDecoder(resp.Body).Decode(&got)
   220  	}
   221  
   222  	resp, err := httputil.RetryRequest("endp", doRequest, readResponseBody, testRetryStrategy)
   223  	c.Assert(err, IsNil)
   224  	c.Assert(resp.StatusCode, Equals, 500)
   225  
   226  	c.Check(failure, Equals, true)
   227  	c.Assert(n, Equals, 5)
   228  }
   229  
   230  func (s *retrySuite) TestRetryRequestUnexpectedEOFHandling(c *C) {
   231  	permanentlyBrokenSrvCalls := 0
   232  	somewhatBrokenSrvCalls := 0
   233  
   234  	mockPermanentlyBrokenServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   235  		permanentlyBrokenSrvCalls++
   236  		w.Header().Add("Content-Length", "1000")
   237  	}))
   238  	c.Assert(mockPermanentlyBrokenServer, NotNil)
   239  	defer mockPermanentlyBrokenServer.Close()
   240  
   241  	mockSomewhatBrokenServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   242  		somewhatBrokenSrvCalls++
   243  		if somewhatBrokenSrvCalls > 3 {
   244  			io.WriteString(w, `{"ok": true}`)
   245  			return
   246  		}
   247  		w.Header().Add("Content-Length", "1000")
   248  	}))
   249  	c.Assert(mockSomewhatBrokenServer, NotNil)
   250  	defer mockSomewhatBrokenServer.Close()
   251  
   252  	cli := httputil.NewHTTPClient(nil)
   253  
   254  	url := ""
   255  	doRequest := func() (*http.Response, error) {
   256  		return cli.Get(url)
   257  	}
   258  
   259  	failure := false
   260  	var got interface{}
   261  	readResponseBody := func(resp *http.Response) error {
   262  		failure = false
   263  		if resp.StatusCode != 200 {
   264  			failure = true
   265  			return nil
   266  		}
   267  		return json.NewDecoder(resp.Body).Decode(&got)
   268  	}
   269  
   270  	// Check that we really recognize unexpected EOF error by failing on all retries
   271  	url = mockPermanentlyBrokenServer.URL
   272  	_, err := httputil.RetryRequest("endp", doRequest, readResponseBody, testRetryStrategy)
   273  	c.Assert(err, NotNil)
   274  	c.Assert(err, Equals, io.ErrUnexpectedEOF)
   275  	c.Assert(err, ErrorMatches, "unexpected EOF")
   276  	// check that we exhausted all retries (as defined by mocked retry strategy)
   277  	c.Assert(permanentlyBrokenSrvCalls, Equals, 5)
   278  	c.Check(failure, Equals, false)
   279  	c.Check(got, Equals, nil)
   280  
   281  	url = mockSomewhatBrokenServer.URL
   282  	failure = false
   283  	got = nil
   284  	// Check that we retry on unexpected EOF and eventually succeed
   285  	_, err = httputil.RetryRequest("endp", doRequest, readResponseBody, testRetryStrategy)
   286  	c.Assert(err, IsNil)
   287  	// check that we retried 4 times
   288  	c.Check(failure, Equals, false)
   289  	c.Check(got, DeepEquals, map[string]interface{}{"ok": true})
   290  	c.Assert(somewhatBrokenSrvCalls, Equals, 4)
   291  }
   292  
   293  func (s *retrySuite) TestRetryRequestFailOnReadResponseBody(c *C) {
   294  	var mockServer *httptest.Server
   295  	mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   296  		io.WriteString(w, "<bad>")
   297  		return
   298  	}))
   299  
   300  	c.Assert(mockServer, NotNil)
   301  	defer mockServer.Close()
   302  
   303  	cli := httputil.NewHTTPClient(nil)
   304  
   305  	doRequest := func() (*http.Response, error) {
   306  		return cli.Get(mockServer.URL)
   307  	}
   308  
   309  	failure := false
   310  	var got interface{}
   311  	readResponseBody := func(resp *http.Response) error {
   312  		failure = false
   313  		if resp.StatusCode != 200 {
   314  			failure = true
   315  			return nil
   316  		}
   317  		return json.NewDecoder(resp.Body).Decode(&got)
   318  	}
   319  
   320  	_, err := httputil.RetryRequest("endp", doRequest, readResponseBody, testRetryStrategy)
   321  	c.Assert(err, ErrorMatches, `invalid character '<' looking for beginning of value`)
   322  	c.Check(failure, Equals, false)
   323  }
   324  
   325  func (s *retrySuite) TestRetryRequestReadResponseBodyFailure(c *C) {
   326  	var mockServer *httptest.Server
   327  	mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   328  		w.WriteHeader(404)
   329  		io.WriteString(w, `{"error": true}`)
   330  		return
   331  	}))
   332  
   333  	c.Assert(mockServer, NotNil)
   334  	defer mockServer.Close()
   335  
   336  	cli := httputil.NewHTTPClient(nil)
   337  
   338  	doRequest := func() (*http.Response, error) {
   339  		return cli.Get(mockServer.URL)
   340  	}
   341  
   342  	failure := false
   343  	var got interface{}
   344  	readResponseBody := func(resp *http.Response) error {
   345  		failure = false
   346  		if resp.StatusCode != 200 {
   347  			failure = true
   348  			return nil
   349  		}
   350  		return json.NewDecoder(resp.Body).Decode(&got)
   351  	}
   352  
   353  	resp, err := httputil.RetryRequest("endp", doRequest, readResponseBody, testRetryStrategy)
   354  	c.Assert(err, IsNil)
   355  	c.Check(failure, Equals, true)
   356  	c.Check(resp.StatusCode, Equals, 404)
   357  }
   358  
   359  func (s *retrySuite) TestRetryRequestTimeoutHandling(c *C) {
   360  	permanentlyBrokenSrvCalls := new(counter)
   361  	somewhatBrokenSrvCalls := new(counter)
   362  
   363  	finished := make(chan struct{})
   364  
   365  	mockPermanentlyBrokenServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   366  		permanentlyBrokenSrvCalls.Inc()
   367  		<-finished
   368  	}))
   369  	c.Assert(mockPermanentlyBrokenServer, NotNil)
   370  	defer mockPermanentlyBrokenServer.Close()
   371  
   372  	mockSomewhatBrokenServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   373  		calls := somewhatBrokenSrvCalls.Inc()
   374  		if calls > 2 {
   375  			io.WriteString(w, `{"ok": true}`)
   376  			return
   377  		}
   378  		<-finished
   379  	}))
   380  	c.Assert(mockSomewhatBrokenServer, NotNil)
   381  	defer mockSomewhatBrokenServer.Close()
   382  
   383  	defer close(finished)
   384  
   385  	cli := httputil.NewHTTPClient(&httputil.ClientOptions{
   386  		Timeout: 100 * time.Millisecond,
   387  	})
   388  
   389  	url := ""
   390  	doRequest := func() (*http.Response, error) {
   391  		return cli.Get(url)
   392  	}
   393  
   394  	failure := false
   395  	var got interface{}
   396  	readResponseBody := func(resp *http.Response) error {
   397  		failure = false
   398  		if resp.StatusCode != 200 {
   399  			failure = true
   400  			return nil
   401  		}
   402  		return json.NewDecoder(resp.Body).Decode(&got)
   403  	}
   404  
   405  	// Check that we really recognize unexpected EOF error by failing on all retries
   406  	url = mockPermanentlyBrokenServer.URL
   407  	_, err := httputil.RetryRequest("endp", doRequest, readResponseBody, testRetryStrategy)
   408  	c.Assert(err, NotNil)
   409  	// context deadline detection when the response body was not received
   410  	// yet is racy and context.DeadlineExceeded errors are not necessarily
   411  	// correctly wrapped as client timeouts
   412  	c.Assert(err, ErrorMatches, `.*(Client.Timeout|context deadline).*`)
   413  	// check that we exhausted all retries (as defined by mocked retry strategy)
   414  	c.Assert(permanentlyBrokenSrvCalls.Count(), Equals, 5)
   415  	c.Check(failure, Equals, false)
   416  	c.Check(got, Equals, nil)
   417  
   418  	url = mockSomewhatBrokenServer.URL
   419  	failure = false
   420  	got = nil
   421  	// Check that we retry on unexpected EOF and eventually succeed
   422  	_, err = httputil.RetryRequest("endp", doRequest, readResponseBody, testRetryStrategy)
   423  	c.Assert(err, IsNil)
   424  	// check that we retried 4 times
   425  	c.Check(failure, Equals, false)
   426  	c.Check(got, DeepEquals, map[string]interface{}{"ok": true})
   427  	c.Assert(somewhatBrokenSrvCalls.Count(), Equals, 3)
   428  }
   429  
   430  func (s *retrySuite) TestRetryDoesNotFailForPermanentDNSErrors(c *C) {
   431  	n := 0
   432  	doRequest := func() (*http.Response, error) {
   433  		n++
   434  
   435  		// this is the error reported when executing the request with a working network & DNS, when
   436  		// a host is unknown.
   437  		return nil, &net.OpError{
   438  			Op:  "dial",
   439  			Net: "tcp",
   440  			Err: fmt.Errorf("no such host"),
   441  		}
   442  	}
   443  
   444  	readResponseBody := func(resp *http.Response) error {
   445  		return nil
   446  	}
   447  
   448  	_, err := httputil.RetryRequest("endp", doRequest, readResponseBody, testRetryStrategy)
   449  	c.Assert(err, NotNil)
   450  	// we try exactly once, a non-existing server is a permanent error
   451  	c.Assert(n, Equals, 1)
   452  }
   453  
   454  func (s *retrySuite) TestRetryOnTemporaryDNSfailure(c *C) {
   455  	n := 0
   456  	doRequest := func() (*http.Response, error) {
   457  		n++
   458  		return nil, &net.OpError{
   459  			Op:  "dial",
   460  			Net: "tcp",
   461  			Err: &net.DNSError{IsTemporary: true},
   462  		}
   463  	}
   464  	readResponseBody := func(resp *http.Response) error {
   465  		return nil
   466  	}
   467  	_, err := httputil.RetryRequest("endp", doRequest, readResponseBody, testRetryStrategy)
   468  	c.Assert(err, NotNil)
   469  	c.Assert(n > 1, Equals, true, Commentf("%v not > 1", n))
   470  }
   471  
   472  func (s *retrySuite) TestRetryOnTemporaryDNSfailureNotGo19(c *C) {
   473  	n := 0
   474  	doRequest := func() (*http.Response, error) {
   475  		n++
   476  		return nil, &net.OpError{
   477  			Op:  "dial",
   478  			Net: "tcp",
   479  			Err: &net.DNSError{
   480  				Err: "[::1]:42463->[::1]:53: read: connection refused",
   481  			},
   482  		}
   483  	}
   484  	readResponseBody := func(resp *http.Response) error {
   485  		return nil
   486  	}
   487  	_, err := httputil.RetryRequest("endp", doRequest, readResponseBody, testRetryStrategy)
   488  	c.Assert(err, NotNil)
   489  	c.Assert(n > 1, Equals, true, Commentf("%v not > 1", n))
   490  }
   491  
   492  func (s *retrySuite) TestRetryOnHttp2ProtocolErrors(c *C) {
   493  	n := 0
   494  	doRequest := func() (*http.Response, error) {
   495  		n++
   496  		return nil, &url.Error{
   497  			Op:  "Get",
   498  			URL: "http://...",
   499  			Err: fmt.Errorf("http.http2StreamError{StreamID:0x1, Code:0x1, Cause:error(nil)}"),
   500  		}
   501  	}
   502  	readResponseBody := func(resp *http.Response) error {
   503  		return nil
   504  	}
   505  	_, err := httputil.RetryRequest("endp", doRequest, readResponseBody, testRetryStrategy)
   506  	c.Assert(err, NotNil)
   507  	c.Assert(n > 1, Equals, true, Commentf("%v not > 1", n))
   508  }