diff --git a/Doc/library/urllib.error.rst b/Doc/library/urllib.error.rst index b8864e36981ab9b..6a71d4a2ebb6845 100644 --- a/Doc/library/urllib.error.rst +++ b/Doc/library/urllib.error.rst @@ -27,6 +27,10 @@ The following exceptions are raised by :mod:`!urllib.error` as appropriate: :exc:`URLError` used to be a subtype of :exc:`IOError`, which is now an alias of :exc:`OSError`. + .. versionchanged:: 3.16 + The :attr:`~OSError.errno` and :attr:`~OSError.strerror` attributes are + now set from :attr:`reason` when it is an :exc:`OSError`. + .. exception:: HTTPError(url, code, msg, hdrs, fp) diff --git a/Lib/test/test_urllib2.py b/Lib/test/test_urllib2.py index 8c402a2ef0eeedb..37b65e1f0de4528 100644 --- a/Lib/test/test_urllib2.py +++ b/Lib/test/test_urllib2.py @@ -82,6 +82,27 @@ def test_parse_http_list(self): def test_URLError_reasonstr(self): err = urllib.error.URLError('reason') self.assertIn(err.reason, str(err)) + self.assertIsNone(err.errno) + self.assertIsNone(err.strerror) + + def test_URLError_OSError_attributes(self): + # gh-50720: errno and strerror are copied from a wrapped OSError. + socket_error = socket.gaierror(-2, 'Name or service not known') + + class FailingHTTPConnection(MockHTTPClass): + def request(self, *args, **kwargs): + raise socket_error + + h = urllib.request.AbstractHTTPHandler() + req = Request("http://example.com/") + req.timeout = None + + with self.assertRaises(urllib.error.URLError) as cm: + h.do_open(FailingHTTPConnection(), req) + err = cm.exception + self.assertIs(err.reason, socket_error) + self.assertEqual(err.errno, -2) + self.assertEqual(err.strerror, 'Name or service not known') class RequestHdrsTests(unittest.TestCase): diff --git a/Lib/urllib/error.py b/Lib/urllib/error.py index beb32d0df48610d..5020a60f1ef9bd0 100644 --- a/Lib/urllib/error.py +++ b/Lib/urllib/error.py @@ -27,6 +27,11 @@ def __init__(self, reason, filename=None): self.reason = reason if filename is not None: self.filename = filename + if isinstance(reason, OSError): + # OSError.__init__() is not called, so copy the attributes + # it would have set from the wrapped error (gh-50720). + self.errno = reason.errno + self.strerror = reason.strerror def __str__(self): return '' % self.reason diff --git a/Misc/NEWS.d/next/Library/2026-10-29-20-52-57.gh-issue-50720.Kd1VMR.rst b/Misc/NEWS.d/next/Library/2026-10-29-20-52-57.gh-issue-50720.Kd1VMR.rst new file mode 100644 index 000000000000000..ddb4377c3c25987 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-10-29-20-52-57.gh-issue-50720.Kd1VMR.rst @@ -0,0 +1,3 @@ +:exc:`urllib.error.URLError` now sets the :attr:`~OSError.errno` and +:attr:`~OSError.strerror` attributes from +:attr:`~urllib.error.URLError.reason` when it is an :exc:`OSError`.