Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
6aee5b8
linkcheck builder: update handling of HTTP 401 status code to conside…
jayaddison May 21, 2023
7f68206
linkcheck builder: introduce a 'linkcheck_allow_unauthorized' config …
jayaddison Sep 21, 2023
2d4f4bc
Update CHANGES.rst
jayaddison Sep 21, 2023
a958e86
Clarify phrasing in CHANGES.rst: the setting has to be configured to …
jayaddison Sep 21, 2023
712f060
CHANGES.rst: fixup: relocate the changelog entry to the correct locat…
jayaddison Sep 21, 2023
ee7348f
docs: add documentation for the 'linkcheck_allow_unauthorized' config…
jayaddison Sep 21, 2023
c19f7c3
CHANGES.rst: nitpick: undo accidental empty-line removal
jayaddison Sep 21, 2023
a290e3c
CHANGES.rst: nitpick: phrasing: 'handle...as broken' -> 'report...as …
jayaddison Sep 22, 2023
46d8206
CHANGES.rst: nitpick: brevity
jayaddison Sep 22, 2023
c895ca2
linkcheck builder: add a deprecation warning indicating that the 'lin…
jayaddison Sep 22, 2023
37b50ae
CHANGES.rst: fixup: add self-attribution
jayaddison Sep 22, 2023
bc3c390
Merge branch 'master' into issue-11433/adjust-linkcheck-http-401-hand…
jayaddison Sep 23, 2023
37634b1
ruff: linting fixups
jayaddison Sep 23, 2023
a81f283
Apply code review suggestion: filter warnings in test case using more…
jayaddison Sep 26, 2023
de6ac23
Apply code review suggestion: make the 'allow_unauthorized' worker va…
jayaddison Sep 26, 2023
9f87c41
docs: add removal note to 'linkcheck_allow_unauthorized' config optio…
jayaddison Sep 26, 2023
2b90b76
linkcheck builder: relocate 'linkcheck_allow_unauthorized' warning to…
jayaddison Sep 26, 2023
d9144cc
fixup: use pytest.mark.filterwarnings to filter warning _before_ app …
jayaddison Sep 26, 2023
fa9be8a
cleanup: remove unused imports
jayaddison Sep 26, 2023
4750345
Apply code review suggestion: set default value for 'allow_unauthoriz…
jayaddison Sep 27, 2023
6d450c2
Updated plan: instead of removing the setting entirely in Sphinx 8.0,…
jayaddison Sep 27, 2023
cdfa342
Apply code review suggestion: when an HTTP 401 response is encountere…
jayaddison Sep 27, 2023
daf4efb
Code behaviour consensus: the URI of unauthorized HTTP responses shou…
jayaddison Sep 30, 2023
e17581a
CHANGES.rst: prefer Pythonic representation of false value
jayaddison Oct 1, 2023
2070c88
Merge branch 'master' into issue-11433/adjust-linkcheck-http-401-hand…
jayaddison Oct 3, 2023
bc97be3
Merge branch 'master' into issue-11433/adjust-linkcheck-http-401-hand…
jayaddison Oct 11, 2023
a75abcf
Merge branch 'master' into issue-11433/adjust-linkcheck-http-401-hand…
jayaddison Dec 11, 2023
5d16bb3
Merge branch 'master' into issue-11433/adjust-linkcheck-http-401-hand…
jayaddison Dec 26, 2023
000af8b
Merge branch 'master' into issue-11433/adjust-linkcheck-http-401-hand…
jayaddison Dec 30, 2023
bfaf179
Merge branch 'master' into issue-11433/adjust-linkcheck-http-401-hand…
jayaddison Jan 9, 2024
16a3695
Update sphinx/builders/linkcheck.py
AA-Turner Jan 9, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ Bugs fixed
* #11715: Apply ``tls_verify`` and ``tls_cacerts`` config to
``ImageDownloader``.
Patch by Nick Touran.
* #11433: Added the ``linkcheck_allow_unauthorized`` configuration option.
Set this option to ``False`` to report HTTP 401 (unauthorized) server
responses as broken.
Patch by James Addison.

Testing
-------
Expand Down
12 changes: 12 additions & 0 deletions doc/usage/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2915,6 +2915,18 @@ Options for the linkcheck builder

.. versionadded:: 4.4

.. confval:: linkcheck_allow_unauthorized

When a webserver responds with an HTTP 401 (unauthorized) response, the
current default behaviour of Sphinx is to treat the link as "working". To
change that behaviour, set this option to ``False``.

The default value for this option will be changed in Sphinx 8.0; from that
version onwards, HTTP 401 responses to checked hyperlinks will be treated
as "broken" by default.

.. versionadded:: 7.3


Options for the XML builder
---------------------------
Expand Down
38 changes: 36 additions & 2 deletions sphinx/builders/linkcheck.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import re
import socket
import time
import warnings
from html.parser import HTMLParser
from os import path
from queue import PriorityQueue, Queue
Expand All @@ -18,6 +19,7 @@
from requests.exceptions import ConnectionError, HTTPError, SSLError, TooManyRedirects

from sphinx.builders.dummy import DummyBuilder
from sphinx.deprecation import RemovedInSphinx80Warning
from sphinx.locale import __
from sphinx.transforms.post_transforms import SphinxPostTransform
from sphinx.util import encode_uri, logging, requests
Expand Down Expand Up @@ -66,6 +68,15 @@ def init(self) -> None:
# set a timeout for non-responding servers
socket.setdefaulttimeout(5.0)

if not self.config.linkcheck_allow_unauthorized:
deprecation_msg = (
"The default value for 'linkcheck_allow_unauthorized' will change "
"from `True` in Sphinx 7.3+ to `False`, meaning that HTTP 401 "
"unauthorized responses will be reported as broken by default. "
"See https://github.com/sphinx-doc/sphinx/issues/11433 for details."
)
warnings.warn(deprecation_msg, RemovedInSphinx80Warning, stacklevel=1)

def finish(self) -> None:
checker = HyperlinkAvailabilityChecker(self.config)
logger.info('')
Expand Down Expand Up @@ -283,6 +294,7 @@ def __init__(self, config: Config,
self.allowed_redirects = config.linkcheck_allowed_redirects
self.retries: int = config.linkcheck_retries
self.rate_limit_timeout = config.linkcheck_rate_limit_timeout
self._allow_unauthorized = config.linkcheck_allow_unauthorized

self.user_agent = config.user_agent
self.tls_verify = config.tls_verify
Expand Down Expand Up @@ -437,9 +449,30 @@ def _check_uri(self, uri: str, hyperlink: Hyperlink) -> tuple[str, str, int]:
except HTTPError as err:
error_message = str(err)

# Unauthorised: the reference probably exists
# Unauthorized: the client did not provide required credentials
if status_code == 401:
return 'working', 'unauthorized', 0
if self._allow_unauthorized:
deprecation_msg = (
"\n---\n"
"The linkcheck builder encountered an HTTP 401 "
"(unauthorized) response, and will report it as "
"'working' in this version of Sphinx to maintain "
"backwards-compatibility."
"\n"
"This logic will change in Sphinx 8.0 which will "
"report the hyperlink as 'broken'."
"\n"
"To explicitly continue treating unauthorized "
"hyperlink responses as 'working', set the "
"'linkcheck_allow_unauthorized' config option to "
"``True``."
"\n"
"See sphinx-doc/sphinx#11433 for details."
"\n---"
)
warnings.warn(deprecation_msg, RemovedInSphinx80Warning, stacklevel=1)
status = 'working' if self._allow_unauthorized else 'broken'
return status, 'unauthorized', 0

# Rate limiting; back-off if allowed, or report failure otherwise
if status_code == 429:
Expand Down Expand Up @@ -625,6 +658,7 @@ def setup(app: Sphinx) -> dict[str, Any]:
app.add_config_value('linkcheck_anchors_ignore', ['^!'], '')
app.add_config_value('linkcheck_anchors_ignore_for_url', (), '', (tuple, list))
app.add_config_value('linkcheck_rate_limit_timeout', 300.0, '')
app.add_config_value('linkcheck_allow_unauthorized', True, '')

app.add_event('linkcheck-process-uri')

Expand Down
30 changes: 24 additions & 6 deletions tests/test_build_linkcheck.py
Original file line number Diff line number Diff line change
Expand Up @@ -348,8 +348,12 @@ class CustomHandler(http.server.BaseHTTPRequestHandler):

def authenticated(method):
def method_if_authenticated(self):
if (expected_token is None
or self.headers["Authorization"] == f"Basic {expected_token}"):
if expected_token is None:
return method(self)
elif not self.headers["Authorization"]:
self.send_response(401, "Unauthorized")
self.end_headers()
elif self.headers["Authorization"] == f"Basic {expected_token}":
return method(self)
else:
self.send_response(403, "Forbidden")
Expand Down Expand Up @@ -392,6 +396,21 @@ def test_auth_header_uses_first_match(app):
assert content["status"] == "working"


@pytest.mark.filterwarnings('ignore::sphinx.deprecation.RemovedInSphinx80Warning')
@pytest.mark.sphinx(
'linkcheck', testroot='linkcheck-localserver', freshenv=True,
confoverrides={'linkcheck_allow_unauthorized': False})
def test_unauthorized_broken(app):
with http_server(custom_handler(valid_credentials=("user1", "password"))):
app.build()

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

assert content["info"] == "unauthorized"
assert content["status"] == "broken"


@pytest.mark.sphinx(
'linkcheck', testroot='linkcheck-localserver', freshenv=True,
confoverrides={'linkcheck_auth': [(r'^$', ('user1', 'password'))]})
Expand All @@ -402,10 +421,9 @@ def test_auth_header_no_match(app):
with open(app.outdir / "output.json", encoding="utf-8") as fp:
content = json.load(fp)

# TODO: should this test's webserver return HTTP 401 here?
# https://github.com/sphinx-doc/sphinx/issues/11433
assert content["info"] == "403 Client Error: Forbidden for url: http://localhost:7777/"
assert content["status"] == "broken"
# This link is considered working based on the default linkcheck_allow_unauthorized=true
assert content["info"] == "unauthorized"
assert content["status"] == "working"


@pytest.mark.sphinx(
Expand Down