@@ -170,7 +170,8 @@ def test_unsupported_redirect_status_is_fatal() -> None:
170170
171171@pytest .mark .parametrize ("status_code" , [403 , 408 , 425 , 429 , 500 , 503 ])
172172def test_transient_and_access_denied_statuses_need_review (status_code ) -> None :
173- session = FakeSession (FakeResponse (status_code ), FakeResponse (status_code ))
173+ head = FakeResponse (status_code )
174+ session = FakeSession (head , FakeResponse (status_code ))
174175 checker = LinkChecker (
175176 guard = guard_for (),
176177 session_factory = lambda : session ,
@@ -179,6 +180,8 @@ def test_transient_and_access_denied_statuses_need_review(status_code) -> None:
179180 min_interval = 0 ,
180181 )
181182 assert checker .check_one (link ()).status == "review"
183+ assert len (session .calls ) == 1
184+ assert head .closed is True
182185
183186
184187@pytest .mark .parametrize (
@@ -345,6 +348,7 @@ def head(self, url, **kwargs):
345348 guard = guard_for (),
346349 session_factory = lambda : BrokenSession (FakeResponse (200 )),
347350 workers = 1 ,
351+ retries = 0 ,
348352 min_interval = 0 ,
349353 )
350354 result = checker .check_one (link ())
@@ -370,6 +374,7 @@ def head(self, url, **kwargs):
370374 guard = guard_for (),
371375 session_factory = lambda : BrokenSession (FakeResponse (200 )),
372376 workers = 1 ,
377+ retries = 0 ,
373378 min_interval = 0 ,
374379 )
375380
@@ -385,18 +390,22 @@ def head(self, url, **kwargs):
385390 guard = guard_for (),
386391 session_factory = lambda : SlowSession (FakeResponse (200 )),
387392 workers = 1 ,
393+ retries = 0 ,
388394 min_interval = 0 ,
389395 )
390396
391397 assert checker .check_one (link ()).status == "review"
392398
393399
394- def test_retry_configuration_ignores_unbounded_retry_after () -> None :
395- session = build_session (guard_for (), retries = 2 , backoff_factor = 0.5 )
400+ def test_adapter_transport_and_status_retries_are_disabled () -> None :
401+ session = build_session (guard_for ())
396402 try :
397403 retry = session .get_adapter ("https://" ).max_retries
398404 assert retry .respect_retry_after_header is False
399405 assert retry .backoff_max == 5.0
406+ assert retry .total == 0
407+ assert retry .connect == 0
408+ assert retry .read == 0
400409 assert retry .status == 0
401410 assert not retry .status_forcelist
402411 finally :
@@ -434,6 +443,108 @@ def head(self, url, **kwargs):
434443 assert second .closed is True
435444
436445
446+ @pytest .mark .parametrize ("retry_after" , ["60" , "9" * 400 ])
447+ def test_large_retry_after_stops_without_get_fallback (
448+ monkeypatch , retry_after : str
449+ ) -> None :
450+ response = FakeResponse (429 , headers = {"Retry-After" : retry_after })
451+ session = FakeSession (response )
452+ checker = LinkChecker (
453+ guard = guard_for (),
454+ session_factory = lambda : session ,
455+ workers = 1 ,
456+ retries = 2 ,
457+ min_interval = 0 ,
458+ )
459+ sleeps = []
460+ monkeypatch .setattr (check_links .time , "sleep" , sleeps .append )
461+
462+ result = checker .check_one (link ())
463+
464+ assert result .status == "review"
465+ assert len (session .calls ) == 1
466+ assert sleeps == []
467+ assert response .closed is True
468+
469+
470+ def test_bounded_retry_after_is_honored (monkeypatch ) -> None :
471+ first = FakeResponse (429 , headers = {"Retry-After" : "2" })
472+ second = FakeResponse (200 )
473+
474+ class RetrySession (FakeSession ):
475+ def __init__ (self ):
476+ super ().__init__ (first )
477+ self .responses = iter ([first , second ])
478+
479+ def head (self , url , ** kwargs ):
480+ self .calls .append (("HEAD" , url , kwargs ))
481+ return next (self .responses )
482+
483+ session = RetrySession ()
484+ checker = LinkChecker (
485+ guard = guard_for (),
486+ session_factory = lambda : session ,
487+ workers = 1 ,
488+ retries = 1 ,
489+ min_interval = 0 ,
490+ )
491+ sleeps = []
492+ monkeypatch .setattr (check_links .time , "sleep" , sleeps .append )
493+
494+ result = checker .check_one (link ())
495+
496+ assert result .status == "working"
497+ assert len (session .calls ) == 2
498+ assert sleeps == [2.0 ]
499+
500+
501+ def test_transport_timeout_retry_is_manual () -> None :
502+ response = FakeResponse (200 )
503+
504+ class FlakySession (FakeSession ):
505+ def __init__ (self ):
506+ super ().__init__ (response )
507+ self .attempt = 0
508+
509+ def head (self , url , ** kwargs ):
510+ self .calls .append (("HEAD" , url , kwargs ))
511+ self .attempt += 1
512+ if self .attempt == 1 :
513+ raise requests .Timeout ("timed out" )
514+ return response
515+
516+ session = FlakySession ()
517+ checker = LinkChecker (
518+ guard = guard_for (),
519+ session_factory = lambda : session ,
520+ workers = 1 ,
521+ retries = 1 ,
522+ backoff_factor = 0 ,
523+ min_interval = 0 ,
524+ )
525+
526+ result = checker .check_one (link ())
527+
528+ assert result .status == "working"
529+ assert len (session .calls ) == 2
530+
531+
532+ def test_wrapped_timeout_remains_review_needed () -> None :
533+ class WrappedTimeoutSession (FakeSession ):
534+ def head (self , url , ** kwargs ):
535+ raise requests .ConnectionError (TimeoutError ("timed out" ))
536+
537+ checker = LinkChecker (
538+ guard = guard_for (),
539+ session_factory = lambda : WrappedTimeoutSession (FakeResponse (200 )),
540+ workers = 1 ,
541+ retries = 0 ,
542+ min_interval = 0 ,
543+ )
544+
545+ assert checker .check_one (link ()).status == "review"
546+
547+
437548def test_responses_close_when_result_processing_fails (monkeypatch ) -> None :
438549 head = FakeResponse (404 )
439550 response = FakeResponse (200 )
0 commit comments