Repository: sphinx-doc/sphinx

Issue:
Linkcheck shouldn't retry on SSL errors
### Describe the bug

```console
(guides/supporting-windows-using-appveyor: line  152) broken    http://tox.testrun.org - HTTPSConnectionPool(host='tox.testrun.org', port=443): Max retries exceeded with url: / (Caused by SSLError(SSLCertVerificationError("hostname 'tox.testrun.org' doesn't match 'codespeak.net'")))
```

### How to Reproduce

```console
$ git clone https://github.com/pypa/packaging.python.org
$ cd packaging.python.org
$ pip install nox virtualenv -U
$ nox -s linkcheck
# After this, see the error cited above
```

### Expected behavior

Linkcheck should mark links as broken, the first time it sees TLS errors related to fetching the respective URLs.
Moreover, linkcheck should memorize hostnames that produced have broken TLS configuration and skip checking any further links having the same domain — they'll result in the same issue so it's reasonable to mark them all broken right away saving resources and making the check fail fast.

### Your project

https://github.com/pypa/packaging.python.org

### Screenshots

_No response_

### OS

GHA CI / Ubuntu 20.04

### Python version

CPython v3.9.6

### Sphinx version

v4.0.3

### Sphinx extensions

sphinx.ext.extlinks, sphinx.ext.intersphinx, sphinx.ext.todo, sphinx_inline_tabs, sphinx_copybutton

### Extra tools

_No response_

### Additional context

https://github.com/pypa/packaging.python.org/pull/968/checks#step:7:116

Discussion:
jayaddison: Thank you, @francoisfreitag!

Patch:
diff --git a/sphinx/builders/linkcheck.py b/sphinx/builders/linkcheck.py
index f4299d14c..479a5b877 100644
--- a/sphinx/builders/linkcheck.py
+++ b/sphinx/builders/linkcheck.py
@@ -18,7 +18,7 @@
 
 from docutils import nodes
 from requests import Response
-from requests.exceptions import ConnectionError, HTTPError, TooManyRedirects
+from requests.exceptions import ConnectionError, HTTPError, SSLError, TooManyRedirects
 
 from sphinx.application import Sphinx
 from sphinx.builders.dummy import DummyBuilder
@@ -333,6 +333,10 @@ def check_uri() -> tuple[str, str, int]:
                     del response
                     break
 
+                except SSLError as err:
+                    # SSL failure; report that the link is broken.
+                    return 'broken', str(err), 0
+
                 except (ConnectionError, TooManyRedirects) as err:
                     # Servers drop the connection on HEAD requests, causing
                     # ConnectionError.
@@ -361,7 +365,7 @@ def check_uri() -> tuple[str, str, int]:
                     continue
 
                 except Exception as err:
-                    # Unhandled exception (intermittent or permanent); report that the
+                    # Unhandled exception (intermittent or permanent); report that
                     # the link is broken.
                     return 'broken', str(err), 0
 


Test patch:
diff --git a/tests/test_build_linkcheck.py b/tests/test_build_linkcheck.py
index f4229cccd..d7c925e7c 100644
--- a/tests/test_build_linkcheck.py
+++ b/tests/test_build_linkcheck.py
@@ -18,6 +18,7 @@
 
 from sphinx.builders.linkcheck import HyperlinkAvailabilityCheckWorker, RateLimit
 from sphinx.testing.util import strip_escseq
+from sphinx.util import requests
 from sphinx.util.console import strip_colors
 
 from .utils import CERT_FILE, http_server, https_server
@@ -394,7 +395,9 @@ def do_GET(self):
 def test_invalid_ssl(app):
     # Link indicates SSL should be used (https) but the server does not handle it.
     with http_server(OKHandler):
-        app.build()
+        with mock.patch("sphinx.builders.linkcheck.requests.get", wraps=requests.get) as get_request:
+            app.build()
+            assert not get_request.called
 
     with open(app.outdir / 'output.json', encoding='utf-8') as fp:
         content = json.load(fp)