github.com/hugh712/snapd@v0.0.0-20200910133618-1a99902bd583/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 }