Skip to content

Conversation

julianz-
Copy link
Contributor

@julianz- julianz- commented Sep 21, 2025

This adds improved handling for WantWriteError and WantReadError so that it retries writing the full buffer in BufferedWriter and retries reading in BufferedReader after a short delay allowing for network buffers to settle. The retry logic also makes use of new changes in PyOpenSSL v25.2.0 and Cryptography v45.0.7 that allow for a moving buffer as discussed in #245.


This change is Reviewable

@webknjaz
Copy link
Member

I clicked "rebase" since main should have the pre-commit config fixed.

Copy link

codecov bot commented Sep 26, 2025

❌ 1 Tests Failed:

Tests completed Failed Passed Skipped
218 1 217 13
View the top 1 failed test(s) by shortest run time
cheroot/test/test_ssl.py::test_ssl_env[VerifyMode.CERT_NONE-True-builtin]
Stack Traces | 0.524s run time
self = <urllib3.response.HTTPResponse object at 0x103232c10>

    #x1B[0m#x1B[37m@contextmanager#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
    #x1B[94mdef#x1B[39;49;00m#x1B[90m #x1B[39;49;00m#x1B[92m_error_catcher#x1B[39;49;00m(#x1B[96mself#x1B[39;49;00m) -> typing.Generator[#x1B[94mNone#x1B[39;49;00m]:#x1B[90m#x1B[39;49;00m
    #x1B[90m    #x1B[39;49;00m#x1B[33m"""#x1B[39;49;00m
    #x1B[33m    Catch low-level python exceptions, instead re-raising urllib3#x1B[39;49;00m
    #x1B[33m    variants, so that low-level exceptions are not leaked in the#x1B[39;49;00m
    #x1B[33m    high-level api.#x1B[39;49;00m
    #x1B[33m#x1B[39;49;00m
    #x1B[33m    On exit, release the connection back to the pool.#x1B[39;49;00m
    #x1B[33m    """#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
        clean_exit = #x1B[94mFalse#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
    #x1B[90m#x1B[39;49;00m
        #x1B[94mtry#x1B[39;49;00m:#x1B[90m#x1B[39;49;00m
            #x1B[94mtry#x1B[39;49;00m:#x1B[90m#x1B[39;49;00m
>               #x1B[94myield#x1B[39;49;00m#x1B[90m#x1B[39;49;00m

clean_exit = False
self       = <urllib3.response.HTTPResponse object at 0x103232c10>

#x1B[1m#x1B[31m..../py/lib/python3.9................../site-packages/urllib3/response.py#x1B[0m:779: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
#x1B[1m#x1B[31m..../py/lib/python3.9................../site-packages/urllib3/response.py#x1B[0m:1248: in read_chunked
    #x1B[0m#x1B[96mself#x1B[39;49;00m._update_chunk_length()#x1B[90m#x1B[39;49;00m
        amt        = 10240
        decode_content = True
        self       = <urllib3.response.HTTPResponse object at 0x103232c10>
#x1B[1m#x1B[31m..../py/lib/python3.9................../site-packages/urllib3/response.py#x1B[0m:1167: in _update_chunk_length
    #x1B[0mline = #x1B[96mself#x1B[39;49;00m._fp.fp.readline()  #x1B[90m# type: ignore[union-attr]#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
        self       = <urllib3.response.HTTPResponse object at 0x103232c10>
#x1B[1m#x1B[.../Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/socket.py#x1B[0m:704: in readinto
    #x1B[0m#x1B[94mreturn#x1B[39;49;00m #x1B[96mself#x1B[39;49;00m._sock.recv_into(b)#x1B[90m#x1B[39;49;00m
        b          = <memory at 0x1031bf640>
        self       = <socket.SocketIO object at 0x1031cdf40>
#x1B[1m#x1B[.../Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/ssl.py#x1B[0m:1242: in recv_into
    #x1B[0m#x1B[94mreturn#x1B[39;49;00m #x1B[96mself#x1B[39;49;00m.read(nbytes, buffer)#x1B[90m#x1B[39;49;00m
        __class__  = <class 'ssl.SSLSocket'>
        buffer     = <memory at 0x1031bf640>
        flags      = 0
        nbytes     = 8192
        self       = <ssl.SSLSocket [closed] fd=-1, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0>
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <ssl.SSLSocket [closed] fd=-1, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0>
len = 8192, buffer = <memory at 0x1031bf640>

    #x1B[0m#x1B[94mdef#x1B[39;49;00m#x1B[90m #x1B[39;49;00m#x1B[92mread#x1B[39;49;00m(#x1B[96mself#x1B[39;49;00m, #x1B[96mlen#x1B[39;49;00m=#x1B[94m1024#x1B[39;49;00m, buffer=#x1B[94mNone#x1B[39;49;00m):#x1B[90m#x1B[39;49;00m
    #x1B[90m    #x1B[39;49;00m#x1B[33m"""Read up to LEN bytes and return them.#x1B[39;49;00m
    #x1B[33m    Return zero-length string on EOF."""#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
    #x1B[90m#x1B[39;49;00m
        #x1B[96mself#x1B[39;49;00m._checkClosed()#x1B[90m#x1B[39;49;00m
        #x1B[94mif#x1B[39;49;00m #x1B[96mself#x1B[39;49;00m._sslobj #x1B[95mis#x1B[39;49;00m #x1B[94mNone#x1B[39;49;00m:#x1B[90m#x1B[39;49;00m
            #x1B[94mraise#x1B[39;49;00m #x1B[96mValueError#x1B[39;49;00m(#x1B[33m"#x1B[39;49;00m#x1B[33mRead on closed or unwrapped SSL socket.#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m)#x1B[90m#x1B[39;49;00m
        #x1B[94mtry#x1B[39;49;00m:#x1B[90m#x1B[39;49;00m
            #x1B[94mif#x1B[39;49;00m buffer #x1B[95mis#x1B[39;49;00m #x1B[95mnot#x1B[39;49;00m #x1B[94mNone#x1B[39;49;00m:#x1B[90m#x1B[39;49;00m
>               #x1B[94mreturn#x1B[39;49;00m #x1B[96mself#x1B[39;49;00m._sslobj.read(#x1B[96mlen#x1B[39;49;00m, buffer)#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[31mE               socket.timeout: The read operation timed out#x1B[0m

buffer     = <memory at 0x1031bf640>
len        = 8192
self       = <ssl.SSLSocket [closed] fd=-1, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0>

#x1B[1m#x1B[.../Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/ssl.py#x1B[0m:1100: timeout

#x1B[33mThe above exception was the direct cause of the following exception:#x1B[0m

    #x1B[0m#x1B[94mdef#x1B[39;49;00m#x1B[90m #x1B[39;49;00m#x1B[92mgenerate#x1B[39;49;00m():#x1B[90m#x1B[39;49;00m
        #x1B[90m# Special case for urllib3.#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
        #x1B[94mif#x1B[39;49;00m #x1B[96mhasattr#x1B[39;49;00m(#x1B[96mself#x1B[39;49;00m.raw, #x1B[33m"#x1B[39;49;00m#x1B[33mstream#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m):#x1B[90m#x1B[39;49;00m
            #x1B[94mtry#x1B[39;49;00m:#x1B[90m#x1B[39;49;00m
>               #x1B[94myield from#x1B[39;49;00m #x1B[96mself#x1B[39;49;00m.raw.stream(chunk_size, decode_content=#x1B[94mTrue#x1B[39;49;00m)#x1B[90m#x1B[39;49;00m

chunk_size = 10240
self       = <Response [200]>

#x1B[1m#x1B[31m..../py/lib/python3.9........./site-packages/requests/models.py#x1B[0m:820: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
#x1B[1m#x1B[31m..../py/lib/python3.9................../site-packages/urllib3/response.py#x1B[0m:1088: in stream
    #x1B[0m#x1B[94myield from#x1B[39;49;00m #x1B[96mself#x1B[39;49;00m.read_chunked(amt, decode_content=decode_content)#x1B[90m#x1B[39;49;00m
        amt        = 10240
        decode_content = True
        self       = <urllib3.response.HTTPResponse object at 0x103232c10>
#x1B[1m#x1B[31m..../py/lib/python3.9................../site-packages/urllib3/response.py#x1B[0m:1277: in read_chunked
    #x1B[0m#x1B[96mself#x1B[39;49;00m._original_response.close()#x1B[90m#x1B[39;49;00m
        amt        = 10240
        decode_content = True
        self       = <urllib3.response.HTTPResponse object at 0x103232c10>
#x1B[1m#x1B[.../Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/contextlib.py#x1B[0m:137: in __exit__
    #x1B[0m#x1B[96mself#x1B[39;49;00m.gen.throw(typ, value, traceback)#x1B[90m#x1B[39;49;00m
        self       = <contextlib._GeneratorContextManager object at 0x1031f86d0>
        traceback  = <traceback object at 0x10321cf40>
        typ        = <class 'socket.timeout'>
        value      = timeout('The read operation timed out')
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <urllib3.response.HTTPResponse object at 0x103232c10>

    #x1B[0m#x1B[37m@contextmanager#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
    #x1B[94mdef#x1B[39;49;00m#x1B[90m #x1B[39;49;00m#x1B[92m_error_catcher#x1B[39;49;00m(#x1B[96mself#x1B[39;49;00m) -> typing.Generator[#x1B[94mNone#x1B[39;49;00m]:#x1B[90m#x1B[39;49;00m
    #x1B[90m    #x1B[39;49;00m#x1B[33m"""#x1B[39;49;00m
    #x1B[33m    Catch low-level python exceptions, instead re-raising urllib3#x1B[39;49;00m
    #x1B[33m    variants, so that low-level exceptions are not leaked in the#x1B[39;49;00m
    #x1B[33m    high-level api.#x1B[39;49;00m
    #x1B[33m#x1B[39;49;00m
    #x1B[33m    On exit, release the connection back to the pool.#x1B[39;49;00m
    #x1B[33m    """#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
        clean_exit = #x1B[94mFalse#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
    #x1B[90m#x1B[39;49;00m
        #x1B[94mtry#x1B[39;49;00m:#x1B[90m#x1B[39;49;00m
            #x1B[94mtry#x1B[39;49;00m:#x1B[90m#x1B[39;49;00m
                #x1B[94myield#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
    #x1B[90m#x1B[39;49;00m
            #x1B[94mexcept#x1B[39;49;00m SocketTimeout #x1B[94mas#x1B[39;49;00m e:#x1B[90m#x1B[39;49;00m
                #x1B[90m# FIXME: Ideally we'd like to include the url in the ReadTimeoutError but#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
                #x1B[90m# there is yet no clean way to get at it from this context.#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
>               #x1B[94mraise#x1B[39;49;00m ReadTimeoutError(#x1B[96mself#x1B[39;49;00m._pool, #x1B[94mNone#x1B[39;49;00m, #x1B[33m"#x1B[39;49;00m#x1B[33mRead timed out.#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m) #x1B[94mfrom#x1B[39;49;00m#x1B[90m #x1B[39;49;00m#x1B[04m#x1B[96me#x1B[39;49;00m  #x1B[90m# type: ignore[arg-type]#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[31mE               urllib3.exceptions.ReadTimeoutError: HTTPSConnectionPool(host='127.0.0.1', port=49615): Read timed out.#x1B[0m

clean_exit = False
self       = <urllib3.response.HTTPResponse object at 0x103232c10>

#x1B[1m#x1B[31m..../py/lib/python3.9................../site-packages/urllib3/response.py#x1B[0m:784: ReadTimeoutError

#x1B[33mDuring handling of the above exception, another exception occurred:#x1B[0m

thread_exceptions = [], recwarn = WarningsRecorder(record=True)
mocker = <pytest_mock.plugin.MockerFixture object at 0x102f3a760>
http_request_timeout = 0.2
tls_http_server = functools.partial(<function make_tls_http_server at 0x102bf8c10>, request=<SubRequest 'tls_http_server' for <Function test_ssl_env[VerifyMode.CERT_NONE-True-builtin]>>)
adapter_type = 'builtin', ca = <trustme.CA object at 0x1031df1c0>
tls_verify_mode = <VerifyMode.CERT_NONE: 0>
tls_certificate = <trustme.LeafCert object at 0x102fd7220>
tls_certificate_chain_pem_path = '.../wmf37v850txck86cpnvwm_zw0000gn/T/tmpkm88hl5w.pem'
tls_certificate_private_key_pem_path = '.../wmf37v850txck86cpnvwm_zw0000gn/T/tmp1r5p6_jq.pem'
tls_ca_certificate_pem_path = '.../wmf37v850txck86cpnvwm_zw0000gn/T/tmpe71byauz.pem'
use_client_cert = True

    #x1B[0m#x1B[37m@pytest#x1B[39;49;00m.mark.parametrize(  #x1B[90m# noqa: C901  # FIXME#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
        #x1B[33m'#x1B[39;49;00m#x1B[33madapter_type#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m,#x1B[90m#x1B[39;49;00m
        (#x1B[90m#x1B[39;49;00m
            pytest.param(#x1B[90m#x1B[39;49;00m
                #x1B[33m'#x1B[39;49;00m#x1B[33mbuiltin#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m,#x1B[90m#x1B[39;49;00m
                marks=pytest.mark.xfail(#x1B[90m#x1B[39;49;00m
                    IS_MACOS #x1B[95mand#x1B[39;49;00m PY310_PLUS,#x1B[90m#x1B[39;49;00m
                    reason=#x1B[33m'#x1B[39;49;00m#x1B[33mUnclosed TLS resource warnings happen on macOS #x1B[39;49;00m#x1B[33m'#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
                    #x1B[33m'#x1B[39;49;00m#x1B[33munder Python 3.10 (#508)#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m,#x1B[90m#x1B[39;49;00m
                    strict=#x1B[94mFalse#x1B[39;49;00m,#x1B[90m#x1B[39;49;00m
                ),#x1B[90m#x1B[39;49;00m
            ),#x1B[90m#x1B[39;49;00m
            #x1B[33m'#x1B[39;49;00m#x1B[33mpyopenssl#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m,#x1B[90m#x1B[39;49;00m
        ),#x1B[90m#x1B[39;49;00m
    )#x1B[90m#x1B[39;49;00m
    #x1B[37m@pytest#x1B[39;49;00m.mark.parametrize(#x1B[90m#x1B[39;49;00m
        (#x1B[33m'#x1B[39;49;00m#x1B[33mtls_verify_mode#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m, #x1B[33m'#x1B[39;49;00m#x1B[33muse_client_cert#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m),#x1B[90m#x1B[39;49;00m
        (#x1B[90m#x1B[39;49;00m
            (ssl.CERT_NONE, #x1B[94mFalse#x1B[39;49;00m),#x1B[90m#x1B[39;49;00m
            (ssl.CERT_NONE, #x1B[94mTrue#x1B[39;49;00m),#x1B[90m#x1B[39;49;00m
            (ssl.CERT_OPTIONAL, #x1B[94mFalse#x1B[39;49;00m),#x1B[90m#x1B[39;49;00m
            (ssl.CERT_OPTIONAL, #x1B[94mTrue#x1B[39;49;00m),#x1B[90m#x1B[39;49;00m
            (ssl.CERT_REQUIRED, #x1B[94mTrue#x1B[39;49;00m),#x1B[90m#x1B[39;49;00m
        ),#x1B[90m#x1B[39;49;00m
    )#x1B[90m#x1B[39;49;00m
    #x1B[94mdef#x1B[39;49;00m#x1B[90m #x1B[39;49;00m#x1B[92mtest_ssl_env#x1B[39;49;00m(  #x1B[90m# noqa: C901  # FIXME#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
        thread_exceptions,#x1B[90m#x1B[39;49;00m
        recwarn,#x1B[90m#x1B[39;49;00m
        mocker,#x1B[90m#x1B[39;49;00m
        http_request_timeout,#x1B[90m#x1B[39;49;00m
        tls_http_server,#x1B[90m#x1B[39;49;00m
        adapter_type,#x1B[90m#x1B[39;49;00m
        ca,#x1B[90m#x1B[39;49;00m
        tls_verify_mode,#x1B[90m#x1B[39;49;00m
        tls_certificate,#x1B[90m#x1B[39;49;00m
        tls_certificate_chain_pem_path,#x1B[90m#x1B[39;49;00m
        tls_certificate_private_key_pem_path,#x1B[90m#x1B[39;49;00m
        tls_ca_certificate_pem_path,#x1B[90m#x1B[39;49;00m
        use_client_cert,#x1B[90m#x1B[39;49;00m
    ):#x1B[90m#x1B[39;49;00m
    #x1B[90m    #x1B[39;49;00m#x1B[33m"""Test the SSL environment generated by the SSL adapters."""#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
        interface, _host, port = _get_conn_data(ANY_INTERFACE_IPV4)#x1B[90m#x1B[39;49;00m
    #x1B[90m#x1B[39;49;00m
        #x1B[94mwith#x1B[39;49;00m mocker.mock_module.patch(#x1B[90m#x1B[39;49;00m
            #x1B[33m'#x1B[39;49;00m#x1B[33midna.core.ulabel#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m,#x1B[90m#x1B[39;49;00m
            return_value=ntob(#x1B[33m'#x1B[39;49;00m#x1B[33m127.0.0.1#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m),#x1B[90m#x1B[39;49;00m
        ):#x1B[90m#x1B[39;49;00m
            client_cert = ca.issue_cert(ntou(#x1B[33m'#x1B[39;49;00m#x1B[33m127.0.0.1#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m))#x1B[90m#x1B[39;49;00m
    #x1B[90m#x1B[39;49;00m
        #x1B[94mwith#x1B[39;49;00m client_cert.private_key_and_cert_chain_pem.tempfile() #x1B[94mas#x1B[39;49;00m cl_pem:#x1B[90m#x1B[39;49;00m
            tls_adapter_cls = get_ssl_adapter_class(name=adapter_type)#x1B[90m#x1B[39;49;00m
            tls_adapter = tls_adapter_cls(#x1B[90m#x1B[39;49;00m
                tls_certificate_chain_pem_path,#x1B[90m#x1B[39;49;00m
                tls_certificate_private_key_pem_path,#x1B[90m#x1B[39;49;00m
            )#x1B[90m#x1B[39;49;00m
            #x1B[94mif#x1B[39;49;00m adapter_type == #x1B[33m'#x1B[39;49;00m#x1B[33mpyopenssl#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m:#x1B[90m#x1B[39;49;00m
                tls_adapter.context = tls_adapter.get_context()#x1B[90m#x1B[39;49;00m
                tls_adapter.context.set_verify(#x1B[90m#x1B[39;49;00m
                    _stdlib_to_openssl_verify[tls_verify_mode],#x1B[90m#x1B[39;49;00m
                    #x1B[94mlambda#x1B[39;49;00m conn, cert, errno, depth, preverify_ok: preverify_ok,#x1B[90m#x1B[39;49;00m
                )#x1B[90m#x1B[39;49;00m
            #x1B[94melse#x1B[39;49;00m:#x1B[90m#x1B[39;49;00m
                tls_adapter.context.verify_mode = tls_verify_mode#x1B[90m#x1B[39;49;00m
    #x1B[90m#x1B[39;49;00m
            ca.configure_trust(tls_adapter.context)#x1B[90m#x1B[39;49;00m
            tls_certificate.configure_cert(tls_adapter.context)#x1B[90m#x1B[39;49;00m
    #x1B[90m#x1B[39;49;00m
            tlswsgiserver = tls_http_server((interface, port), tls_adapter)#x1B[90m#x1B[39;49;00m
    #x1B[90m#x1B[39;49;00m
            interface, _host, port = _get_conn_data(tlswsgiserver.bind_addr)#x1B[90m#x1B[39;49;00m
    #x1B[90m#x1B[39;49;00m
>           resp = requests.get(#x1B[90m#x1B[39;49;00m
                #x1B[33m'#x1B[39;49;00m#x1B[33mhttps://#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m + interface + #x1B[33m'#x1B[39;49;00m#x1B[33m:#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m + #x1B[96mstr#x1B[39;49;00m(port) + #x1B[33m'#x1B[39;49;00m#x1B[33m/env#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m,#x1B[90m#x1B[39;49;00m
                timeout=http_request_timeout,#x1B[90m#x1B[39;49;00m
                verify=tls_ca_certificate_pem_path,#x1B[90m#x1B[39;49;00m
                cert=cl_pem #x1B[94mif#x1B[39;49;00m use_client_cert #x1B[94melse#x1B[39;49;00m #x1B[94mNone#x1B[39;49;00m,#x1B[90m#x1B[39;49;00m
            )#x1B[90m#x1B[39;49;00m

_host      = '127.0.0.1'
adapter_type = 'builtin'
ca         = <trustme.CA object at 0x1031df1c0>
cl_pem     = '.../wmf37v850txck86cpnvwm_zw0000gn/T/tmpn14_2z7m.pem'
client_cert = <trustme.LeafCert object at 0x103223d90>
http_request_timeout = 0.2
interface  = '127.0.0.1'
mocker     = <pytest_mock.plugin.MockerFixture object at 0x102f3a760>
port       = 49615
recwarn    = WarningsRecorder(record=True)
thread_exceptions = []
tls_adapter = <cheroot.ssl.builtin.BuiltinSSLAdapter object at 0x103223d30>
tls_adapter_cls = <class 'cheroot.ssl.builtin.BuiltinSSLAdapter'>
tls_ca_certificate_pem_path = '.../wmf37v850txck86cpnvwm_zw0000gn/T/tmpe71byauz.pem'
tls_certificate = <trustme.LeafCert object at 0x102fd7220>
tls_certificate_chain_pem_path = '.../wmf37v850txck86cpnvwm_zw0000gn/T/tmpkm88hl5w.pem'
tls_certificate_private_key_pem_path = '.../wmf37v850txck86cpnvwm_zw0000gn/T/tmp1r5p6_jq.pem'
tls_http_server = functools.partial(<function make_tls_http_server at 0x102bf8c10>, request=<SubRequest 'tls_http_server' for <Function test_ssl_env[VerifyMode.CERT_NONE-True-builtin]>>)
tls_verify_mode = <VerifyMode.CERT_NONE: 0>
tlswsgiserver = <cheroot.server.HTTPServer object at 0x103223d60>
use_client_cert = True

#x1B[1m#x1B[31mcheroot/test/test_ssl.py#x1B[0m:551: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
#x1B[1m#x1B[31m..../py/lib/python3.9....../site-packages/requests/api.py#x1B[0m:73: in get
    #x1B[0m#x1B[94mreturn#x1B[39;49;00m request(#x1B[33m"#x1B[39;49;00m#x1B[33mget#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m, url, params=params, **kwargs)#x1B[90m#x1B[39;49;00m
        kwargs     = {'cert': '.../wmf37v850txck86cpnvwm_zw0000gn/T/tmpn14_2z7m.pem', 'timeout': 0.2, 'verify': '.../wmf37v850txck86cpnvwm_zw0000gn/T/tmpe71byauz.pem'}
        params     = None
        url        = 'https://127.0.0.1:49615/env'
#x1B[1m#x1B[31m..../py/lib/python3.9....../site-packages/requests/api.py#x1B[0m:59: in request
    #x1B[0m#x1B[94mreturn#x1B[39;49;00m session.request(method=method, url=url, **kwargs)#x1B[90m#x1B[39;49;00m
        kwargs     = {'cert': '.../wmf37v850txck86cpnvwm_zw0000gn/T/tmpn14_2z7m.pem', 'params': None, 'timeout': 0.2, 'verify': '.../wmf37v850txck86cpnvwm_zw0000gn/T/tmpe71byauz.pem'}
        method     = 'get'
        session    = <requests.sessions.Session object at 0x1031f4430>
        url        = 'https://127.0.0.1:49615/env'
#x1B[1m#x1B[31m..../py/lib/python3.9....../site-packages/requests/sessions.py#x1B[0m:589: in request
    #x1B[0mresp = #x1B[96mself#x1B[39;49;00m.send(prep, **send_kwargs)#x1B[90m#x1B[39;49;00m
        allow_redirects = True
        auth       = None
        cert       = '.../wmf37v850txck86cpnvwm_zw0000gn/T/tmpn14_2z7m.pem'
        cookies    = None
        data       = None
        files      = None
        headers    = None
        hooks      = None
        json       = None
        method     = 'get'
        params     = None
        prep       = <PreparedRequest [GET]>
        proxies    = {}
        req        = <Request [GET]>
        self       = <requests.sessions.Session object at 0x1031f4430>
        send_kwargs = {'allow_redirects': True, 'cert': '.../wmf37v850txck86cpnvwm_zw0000gn/T/tmpn14_2z7m.pem', 'proxies': OrderedDict(), 'stream': False, ...}
        settings   = {'cert': '.../wmf37v850txck86cpnvwm_zw0000gn/T/tmpn14_2z7m.pem', 'proxies': OrderedDict(), 'stream': False, 'verify': '.../wmf37v850txck86cpnvwm_zw0000gn/T/tmpe71byauz.pem'}
        stream     = None
        timeout    = 0.2
        url        = 'https://127.0.0.1:49615/env'
        verify     = '.../wmf37v850txck86cpnvwm_zw0000gn/T/tmpe71byauz.pem'
#x1B[1m#x1B[31m..../py/lib/python3.9....../site-packages/requests/sessions.py#x1B[0m:746: in send
    #x1B[0mr.content#x1B[90m#x1B[39;49;00m
        adapter    = <requests.adapters.HTTPAdapter object at 0x10320a790>
        allow_redirects = True
        elapsed    = 0.00445103645324707
        gen        = <generator object SessionRedirectMixin.resolve_redirects at 0x1030b8350>
        history    = []
        hooks      = {'response': []}
        kwargs     = {'cert': '.../wmf37v850txck86cpnvwm_zw0000gn/T/tmpn14_2z7m.pem', 'proxies': OrderedDict(), 'stream': False, 'timeout': 0.2, ...}
        r          = <Response [200]>
        request    = <PreparedRequest [GET]>
        self       = <requests.sessions.Session object at 0x1031f4430>
        start      = 1759792538.319616
        stream     = False
#x1B[1m#x1B[31m..../py/lib/python3.9........./site-packages/requests/models.py#x1B[0m:902: in content
    #x1B[0m#x1B[96mself#x1B[39;49;00m._content = #x1B[33mb#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m.join(#x1B[96mself#x1B[39;49;00m.iter_content(CONTENT_CHUNK_SIZE)) #x1B[95mor#x1B[39;49;00m #x1B[33mb#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
        self       = <Response [200]>
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

    #x1B[0m#x1B[94mdef#x1B[39;49;00m#x1B[90m #x1B[39;49;00m#x1B[92mgenerate#x1B[39;49;00m():#x1B[90m#x1B[39;49;00m
        #x1B[90m# Special case for urllib3.#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
        #x1B[94mif#x1B[39;49;00m #x1B[96mhasattr#x1B[39;49;00m(#x1B[96mself#x1B[39;49;00m.raw, #x1B[33m"#x1B[39;49;00m#x1B[33mstream#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m):#x1B[90m#x1B[39;49;00m
            #x1B[94mtry#x1B[39;49;00m:#x1B[90m#x1B[39;49;00m
                #x1B[94myield from#x1B[39;49;00m #x1B[96mself#x1B[39;49;00m.raw.stream(chunk_size, decode_content=#x1B[94mTrue#x1B[39;49;00m)#x1B[90m#x1B[39;49;00m
            #x1B[94mexcept#x1B[39;49;00m ProtocolError #x1B[94mas#x1B[39;49;00m e:#x1B[90m#x1B[39;49;00m
                #x1B[94mraise#x1B[39;49;00m ChunkedEncodingError(e)#x1B[90m#x1B[39;49;00m
            #x1B[94mexcept#x1B[39;49;00m DecodeError #x1B[94mas#x1B[39;49;00m e:#x1B[90m#x1B[39;49;00m
                #x1B[94mraise#x1B[39;49;00m ContentDecodingError(e)#x1B[90m#x1B[39;49;00m
            #x1B[94mexcept#x1B[39;49;00m ReadTimeoutError #x1B[94mas#x1B[39;49;00m e:#x1B[90m#x1B[39;49;00m
>               #x1B[94mraise#x1B[39;49;00m #x1B[96mConnectionError#x1B[39;49;00m(e)#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[31mE               requests.exceptions.ConnectionError: HTTPSConnectionPool(host='127.0.0.1', port=49615): Read timed out.#x1B[0m

chunk_size = 10240
self       = <Response [200]>

#x1B[1m#x1B[31m..../py/lib/python3.9........./site-packages/requests/models.py#x1B[0m:826: ConnectionError

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

@webknjaz
Copy link
Member

@julianz- do you think your thing will let us make use of pyca/pyopenssl#954 ?

@julianz-
Copy link
Contributor Author

@julianz- do you think your thing will let us make use of pyca/pyopenssl#954 ?

I made a comment about the code there but did you want me to add in your change to this PR?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you move unrelated typing updates into a separate PR? Ideally, we should just migrate the annotations from stubs into Python modules. But that'd be another effort. I think pyrefly autotype might help — I've tried it on a portion of a different code base and it was able to inject some types (that required post-processing, though).

@webknjaz
Copy link
Member

@julianz- do you think your thing will let us make use of pyca/pyopenssl#954 ?

I made a comment about the code there but did you want me to add in your change to this PR?

That's a long-standing idea. And I've lost some context over the years. But I think I wanted us to use sendall() instead of reimplementing it on our side. Since you've spend substantial effort in that project, I imagine you're the best person to judge whether this is doable in general and whether this sounds like it should be in the scope of this PR or not.

@julianz- julianz- force-pushed the wantWriteError-fix branch 4 times, most recently from 5c4225d to 828b1b1 Compare September 28, 2025 04:49
@julianz-
Copy link
Contributor Author

@julianz- do you think your thing will let us make use of pyca/pyopenssl#954 ?

I made a comment about the code there but did you want me to add in your change to this PR?

That's a long-standing idea. And I've lost some context over the years. But I think I wanted us to use sendall() instead of reimplementing it on our side. Since you've spend substantial effort in that project, I imagine you're the best person to judge whether this is doable in general and whether this sounds like it should be in the scope of this PR or not.

I think the change there is pretty similar so it might be possible though maybe we should just get this PR done first?

@webknjaz
Copy link
Member

maybe we should just get this PR done first?

Yes, let's do so.

@julianz- julianz- force-pushed the wantWriteError-fix branch 5 times, most recently from 3554f0a to 67137c6 Compare September 30, 2025 06:17
@julianz-
Copy link
Contributor Author

julianz- commented Sep 30, 2025

Some tests fail because the timeouts on SSL tests are too short.
In conftest.py, computed_timeout needs to be higher. 0.5 seems to work fairly well.
I didn't add to this PR. Will create another one for this.

def http_request_timeout():
"""Return a common HTTP request timeout for tests with queries."""
computed_timeout = 0.1

Also saw another timeout failure on test_keepalive_conn_management()
where test_client.server_instance.timeout = 2 was not enough to prevent another failure on the CI.

@julianz- julianz- force-pushed the wantWriteError-fix branch 2 times, most recently from fd4d129 to bbfcc7e Compare September 30, 2025 22:36
@webknjaz
Copy link
Member

webknjaz commented Oct 3, 2025

@julianz- thanks for that PR, I've posted some comments. But could you be more specific when you're talking about the failures your saw — linking specific failing job logs would be helpful for me to understand the context better.

@julianz- julianz- force-pushed the wantWriteError-fix branch 2 times, most recently from 2442063 to c9379be Compare October 6, 2025 23:01
Added handling for WantWriteError and WantReadError in BufferedWriter
and StreamReader to enable retries. This addresses long standing issues
discussed in cherrypy#245. The reliability of the fix relies on using pyOpenSSL
v25.2.0 or greater, as earlier versions have known bugs that affect
the retry logic.
@julianz- julianz- force-pushed the wantWriteError-fix branch from c9379be to 4e4afa4 Compare October 6, 2025 23:01
@hardikmodha
Copy link

@julianz- Thank you for providing a fix for this long-pending issue via this PR. @webknjaz Could you confirm if it is good to be merged?

@webknjaz
Copy link
Member

@hardikmodha it needs some work + more review. We're working on a few other PRs that improve a few more generic things. I imagine Julian and I will get back to this once those PRs are in.

import socket
import time

from OpenSSL import SSL
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this will have to become conditional/guarded since pyOpenSSL is an optional dependency. It might be a good idea to add infra for running the tests w/o optional deps but that certainly out of the scope of this PR.

Comment on lines +49 to +50
# This catches errors like EBADF (Bad File Descriptor)
# or EPIPE (Broken pipe), which indicate the underlying
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These can be made more specific instead of a broad OSError.

):
# these errors require retries with the same data
# regardless of whether data has already been written
continue
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do these leak into the generic writer? Is there an underlying layer where we could catch these and re-raise and common/generic exceptions? Can we scope accessing PyOpenSSL to the pyopenssl.py module?

If not, we'll probably have to have a common var in errors.py but I don't really like leaking this into outer layers. Need to think of a better structure while we're on it.

for _ in range(MAX_ATTEMPTS):
try:
val = super().read(*args, **kwargs)
except (SSL.WantReadError, SSL.WantWriteError) as ssl_want_error:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've basically the same concern with leaking PyOpenSSL into the outer scope here as in https://github.com/cherrypy/cheroot/pull/764/files#r2432889204.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This shouldn't be needed.

@@ -0,0 +1,7 @@
Added handling for WantWriteError and WantReadError in BufferedWriter
and StreamReader to enable retries. This addresses long standing issues
discussed in #245. The reliability of the fix relies on using pyOpenSSL
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use :issue:`245` for refs. It's not Markdown.

Additionally, you could symlink this change note to that number as well so both will be linked in the change log.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Symlink 245.bugfix.rst to this file.

Comment on lines +1 to +2
Added handling for WantWriteError and WantReadError in BufferedWriter
and StreamReader to enable retries. This addresses long standing issues
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All these are linkable in Sphinx. pyOpenSSL is plugged via intersphinx. And internal objects are exposed too.

See these to discover the refs:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants