Skip to content

Commit

Permalink
linkcheck: add a distinct 'timeout' reporting status
Browse files Browse the repository at this point in the history
  • Loading branch information
jayaddison committed Jan 12, 2024
1 parent bcb1825 commit 4809c8f
Show file tree
Hide file tree
Showing 3 changed files with 38 additions and 1 deletion.
2 changes: 2 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ Bugs fixed
Set this option to ``False`` to report HTTP 401 (unauthorized) server
responses as broken.
Patch by James Addison.
* #11868: linkcheck: added a distinct ``timeout`` reporting status code.
Patch by James Addison.

Testing
-------
Expand Down
16 changes: 15 additions & 1 deletion sphinx/builders/linkcheck.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

from docutils import nodes
from requests.exceptions import ConnectionError, HTTPError, SSLError, TooManyRedirects
from requests.exceptions import Timeout as RequestTimeout

from sphinx.builders.dummy import DummyBuilder
from sphinx.deprecation import RemovedInSphinx80Warning
Expand Down Expand Up @@ -64,6 +65,7 @@ class CheckExternalLinksBuilder(DummyBuilder):

def init(self) -> None:
self.broken_hyperlinks = 0
self.timed_out_hyperlinks = 0
self.hyperlinks: dict[str, Hyperlink] = {}
# set a timeout for non-responding servers
socket.setdefaulttimeout(5.0)
Expand All @@ -88,7 +90,7 @@ def finish(self) -> None:
for result in checker.check(self.hyperlinks):
self.process_result(result)

if self.broken_hyperlinks:
if self.broken_hyperlinks or self.timed_out_hyperlinks:
self.app.statuscode = 1

def process_result(self, result: CheckResult) -> None:
Expand All @@ -115,6 +117,15 @@ def process_result(self, result: CheckResult) -> None:
self.write_entry('local', result.docname, filename, result.lineno, result.uri)
elif result.status == 'working':
logger.info(darkgreen('ok ') + result.uri + result.message)
elif result.status == 'timeout':
if self.app.quiet or self.app.warningiserror:
logger.warning('timeout ' + result.uri + result.message,
location=(result.docname, result.lineno))
else:
logger.info(red('timeout ') + result.uri + red(' - ' + result.message))
self.write_entry('timeout', result.docname, filename, result.lineno,
result.uri + ': ' + result.message)
self.timed_out_hyperlinks += 1
elif result.status == 'broken':
if self.app.quiet or self.app.warningiserror:
logger.warning(__('broken link: %s (%s)'), result.uri, result.message,
Expand Down Expand Up @@ -436,6 +447,9 @@ def _check_uri(self, uri: str, hyperlink: Hyperlink) -> tuple[str, str, int]:
del response
break

except RequestTimeout as err:
return 'timeout', str(err), 0

except SSLError as err:
# SSL failure; report that the link is broken.
return 'broken', str(err), 0
Expand Down
21 changes: 21 additions & 0 deletions tests/test_build_linkcheck.py
Original file line number Diff line number Diff line change
Expand Up @@ -854,6 +854,27 @@ def test_too_many_requests_retry_after_without_header(app, capsys):
)


@pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver', freshenv=True)
def test_requests_timeout(app):
class DelayedResponseHandler(http.server.BaseHTTPRequestHandler):
protocol_version = "HTTP/1.1"

def do_GET(self):
time.sleep(0.2) # wait before sending any response data
self.send_response(200, "OK")
self.send_header("Content-Length", "0")
self.end_headers()

app.config.linkcheck_timeout = 0.01
with http_server(DelayedResponseHandler):
app.build()

with open(app.outdir / "output.json", encoding="utf-8") as fp:
content = json.load(fp)

assert content["status"] == "timeout"


@pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver', freshenv=True)
def test_too_many_requests_user_timeout(app):
app.config.linkcheck_rate_limit_timeout = 0.0
Expand Down

0 comments on commit 4809c8f

Please sign in to comment.