Skip to content

Conversation

manthey
Copy link

@manthey manthey commented Oct 2, 2025

In Python < 3.13, the code backfills a shutdown method for a queue, but this isn't complete; an existing get call is not interrupted by shutdown and this means that when the unservicable queue is empty, it never stops, preventing the system from stopping. By daemonizing the unservicable thread, the server can stop and exit.

A better solution would probably involve copying a fair amount of code from Python 3.13's queue implementation. In my opinion that isn't worth the maintenance burden. If desired, perhaps we should only daemonize this thread on Python < 3.13.

What kind of change does this PR introduce?

  • 🐞 bug fix
  • 🐣 feature
  • 📋 docs update
  • 📋 tests/coverage improvement
  • 📋 refactoring
  • 💥 other

📋 What is the related issue number (starting with #)

Resolves #769

What is the current behavior? (You can also link to an open issue here)

It is impossible to stop a server that has no pending unservicable requests on Python < 3.13.

What is the new behavior (if this is a feature change)?

You can stop a server that has no pending unservicable requests.


This change is Reviewable

In Python < 3.13, the code backfills a shutdown method for a queue, but
this isn't complete; an existing get call is not interrupted by shutdown
and this means that when the unservicable queue is empty, it never
stops, preventing the system from stopping.  By daemonizing the
unservicable thread, the server can stop and exit.

A better solution would probably involve copying a fair amount of code
from Python 3.13's queue implementation.  In my opinion that isn't worth
the maintenance burden.  If desired, perhaps we should only daemonize
this thread on Python < 3.13.

Signed-off-by: David Manthey <[email protected]>
Copy link

codecov bot commented Oct 2, 2025

❌ 1 Tests Failed:

Tests completed Failed Passed Skipped
212 1 211 15
View the full list of 3 ❄️ flaky test(s)
cheroot\test\test_conn.py::test::test_conn::test_Content_Length_out[/wrong_cl_unbuffered-200-I too]

Flake rate in main: 11.11% (Passed 8 times, Failed 1 times)

Stack Traces | 0.002s run time
self = <cheroot.server.HTTPConnection object at 0x000002D280403D90>

    #x1B[0m#x1B[94mdef#x1B[39;49;00m#x1B[90m #x1B[39;49;00m#x1B[92mcommunicate#x1B[39;49;00m(#x1B[96mself#x1B[39;49;00m):  #x1B[90m# noqa: C901  # FIXME#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
    #x1B[90m    #x1B[39;49;00m#x1B[33m"""Read each request and respond appropriately.#x1B[39;49;00m
    #x1B[33m#x1B[39;49;00m
    #x1B[33m    Returns true if the connection should be kept open.#x1B[39;49;00m
    #x1B[33m    """#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
        request_seen = #x1B[94mFalse#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
        #x1B[94mtry#x1B[39;49;00m:#x1B[90m#x1B[39;49;00m
            req = #x1B[96mself#x1B[39;49;00m.RequestHandlerClass(#x1B[96mself#x1B[39;49;00m.server, #x1B[96mself#x1B[39;49;00m)#x1B[90m#x1B[39;49;00m
            req.parse_request()#x1B[90m#x1B[39;49;00m
            #x1B[94mif#x1B[39;49;00m #x1B[96mself#x1B[39;49;00m.server.stats[#x1B[33m'#x1B[39;49;00m#x1B[33mEnabled#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m]:#x1B[90m#x1B[39;49;00m
                #x1B[96mself#x1B[39;49;00m.requests_seen += #x1B[94m1#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
            #x1B[94mif#x1B[39;49;00m #x1B[95mnot#x1B[39;49;00m req.ready:#x1B[90m#x1B[39;49;00m
                #x1B[90m# Something went wrong in the parsing (and the server has#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
                #x1B[90m# probably already made a simple_response). Return and#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
                #x1B[90m# let the conn close.#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
                #x1B[94mreturn#x1B[39;49;00m #x1B[94mFalse#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
    #x1B[90m#x1B[39;49;00m
            request_seen = #x1B[94mTrue#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
>           req.respond()#x1B[90m#x1B[39;49;00m

req        = <cheroot.server.HTTPRequest object at 0x000002D2804B4C30>
request_seen = True
self       = <cheroot.server.HTTPConnection object at 0x000002D280403D90>

#x1B[1m#x1B[31mcheroot\server.py#x1B[0m:1347: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
#x1B[1m#x1B[31mcheroot\server.py#x1B[0m:1138: in respond
    #x1B[0m#x1B[96mself#x1B[39;49;00m.server.gateway(#x1B[96mself#x1B[39;49;00m).respond()#x1B[90m#x1B[39;49;00m
        cl         = 0
        mrbs       = 1001
        self       = <cheroot.server.HTTPRequest object at 0x000002D2804B4C30>
#x1B[1m#x1B[31mcheroot\wsgi.py#x1B[0m:153: in respond
    #x1B[0m#x1B[96mself#x1B[39;49;00m.write(chunk)#x1B[90m#x1B[39;49;00m
        chunk      = b' have too many bytes'
        response   = [b'I too', b' have too many bytes']
        self       = <cheroot.wsgi.Gateway_10 object at 0x000002D2804B4E90>
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <cheroot.wsgi.Gateway_10 object at 0x000002D2804B4E90>, chunk = b' have'

    #x1B[0m#x1B[94mdef#x1B[39;49;00m#x1B[90m #x1B[39;49;00m#x1B[92mwrite#x1B[39;49;00m(#x1B[96mself#x1B[39;49;00m, chunk):#x1B[90m#x1B[39;49;00m
    #x1B[90m    #x1B[39;49;00m#x1B[33m"""WSGI callable to write unbuffered data to the client.#x1B[39;49;00m
    #x1B[33m#x1B[39;49;00m
    #x1B[33m    This method is also used internally by start_response (to write#x1B[39;49;00m
    #x1B[33m    data from the iterable returned by the WSGI application).#x1B[39;49;00m
    #x1B[33m    """#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
        #x1B[94mif#x1B[39;49;00m #x1B[95mnot#x1B[39;49;00m #x1B[96mself#x1B[39;49;00m.started_response:#x1B[90m#x1B[39;49;00m
            #x1B[94mraise#x1B[39;49;00m #x1B[96mRuntimeError#x1B[39;49;00m(#x1B[33m'#x1B[39;49;00m#x1B[33mWSGI write called before start_response.#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m)#x1B[90m#x1B[39;49;00m
    #x1B[90m#x1B[39;49;00m
        chunklen = #x1B[96mlen#x1B[39;49;00m(chunk)#x1B[90m#x1B[39;49;00m
        rbo = #x1B[96mself#x1B[39;49;00m.remaining_bytes_out#x1B[90m#x1B[39;49;00m
        #x1B[94mif#x1B[39;49;00m rbo #x1B[95mis#x1B[39;49;00m #x1B[95mnot#x1B[39;49;00m #x1B[94mNone#x1B[39;49;00m #x1B[95mand#x1B[39;49;00m chunklen > rbo:#x1B[90m#x1B[39;49;00m
            #x1B[94mif#x1B[39;49;00m #x1B[95mnot#x1B[39;49;00m #x1B[96mself#x1B[39;49;00m.req.sent_headers:#x1B[90m#x1B[39;49;00m
                #x1B[90m# Whew. We can send a 500 to the client.#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
                #x1B[96mself#x1B[39;49;00m.req.simple_response(#x1B[90m#x1B[39;49;00m
                    #x1B[33m'#x1B[39;49;00m#x1B[33m500 Internal Server Error#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m,#x1B[90m#x1B[39;49;00m
                    #x1B[33m'#x1B[39;49;00m#x1B[33mThe requested resource returned more bytes than the #x1B[39;49;00m#x1B[33m'#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
                    #x1B[33m'#x1B[39;49;00m#x1B[33mdeclared Content-Length.#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m,#x1B[90m#x1B[39;49;00m
                )#x1B[90m#x1B[39;49;00m
            #x1B[94melse#x1B[39;49;00m:#x1B[90m#x1B[39;49;00m
                #x1B[90m# Dang. We have probably already sent data. Truncate the chunk#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
                #x1B[90m# to fit (so the client doesn't hang) and raise an error later.#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
                chunk = chunk[:rbo]#x1B[90m#x1B[39;49;00m
    #x1B[90m#x1B[39;49;00m
        #x1B[96mself#x1B[39;49;00m.req.ensure_headers_sent()#x1B[90m#x1B[39;49;00m
    #x1B[90m#x1B[39;49;00m
        #x1B[96mself#x1B[39;49;00m.req.write(chunk)#x1B[90m#x1B[39;49;00m
    #x1B[90m#x1B[39;49;00m
        #x1B[94mif#x1B[39;49;00m rbo #x1B[95mis#x1B[39;49;00m #x1B[95mnot#x1B[39;49;00m #x1B[94mNone#x1B[39;49;00m:#x1B[90m#x1B[39;49;00m
            rbo -= chunklen#x1B[90m#x1B[39;49;00m
            #x1B[94mif#x1B[39;49;00m rbo < #x1B[94m0#x1B[39;49;00m:#x1B[90m#x1B[39;49;00m
>               #x1B[94mraise#x1B[39;49;00m #x1B[96mValueError#x1B[39;49;00m(#x1B[90m#x1B[39;49;00m
                    #x1B[33m'#x1B[39;49;00m#x1B[33mResponse body exceeds the declared Content-Length.#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m,#x1B[90m#x1B[39;49;00m
                )#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[31mE               ValueError: Response body exceeds the declared Content-Length.#x1B[0m

chunk      = b' have'
chunklen   = 20
rbo        = -15
self       = <cheroot.wsgi.Gateway_10 object at 0x000002D2804B4E90>

#x1B[1m#x1B[31mcheroot\wsgi.py#x1B[0m:239: ValueError

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

self = <cheroot.makefile.StreamWriter name=-1>

    #x1B[0m#x1B[94mdef#x1B[39;49;00m#x1B[90m #x1B[39;49;00m#x1B[92m__del__#x1B[39;49;00m(#x1B[96mself#x1B[39;49;00m):#x1B[90m#x1B[39;49;00m
    #x1B[90m    #x1B[39;49;00m#x1B[33m"""Destructor.  Calls close()."""#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
        #x1B[94mtry#x1B[39;49;00m:#x1B[90m#x1B[39;49;00m
            closed = #x1B[96mself#x1B[39;49;00m.closed#x1B[90m#x1B[39;49;00m
        #x1B[94mexcept#x1B[39;49;00m #x1B[96mAttributeError#x1B[39;49;00m:#x1B[90m#x1B[39;49;00m
            #x1B[90m# If getting closed fails, then the object is probably#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
            #x1B[90m# in an unusable state, so ignore.#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
            #x1B[94mreturn#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
    #x1B[90m#x1B[39;49;00m
        #x1B[94mif#x1B[39;49;00m closed:#x1B[90m#x1B[39;49;00m
            #x1B[94mreturn#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
    #x1B[90m#x1B[39;49;00m
        #x1B[94mif#x1B[39;49;00m dealloc_warn := #x1B[96mgetattr#x1B[39;49;00m(#x1B[96mself#x1B[39;49;00m, #x1B[33m"#x1B[39;49;00m#x1B[33m_dealloc_warn#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m, #x1B[94mNone#x1B[39;49;00m):#x1B[90m#x1B[39;49;00m
            dealloc_warn(#x1B[96mself#x1B[39;49;00m)#x1B[90m#x1B[39;49;00m
    #x1B[90m#x1B[39;49;00m
        #x1B[90m# If close() fails, the caller logs the exception with#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
        #x1B[90m# sys.unraisablehook. close() must be called at the end at __del__().#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
>       #x1B[96mself#x1B[39;49;00m.close()#x1B[90m#x1B[39;49;00m

closed     = False
dealloc_warn = <bound method _BufferedIOMixin._dealloc_warn of <cheroot.makefile.StreamWriter name=-1>>
self       = <cheroot.makefile.StreamWriter name=-1>

#x1B[1m#x1B[31mC:\hostedtoolcache\windows\Python\3.14.0-rc.3\x64\Lib\_pyio.py#x1B[0m:415: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
#x1B[1m#x1B[31mC:\hostedtoolcache\windows\Python\3.14.0-rc.3\x64\Lib\_pyio.py#x1B[0m:1310: in close
    #x1B[0m#x1B[96mself#x1B[39;49;00m.flush()#x1B[90m#x1B[39;49;00m
        self       = <cheroot.makefile.StreamWriter name=-1>
#x1B[1m#x1B[31mC:\hostedtoolcache\windows\Python\3.14.0-rc.3\x64\Lib\_pyio.py#x1B[0m:1271: in flush
    #x1B[0m#x1B[96mself#x1B[39;49;00m._flush_unlocked()#x1B[90m#x1B[39;49;00m
        self       = <cheroot.makefile.StreamWriter name=-1>
#x1B[1m#x1B[31mcheroot\makefile.py#x1B[0m:32: in _flush_unlocked
    #x1B[0mn = #x1B[96mself#x1B[39;49;00m.raw.write(#x1B[96mbytes#x1B[39;49;00m(#x1B[96mself#x1B[39;49;00m._write_buf))#x1B[90m#x1B[39;49;00m
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^#x1B[90m#x1B[39;49;00m
        self       = <cheroot.makefile.StreamWriter name=-1>
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <socket.SocketIO object at 0x000002D2801EF430>
b = b'I have too many bytes'

    #x1B[0m#x1B[94mdef#x1B[39;49;00m#x1B[90m #x1B[39;49;00m#x1B[92mwrite#x1B[39;49;00m(#x1B[96mself#x1B[39;49;00m, b):#x1B[90m#x1B[39;49;00m
    #x1B[90m    #x1B[39;49;00m#x1B[33m"""Write the given bytes or bytearray object *b* to the socket#x1B[39;49;00m
    #x1B[33m    and return the number of bytes written.  This can be less than#x1B[39;49;00m
    #x1B[33m    len(b) if not all data could be written.  If the socket is#x1B[39;49;00m
    #x1B[33m    non-blocking and no bytes could be written None is returned.#x1B[39;49;00m
    #x1B[33m    """#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
        #x1B[96mself#x1B[39;49;00m._checkClosed()#x1B[90m#x1B[39;49;00m
        #x1B[96mself#x1B[39;49;00m._checkWritable()#x1B[90m#x1B[39;49;00m
        #x1B[94mtry#x1B[39;49;00m:#x1B[90m#x1B[39;49;00m
>           #x1B[94mreturn#x1B[39;49;00m #x1B[96mself#x1B[39;49;00m._sock.send(b)#x1B[90m#x1B[39;49;00m
                   ^^^^^^^^^^^^^^^^^^#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[31mE           OSError: [WinError 10038] An operation was attempted on something that is not a socket#x1B[0m

b          = b'I have too many bytes'
self       = <socket.SocketIO object at 0x000002D2801EF430>

#x1B[1m#x1B[31mC:\hostedtoolcache\windows\Python\3.14.0-rc.3\x64\Lib\socket.py#x1B[0m:743: OSError

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

cls = <class '_pytest.runner.CallInfo'>
func = <function call_and_report.<locals>.<lambda> at 0x000002D2806E9DD0>
when = 'teardown'
reraise = (<class '_pytest.outcomes.Exit'>, <class 'KeyboardInterrupt'>)

    #x1B[0m#x1B[37m@classmethod#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
    #x1B[94mdef#x1B[39;49;00m#x1B[90m #x1B[39;49;00m#x1B[92mfrom_call#x1B[39;49;00m(#x1B[90m#x1B[39;49;00m
        #x1B[96mcls#x1B[39;49;00m,#x1B[90m#x1B[39;49;00m
        func: Callable[[], TResult],#x1B[90m#x1B[39;49;00m
        when: Literal[#x1B[33m"#x1B[39;49;00m#x1B[33mcollect#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m, #x1B[33m"#x1B[39;49;00m#x1B[33msetup#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m, #x1B[33m"#x1B[39;49;00m#x1B[33mcall#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m, #x1B[33m"#x1B[39;49;00m#x1B[33mteardown#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m],#x1B[90m#x1B[39;49;00m
        reraise: #x1B[96mtype#x1B[39;49;00m[#x1B[96mBaseException#x1B[39;49;00m] | #x1B[96mtuple#x1B[39;49;00m[#x1B[96mtype#x1B[39;49;00m[#x1B[96mBaseException#x1B[39;49;00m], ...] | #x1B[94mNone#x1B[39;49;00m = #x1B[94mNone#x1B[39;49;00m,#x1B[90m#x1B[39;49;00m
    ) -> CallInfo[TResult]:#x1B[90m#x1B[39;49;00m
    #x1B[90m    #x1B[39;49;00m#x1B[33m"""Call func, wrapping the result in a CallInfo.#x1B[39;49;00m
    #x1B[33m#x1B[39;49;00m
    #x1B[33m    :param func:#x1B[39;49;00m
    #x1B[33m        The function to call. Called without arguments.#x1B[39;49;00m
    #x1B[33m    :type func: Callable[[], _pytest.runner.TResult]#x1B[39;49;00m
    #x1B[33m    :param when:#x1B[39;49;00m
    #x1B[33m        The phase in which the function is called.#x1B[39;49;00m
    #x1B[33m    :param reraise:#x1B[39;49;00m
    #x1B[33m        Exception or exceptions that shall propagate if raised by the#x1B[39;49;00m
    #x1B[33m        function, instead of being wrapped in the CallInfo.#x1B[39;49;00m
    #x1B[33m    """#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
        excinfo = #x1B[94mNone#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
        instant = timing.Instant()#x1B[90m#x1B[39;49;00m
        #x1B[94mtry#x1B[39;49;00m:#x1B[90m#x1B[39;49;00m
>           result: TResult | #x1B[94mNone#x1B[39;49;00m = func()#x1B[90m#x1B[39;49;00m
                                     ^^^^^^#x1B[90m#x1B[39;49;00m

cls        = <class '_pytest.runner.CallInfo'>
duration   = Duration(start=Instant(time=1759516484.1506557, perf_count=1426.0051067), stop=Instant(time=1759516484.2169213, perf_count=1426.0713733))
excinfo    = <ExceptionInfo PytestUnraisableExceptionWarning('Exception ignored while calling deallocator <function IOBase.__del__ at 0x000002D2FDE8A8D0>: None\n') tblen=13>
func       = <function call_and_report.<locals>.<lambda> at 0x000002D2806E9DD0>
instant    = Instant(time=1759516484.1506557, perf_count=1426.0051067)
reraise    = (<class '_pytest.outcomes.Exit'>, <class 'KeyboardInterrupt'>)
result     = None
when       = 'teardown'

#x1B[1m#x1B[31m.tox\py\Lib\site-packages\_pytest\runner.py#x1B[0m:344: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
#x1B[1m#x1B[31m.tox\py\Lib\site-packages\_pytest\runner.py#x1B[0m:246: in <lambda>
    #x1B[0m#x1B[94mlambda#x1B[39;49;00m: runtest_hook(item=item, **kwds), when=when, reraise=reraise#x1B[90m#x1B[39;49;00m
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^#x1B[90m#x1B[39;49;00m
        item       = <Function test_Content_Length_out[/wrong_cl_unbuffered-200-I too]>
        kwds       = {'nextitem': <Function test_598>}
        runtest_hook = <HookCaller 'pytest_runtest_teardown'>
#x1B[1m#x1B[31m.tox\py\Lib\site-packages\pluggy\_hooks.py#x1B[0m:512: in __call__
    #x1B[0m#x1B[94mreturn#x1B[39;49;00m #x1B[96mself#x1B[39;49;00m._hookexec(#x1B[96mself#x1B[39;49;00m.name, #x1B[96mself#x1B[39;49;00m._hookimpls.copy(), kwargs, firstresult)#x1B[90m#x1B[39;49;00m
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^#x1B[90m#x1B[39;49;00m
        firstresult = False
        kwargs     = {'item': <Function test_Content_Length_out[/wrong_cl_unbuffered-200-I too]>, 'nextitem': <Function test_598>}
        self       = <HookCaller 'pytest_runtest_teardown'>
#x1B[1m#x1B[31m.tox\py\Lib\site-packages\pluggy\_manager.py#x1B[0m:120: in _hookexec
    #x1B[0m#x1B[94mreturn#x1B[39;49;00m #x1B[96mself#x1B[39;49;00m._inner_hookexec(hook_name, methods, kwargs, firstresult)#x1B[90m#x1B[39;49;00m
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^#x1B[90m#x1B[39;49;00m
        firstresult = False
        hook_name  = 'pytest_runtest_teardown'
        kwargs     = {'item': <Function test_Content_Length_out[/wrong_cl_unbuffered-200-I too]>, 'nextitem': <Function test_598>}
        methods    = [<HookImpl plugin_name='threadexception', plugin=<module '_pytest.threadexception' from 'D:\\a\\cheroot\\cheroot\\.tox...tIOWrapper name='nul' mode='r' encoding='utf-8'>> _state='suspended' _in_suspended=False> _capture_fixture=None>>, ...]
        self       = <_pytest.config.PytestPluginManager object at 0x000002D2FC9CBB60>
#x1B[1m#x1B[31m.tox\py\Lib\site-packages\_pytest\logging.py#x1B[0m:858: in pytest_runtest_teardown
    #x1B[0m#x1B[94myield#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
        item       = <Function test_Content_Length_out[/wrong_cl_unbuffered-200-I too]>
        self       = <_pytest.logging.LoggingPlugin object at 0x000002D2FE04F620>
#x1B[1m#x1B[31m.tox\py\Lib\site-packages\_pytest\capture.py#x1B[0m:905: in pytest_runtest_teardown
    #x1B[0m#x1B[94mreturn#x1B[39;49;00m (#x1B[94myield#x1B[39;49;00m)#x1B[90m#x1B[39;49;00m
            ^^^^^#x1B[90m#x1B[39;49;00m
        item       = <Function test_Content_Length_out[/wrong_cl_unbuffered-200-I too]>
        self       = <CaptureManager _method='fd' _global_capturing=<MultiCapture out=<FDCapture 1 oldfd=8 _state='suspended' tmpfile=<Enco..._io.TextIOWrapper name='nul' mode='r' encoding='utf-8'>> _state='suspended' _in_suspended=False> _capture_fixture=None>
#x1B[1m#x1B[31m.tox\py\Lib\site-packages\_pytest\unraisableexception.py#x1B[0m:163: in pytest_runtest_teardown
    #x1B[0mcollect_unraisable(item.config)#x1B[90m#x1B[39;49;00m
        item       = <Function test_Content_Length_out[/wrong_cl_unbuffered-200-I too]>
#x1B[1m#x1B[31m.tox\py\Lib\site-packages\_pytest\unraisableexception.py#x1B[0m:79: in collect_unraisable
    #x1B[0m#x1B[94mraise#x1B[39;49;00m errors[#x1B[94m0#x1B[39;49;00m]#x1B[90m#x1B[39;49;00m
        config     = <_pytest.config.Config object at 0x000002D2FD325160>
        msg        = 'Exception ignored while calling deallocator <function IOBase.__del__ at 0x000002D2FDE8A8D0>: None\n\nTraceback (most ...        ~~~~~~~~~~~~~~~^^^\nOSError: [WinError 10038] An operation was attempted on something that is not a socket\n\n'
        pop_unraisable = <built-in method pop of collections.deque object at 0x000002D2FE02E020>
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

config = <_pytest.config.Config object at 0x000002D2FD325160>

    #x1B[0m#x1B[94mdef#x1B[39;49;00m#x1B[90m #x1B[39;49;00m#x1B[92mcollect_unraisable#x1B[39;49;00m(config: Config) -> #x1B[94mNone#x1B[39;49;00m:#x1B[90m#x1B[39;49;00m
        pop_unraisable = config.stash[unraisable_exceptions].pop#x1B[90m#x1B[39;49;00m
        errors: #x1B[96mlist#x1B[39;49;00m[pytest.PytestUnraisableExceptionWarning | #x1B[96mRuntimeError#x1B[39;49;00m] = []#x1B[90m#x1B[39;49;00m
        meta = #x1B[94mNone#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
        hook_error = #x1B[94mNone#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
        #x1B[94mtry#x1B[39;49;00m:#x1B[90m#x1B[39;49;00m
            #x1B[94mwhile#x1B[39;49;00m #x1B[94mTrue#x1B[39;49;00m:#x1B[90m#x1B[39;49;00m
                #x1B[94mtry#x1B[39;49;00m:#x1B[90m#x1B[39;49;00m
                    meta = pop_unraisable()#x1B[90m#x1B[39;49;00m
                #x1B[94mexcept#x1B[39;49;00m #x1B[96mIndexError#x1B[39;49;00m:#x1B[90m#x1B[39;49;00m
                    #x1B[94mbreak#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
    #x1B[90m#x1B[39;49;00m
                #x1B[94mif#x1B[39;49;00m #x1B[96misinstance#x1B[39;49;00m(meta, #x1B[96mBaseException#x1B[39;49;00m):#x1B[90m#x1B[39;49;00m
                    hook_error = #x1B[96mRuntimeError#x1B[39;49;00m(#x1B[33m"#x1B[39;49;00m#x1B[33mFailed to process unraisable exception#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m)#x1B[90m#x1B[39;49;00m
                    hook_error.__cause__ = meta#x1B[90m#x1B[39;49;00m
                    errors.append(hook_error)#x1B[90m#x1B[39;49;00m
                    #x1B[94mcontinue#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
    #x1B[90m#x1B[39;49;00m
                msg = meta.msg#x1B[90m#x1B[39;49;00m
                #x1B[94mtry#x1B[39;49;00m:#x1B[90m#x1B[39;49;00m
>                   warnings.warn(pytest.PytestUnraisableExceptionWarning(msg))#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[31mE                   pytest.PytestUnraisableExceptionWarning: Exception ignored while calling deallocator <function IOBase.__del__ at 0x000002D2FDE8A8D0>: None#x1B[0m

config     = <_pytest.config.Config object at 0x000002D2FD325160>
msg        = 'Exception ignored while calling deallocator <function IOBase.__del__ at 0x000002D2FDE8A8D0>: None\n\nTraceback (most ...        ~~~~~~~~~~~~~~~^^^\nOSError: [WinError 10038] An operation was attempted on something that is not a socket\n\n'
pop_unraisable = <built-in method pop of collections.deque object at 0x000002D2FE02E020>

#x1B[1m#x1B[31m.tox\py\Lib\site-packages\_pytest\unraisableexception.py#x1B[0m:67: PytestUnraisableExceptionWarning
cheroot\test\test_ssl.py::test::test_ssl::test_http_over_https_error[0.0.0.0-builtin]

Flake rate in main: 33.33% (Passed 8 times, Failed 4 times)

Stack Traces | 0.304s run time
self = <cheroot.makefile.StreamWriter name=-1>

    #x1B[0m#x1B[94mdef#x1B[39;49;00m#x1B[90m #x1B[39;49;00m#x1B[92m__del__#x1B[39;49;00m(#x1B[96mself#x1B[39;49;00m):#x1B[90m#x1B[39;49;00m
    #x1B[90m    #x1B[39;49;00m#x1B[33m"""Destructor.  Calls close()."""#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
        #x1B[94mtry#x1B[39;49;00m:#x1B[90m#x1B[39;49;00m
            closed = #x1B[96mself#x1B[39;49;00m.closed#x1B[90m#x1B[39;49;00m
        #x1B[94mexcept#x1B[39;49;00m #x1B[96mAttributeError#x1B[39;49;00m:#x1B[90m#x1B[39;49;00m
            #x1B[90m# If getting closed fails, then the object is probably#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
            #x1B[90m# in an unusable state, so ignore.#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
            #x1B[94mreturn#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
    #x1B[90m#x1B[39;49;00m
        #x1B[94mif#x1B[39;49;00m closed:#x1B[90m#x1B[39;49;00m
            #x1B[94mreturn#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
    #x1B[90m#x1B[39;49;00m
        #x1B[94mif#x1B[39;49;00m dealloc_warn := #x1B[96mgetattr#x1B[39;49;00m(#x1B[96mself#x1B[39;49;00m, #x1B[33m"#x1B[39;49;00m#x1B[33m_dealloc_warn#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m, #x1B[94mNone#x1B[39;49;00m):#x1B[90m#x1B[39;49;00m
            dealloc_warn(#x1B[96mself#x1B[39;49;00m)#x1B[90m#x1B[39;49;00m
    #x1B[90m#x1B[39;49;00m
        #x1B[90m# If close() fails, the caller logs the exception with#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
        #x1B[90m# sys.unraisablehook. close() must be called at the end at __del__().#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
>       #x1B[96mself#x1B[39;49;00m.close()#x1B[90m#x1B[39;49;00m

closed     = False
dealloc_warn = <bound method _BufferedIOMixin._dealloc_warn of <cheroot.makefile.StreamWriter name=-1>>
self       = <cheroot.makefile.StreamWriter name=-1>

#x1B[1m#x1B[31mC:\hostedtoolcache\windows\Python\3.14.0-rc.3\x64\Lib\_pyio.py#x1B[0m:415: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
#x1B[1m#x1B[31mC:\hostedtoolcache\windows\Python\3.14.0-rc.3\x64\Lib\_pyio.py#x1B[0m:1310: in close
    #x1B[0m#x1B[96mself#x1B[39;49;00m.flush()#x1B[90m#x1B[39;49;00m
        self       = <cheroot.makefile.StreamWriter name=-1>
#x1B[1m#x1B[31mC:\hostedtoolcache\windows\Python\3.14.0-rc.3\x64\Lib\_pyio.py#x1B[0m:1271: in flush
    #x1B[0m#x1B[96mself#x1B[39;49;00m._flush_unlocked()#x1B[90m#x1B[39;49;00m
        self       = <cheroot.makefile.StreamWriter name=-1>
#x1B[1m#x1B[31mcheroot\makefile.py#x1B[0m:32: in _flush_unlocked
    #x1B[0mn = #x1B[96mself#x1B[39;49;00m.raw.write(#x1B[96mbytes#x1B[39;49;00m(#x1B[96mself#x1B[39;49;00m._write_buf))#x1B[90m#x1B[39;49;00m
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^#x1B[90m#x1B[39;49;00m
        self       = <cheroot.makefile.StreamWriter name=-1>
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <socket.SocketIO object at 0x0000013454E286A0>
b = b'HTTP/1.1 400 Bad Request\r\nContent-Length: 85\r\nContent-Type: text/plain\r\n\r\nThe client sent a plain HTTP request, but this server only speaks HTTPS on this port.'

    #x1B[0m#x1B[94mdef#x1B[39;49;00m#x1B[90m #x1B[39;49;00m#x1B[92mwrite#x1B[39;49;00m(#x1B[96mself#x1B[39;49;00m, b):#x1B[90m#x1B[39;49;00m
    #x1B[90m    #x1B[39;49;00m#x1B[33m"""Write the given bytes or bytearray object *b* to the socket#x1B[39;49;00m
    #x1B[33m    and return the number of bytes written.  This can be less than#x1B[39;49;00m
    #x1B[33m    len(b) if not all data could be written.  If the socket is#x1B[39;49;00m
    #x1B[33m    non-blocking and no bytes could be written None is returned.#x1B[39;49;00m
    #x1B[33m    """#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
        #x1B[96mself#x1B[39;49;00m._checkClosed()#x1B[90m#x1B[39;49;00m
        #x1B[96mself#x1B[39;49;00m._checkWritable()#x1B[90m#x1B[39;49;00m
        #x1B[94mtry#x1B[39;49;00m:#x1B[90m#x1B[39;49;00m
>           #x1B[94mreturn#x1B[39;49;00m #x1B[96mself#x1B[39;49;00m._sock.send(b)#x1B[90m#x1B[39;49;00m
                   ^^^^^^^^^^^^^^^^^^#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[31mE           OSError: [WinError 10038] An operation was attempted on something that is not a socket#x1B[0m

b          = b'HTTP/1.1 400 Bad Request\r\nContent-Length: 85\r\nContent-Type: text/plain\r\n\r\nThe client sent a plain HTTP request, but this server only speaks HTTPS on this port.'
self       = <socket.SocketIO object at 0x0000013454E286A0>

#x1B[1m#x1B[31mC:\hostedtoolcache\windows\Python\3.14.0-rc.3\x64\Lib\socket.py#x1B[0m:743: OSError

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

cls = <class '_pytest.runner.CallInfo'>
func = <function call_and_report.<locals>.<lambda> at 0x00000134560EC5C0>
when = 'teardown'
reraise = (<class '_pytest.outcomes.Exit'>, <class 'KeyboardInterrupt'>)

    #x1B[0m#x1B[37m@classmethod#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
    #x1B[94mdef#x1B[39;49;00m#x1B[90m #x1B[39;49;00m#x1B[92mfrom_call#x1B[39;49;00m(#x1B[90m#x1B[39;49;00m
        #x1B[96mcls#x1B[39;49;00m,#x1B[90m#x1B[39;49;00m
        func: Callable[[], TResult],#x1B[90m#x1B[39;49;00m
        when: Literal[#x1B[33m"#x1B[39;49;00m#x1B[33mcollect#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m, #x1B[33m"#x1B[39;49;00m#x1B[33msetup#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m, #x1B[33m"#x1B[39;49;00m#x1B[33mcall#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m, #x1B[33m"#x1B[39;49;00m#x1B[33mteardown#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m],#x1B[90m#x1B[39;49;00m
        reraise: #x1B[96mtype#x1B[39;49;00m[#x1B[96mBaseException#x1B[39;49;00m] | #x1B[96mtuple#x1B[39;49;00m[#x1B[96mtype#x1B[39;49;00m[#x1B[96mBaseException#x1B[39;49;00m], ...] | #x1B[94mNone#x1B[39;49;00m = #x1B[94mNone#x1B[39;49;00m,#x1B[90m#x1B[39;49;00m
    ) -> CallInfo[TResult]:#x1B[90m#x1B[39;49;00m
    #x1B[90m    #x1B[39;49;00m#x1B[33m"""Call func, wrapping the result in a CallInfo.#x1B[39;49;00m
    #x1B[33m#x1B[39;49;00m
    #x1B[33m    :param func:#x1B[39;49;00m
    #x1B[33m        The function to call. Called without arguments.#x1B[39;49;00m
    #x1B[33m    :type func: Callable[[], _pytest.runner.TResult]#x1B[39;49;00m
    #x1B[33m    :param when:#x1B[39;49;00m
    #x1B[33m        The phase in which the function is called.#x1B[39;49;00m
    #x1B[33m    :param reraise:#x1B[39;49;00m
    #x1B[33m        Exception or exceptions that shall propagate if raised by the#x1B[39;49;00m
    #x1B[33m        function, instead of being wrapped in the CallInfo.#x1B[39;49;00m
    #x1B[33m    """#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
        excinfo = #x1B[94mNone#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
        instant = timing.Instant()#x1B[90m#x1B[39;49;00m
        #x1B[94mtry#x1B[39;49;00m:#x1B[90m#x1B[39;49;00m
>           result: TResult | #x1B[94mNone#x1B[39;49;00m = func()#x1B[90m#x1B[39;49;00m
                                     ^^^^^^#x1B[90m#x1B[39;49;00m

cls        = <class '_pytest.runner.CallInfo'>
duration   = Duration(start=Instant(time=1759516502.3660817, perf_count=1444.2205649), stop=Instant(time=1759516502.4209049, perf_count=1444.2753894))
excinfo    = <ExceptionInfo PytestUnraisableExceptionWarning('Exception ignored while calling deallocator <function IOBase.__del__ at 0x0000013451FDA8D0>: None\n') tblen=13>
func       = <function call_and_report.<locals>.<lambda> at 0x00000134560EC5C0>
instant    = Instant(time=1759516502.3660817, perf_count=1444.2205649)
reraise    = (<class '_pytest.outcomes.Exit'>, <class 'KeyboardInterrupt'>)
result     = None
when       = 'teardown'

#x1B[1m#x1B[31m.tox\py\Lib\site-packages\_pytest\runner.py#x1B[0m:344: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
#x1B[1m#x1B[31m.tox\py\Lib\site-packages\_pytest\runner.py#x1B[0m:246: in <lambda>
    #x1B[0m#x1B[94mlambda#x1B[39;49;00m: runtest_hook(item=item, **kwds), when=when, reraise=reraise#x1B[90m#x1B[39;49;00m
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^#x1B[90m#x1B[39;49;00m
        item       = <Function test_http_over_https_error[0.0.0.0-builtin]>
        kwds       = {'nextitem': <Function test_http_over_https_error[::-builtin]>}
        runtest_hook = <HookCaller 'pytest_runtest_teardown'>
#x1B[1m#x1B[31m.tox\py\Lib\site-packages\pluggy\_hooks.py#x1B[0m:512: in __call__
    #x1B[0m#x1B[94mreturn#x1B[39;49;00m #x1B[96mself#x1B[39;49;00m._hookexec(#x1B[96mself#x1B[39;49;00m.name, #x1B[96mself#x1B[39;49;00m._hookimpls.copy(), kwargs, firstresult)#x1B[90m#x1B[39;49;00m
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^#x1B[90m#x1B[39;49;00m
        firstresult = False
        kwargs     = {'item': <Function test_http_over_https_error[0.0.0.0-builtin]>, 'nextitem': <Function test_http_over_https_error[::-builtin]>}
        self       = <HookCaller 'pytest_runtest_teardown'>
#x1B[1m#x1B[31m.tox\py\Lib\site-packages\pluggy\_manager.py#x1B[0m:120: in _hookexec
    #x1B[0m#x1B[94mreturn#x1B[39;49;00m #x1B[96mself#x1B[39;49;00m._inner_hookexec(hook_name, methods, kwargs, firstresult)#x1B[90m#x1B[39;49;00m
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^#x1B[90m#x1B[39;49;00m
        firstresult = False
        hook_name  = 'pytest_runtest_teardown'
        kwargs     = {'item': <Function test_http_over_https_error[0.0.0.0-builtin]>, 'nextitem': <Function test_http_over_https_error[::-builtin]>}
        methods    = [<HookImpl plugin_name='threadexception', plugin=<module '_pytest.threadexception' from 'D:\\a\\cheroot\\cheroot\\.tox...tIOWrapper name='nul' mode='r' encoding='utf-8'>> _state='suspended' _in_suspended=False> _capture_fixture=None>>, ...]
        self       = <_pytest.config.PytestPluginManager object at 0x0000013450AABB60>
#x1B[1m#x1B[31m.tox\py\Lib\site-packages\_pytest\logging.py#x1B[0m:858: in pytest_runtest_teardown
    #x1B[0m#x1B[94myield#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
        item       = <Function test_http_over_https_error[0.0.0.0-builtin]>
        self       = <_pytest.logging.LoggingPlugin object at 0x00000134521EF620>
#x1B[1m#x1B[31m.tox\py\Lib\site-packages\_pytest\capture.py#x1B[0m:905: in pytest_runtest_teardown
    #x1B[0m#x1B[94mreturn#x1B[39;49;00m (#x1B[94myield#x1B[39;49;00m)#x1B[90m#x1B[39;49;00m
            ^^^^^#x1B[90m#x1B[39;49;00m
        item       = <Function test_http_over_https_error[0.0.0.0-builtin]>
        self       = <CaptureManager _method='fd' _global_capturing=<MultiCapture out=<FDCapture 1 oldfd=8 _state='suspended' tmpfile=<Enco..._io.TextIOWrapper name='nul' mode='r' encoding='utf-8'>> _state='suspended' _in_suspended=False> _capture_fixture=None>
#x1B[1m#x1B[31m.tox\py\Lib\site-packages\_pytest\unraisableexception.py#x1B[0m:163: in pytest_runtest_teardown
    #x1B[0mcollect_unraisable(item.config)#x1B[90m#x1B[39;49;00m
        item       = <Function test_http_over_https_error[0.0.0.0-builtin]>
#x1B[1m#x1B[31m.tox\py\Lib\site-packages\_pytest\unraisableexception.py#x1B[0m:79: in collect_unraisable
    #x1B[0m#x1B[94mraise#x1B[39;49;00m errors[#x1B[94m0#x1B[39;49;00m]#x1B[90m#x1B[39;49;00m
        config     = <_pytest.config.Config object at 0x0000013451425160>
        msg        = 'Exception ignored while calling deallocator <function IOBase.__del__ at 0x0000013451FDA8D0>: None\n\nTraceback (most ...        ~~~~~~~~~~~~~~~^^^\nOSError: [WinError 10038] An operation was attempted on something that is not a socket\n\n'
        pop_unraisable = <built-in method pop of collections.deque object at 0x00000134521CE020>
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

config = <_pytest.config.Config object at 0x0000013451425160>

    #x1B[0m#x1B[94mdef#x1B[39;49;00m#x1B[90m #x1B[39;49;00m#x1B[92mcollect_unraisable#x1B[39;49;00m(config: Config) -> #x1B[94mNone#x1B[39;49;00m:#x1B[90m#x1B[39;49;00m
        pop_unraisable = config.stash[unraisable_exceptions].pop#x1B[90m#x1B[39;49;00m
        errors: #x1B[96mlist#x1B[39;49;00m[pytest.PytestUnraisableExceptionWarning | #x1B[96mRuntimeError#x1B[39;49;00m] = []#x1B[90m#x1B[39;49;00m
        meta = #x1B[94mNone#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
        hook_error = #x1B[94mNone#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
        #x1B[94mtry#x1B[39;49;00m:#x1B[90m#x1B[39;49;00m
            #x1B[94mwhile#x1B[39;49;00m #x1B[94mTrue#x1B[39;49;00m:#x1B[90m#x1B[39;49;00m
                #x1B[94mtry#x1B[39;49;00m:#x1B[90m#x1B[39;49;00m
                    meta = pop_unraisable()#x1B[90m#x1B[39;49;00m
                #x1B[94mexcept#x1B[39;49;00m #x1B[96mIndexError#x1B[39;49;00m:#x1B[90m#x1B[39;49;00m
                    #x1B[94mbreak#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
    #x1B[90m#x1B[39;49;00m
                #x1B[94mif#x1B[39;49;00m #x1B[96misinstance#x1B[39;49;00m(meta, #x1B[96mBaseException#x1B[39;49;00m):#x1B[90m#x1B[39;49;00m
                    hook_error = #x1B[96mRuntimeError#x1B[39;49;00m(#x1B[33m"#x1B[39;49;00m#x1B[33mFailed to process unraisable exception#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m)#x1B[90m#x1B[39;49;00m
                    hook_error.__cause__ = meta#x1B[90m#x1B[39;49;00m
                    errors.append(hook_error)#x1B[90m#x1B[39;49;00m
                    #x1B[94mcontinue#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
    #x1B[90m#x1B[39;49;00m
                msg = meta.msg#x1B[90m#x1B[39;49;00m
                #x1B[94mtry#x1B[39;49;00m:#x1B[90m#x1B[39;49;00m
>                   warnings.warn(pytest.PytestUnraisableExceptionWarning(msg))#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[31mE                   pytest.PytestUnraisableExceptionWarning: Exception ignored while calling deallocator <function IOBase.__del__ at 0x0000013451FDA8D0>: None#x1B[0m

config     = <_pytest.config.Config object at 0x0000013451425160>
msg        = 'Exception ignored while calling deallocator <function IOBase.__del__ at 0x0000013451FDA8D0>: None\n\nTraceback (most ...        ~~~~~~~~~~~~~~~^^^\nOSError: [WinError 10038] An operation was attempted on something that is not a socket\n\n'
pop_unraisable = <built-in method pop of collections.deque object at 0x00000134521CE020>

#x1B[1m#x1B[31m.tox\py\Lib\site-packages\_pytest\unraisableexception.py#x1B[0m:67: PytestUnraisableExceptionWarning
cheroot\test\test_ssl.py::test::test_ssl::test_http_over_https_error[::-builtin]

Flake rate in main: 33.33% (Passed 8 times, Failed 4 times)

Stack Traces | 0.278s run time
self = <cheroot.makefile.StreamWriter name=-1>

    #x1B[0m#x1B[94mdef#x1B[39;49;00m#x1B[90m #x1B[39;49;00m#x1B[92m__del__#x1B[39;49;00m(#x1B[96mself#x1B[39;49;00m):#x1B[90m#x1B[39;49;00m
    #x1B[90m    #x1B[39;49;00m#x1B[33m"""Destructor.  Calls close()."""#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
        #x1B[94mtry#x1B[39;49;00m:#x1B[90m#x1B[39;49;00m
            closed = #x1B[96mself#x1B[39;49;00m.closed#x1B[90m#x1B[39;49;00m
        #x1B[94mexcept#x1B[39;49;00m #x1B[96mAttributeError#x1B[39;49;00m:#x1B[90m#x1B[39;49;00m
            #x1B[90m# If getting closed fails, then the object is probably#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
            #x1B[90m# in an unusable state, so ignore.#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
            #x1B[94mreturn#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
    #x1B[90m#x1B[39;49;00m
        #x1B[94mif#x1B[39;49;00m closed:#x1B[90m#x1B[39;49;00m
            #x1B[94mreturn#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
    #x1B[90m#x1B[39;49;00m
        #x1B[94mif#x1B[39;49;00m dealloc_warn := #x1B[96mgetattr#x1B[39;49;00m(#x1B[96mself#x1B[39;49;00m, #x1B[33m"#x1B[39;49;00m#x1B[33m_dealloc_warn#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m, #x1B[94mNone#x1B[39;49;00m):#x1B[90m#x1B[39;49;00m
            dealloc_warn(#x1B[96mself#x1B[39;49;00m)#x1B[90m#x1B[39;49;00m
    #x1B[90m#x1B[39;49;00m
        #x1B[90m# If close() fails, the caller logs the exception with#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
        #x1B[90m# sys.unraisablehook. close() must be called at the end at __del__().#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
>       #x1B[96mself#x1B[39;49;00m.close()#x1B[90m#x1B[39;49;00m

closed     = False
dealloc_warn = <bound method _BufferedIOMixin._dealloc_warn of <cheroot.makefile.StreamWriter name=-1>>
self       = <cheroot.makefile.StreamWriter name=-1>

#x1B[1m#x1B[31mC:\hostedtoolcache\windows\Python\3.14.0-rc.3\x64\Lib\_pyio.py#x1B[0m:415: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
#x1B[1m#x1B[31mC:\hostedtoolcache\windows\Python\3.14.0-rc.3\x64\Lib\_pyio.py#x1B[0m:1310: in close
    #x1B[0m#x1B[96mself#x1B[39;49;00m.flush()#x1B[90m#x1B[39;49;00m
        self       = <cheroot.makefile.StreamWriter name=-1>
#x1B[1m#x1B[31mC:\hostedtoolcache\windows\Python\3.14.0-rc.3\x64\Lib\_pyio.py#x1B[0m:1271: in flush
    #x1B[0m#x1B[96mself#x1B[39;49;00m._flush_unlocked()#x1B[90m#x1B[39;49;00m
        self       = <cheroot.makefile.StreamWriter name=-1>
#x1B[1m#x1B[31mcheroot\makefile.py#x1B[0m:32: in _flush_unlocked
    #x1B[0mn = #x1B[96mself#x1B[39;49;00m.raw.write(#x1B[96mbytes#x1B[39;49;00m(#x1B[96mself#x1B[39;49;00m._write_buf))#x1B[90m#x1B[39;49;00m
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^#x1B[90m#x1B[39;49;00m
        self       = <cheroot.makefile.StreamWriter name=-1>
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <socket.SocketIO object at 0x0000013454F8EFE0>
b = b'HTTP/1.1 400 Bad Request\r\nContent-Length: 85\r\nContent-Type: text/plain\r\n\r\nThe client sent a plain HTTP request, but this server only speaks HTTPS on this port.'

    #x1B[0m#x1B[94mdef#x1B[39;49;00m#x1B[90m #x1B[39;49;00m#x1B[92mwrite#x1B[39;49;00m(#x1B[96mself#x1B[39;49;00m, b):#x1B[90m#x1B[39;49;00m
    #x1B[90m    #x1B[39;49;00m#x1B[33m"""Write the given bytes or bytearray object *b* to the socket#x1B[39;49;00m
    #x1B[33m    and return the number of bytes written.  This can be less than#x1B[39;49;00m
    #x1B[33m    len(b) if not all data could be written.  If the socket is#x1B[39;49;00m
    #x1B[33m    non-blocking and no bytes could be written None is returned.#x1B[39;49;00m
    #x1B[33m    """#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
        #x1B[96mself#x1B[39;49;00m._checkClosed()#x1B[90m#x1B[39;49;00m
        #x1B[96mself#x1B[39;49;00m._checkWritable()#x1B[90m#x1B[39;49;00m
        #x1B[94mtry#x1B[39;49;00m:#x1B[90m#x1B[39;49;00m
>           #x1B[94mreturn#x1B[39;49;00m #x1B[96mself#x1B[39;49;00m._sock.send(b)#x1B[90m#x1B[39;49;00m
                   ^^^^^^^^^^^^^^^^^^#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[31mE           OSError: [WinError 10038] An operation was attempted on something that is not a socket#x1B[0m

b          = b'HTTP/1.1 400 Bad Request\r\nContent-Length: 85\r\nContent-Type: text/plain\r\n\r\nThe client sent a plain HTTP request, but this server only speaks HTTPS on this port.'
self       = <socket.SocketIO object at 0x0000013454F8EFE0>

#x1B[1m#x1B[31mC:\hostedtoolcache\windows\Python\3.14.0-rc.3\x64\Lib\socket.py#x1B[0m:743: OSError

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

cls = <class '_pytest.runner.CallInfo'>
func = <function call_and_report.<locals>.<lambda> at 0x000001345615B950>
when = 'teardown'
reraise = (<class '_pytest.outcomes.Exit'>, <class 'KeyboardInterrupt'>)

    #x1B[0m#x1B[37m@classmethod#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
    #x1B[94mdef#x1B[39;49;00m#x1B[90m #x1B[39;49;00m#x1B[92mfrom_call#x1B[39;49;00m(#x1B[90m#x1B[39;49;00m
        #x1B[96mcls#x1B[39;49;00m,#x1B[90m#x1B[39;49;00m
        func: Callable[[], TResult],#x1B[90m#x1B[39;49;00m
        when: Literal[#x1B[33m"#x1B[39;49;00m#x1B[33mcollect#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m, #x1B[33m"#x1B[39;49;00m#x1B[33msetup#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m, #x1B[33m"#x1B[39;49;00m#x1B[33mcall#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m, #x1B[33m"#x1B[39;49;00m#x1B[33mteardown#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m],#x1B[90m#x1B[39;49;00m
        reraise: #x1B[96mtype#x1B[39;49;00m[#x1B[96mBaseException#x1B[39;49;00m] | #x1B[96mtuple#x1B[39;49;00m[#x1B[96mtype#x1B[39;49;00m[#x1B[96mBaseException#x1B[39;49;00m], ...] | #x1B[94mNone#x1B[39;49;00m = #x1B[94mNone#x1B[39;49;00m,#x1B[90m#x1B[39;49;00m
    ) -> CallInfo[TResult]:#x1B[90m#x1B[39;49;00m
    #x1B[90m    #x1B[39;49;00m#x1B[33m"""Call func, wrapping the result in a CallInfo.#x1B[39;49;00m
    #x1B[33m#x1B[39;49;00m
    #x1B[33m    :param func:#x1B[39;49;00m
    #x1B[33m        The function to call. Called without arguments.#x1B[39;49;00m
    #x1B[33m    :type func: Callable[[], _pytest.runner.TResult]#x1B[39;49;00m
    #x1B[33m    :param when:#x1B[39;49;00m
    #x1B[33m        The phase in which the function is called.#x1B[39;49;00m
    #x1B[33m    :param reraise:#x1B[39;49;00m
    #x1B[33m        Exception or exceptions that shall propagate if raised by the#x1B[39;49;00m
    #x1B[33m        function, instead of being wrapped in the CallInfo.#x1B[39;49;00m
    #x1B[33m    """#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
        excinfo = #x1B[94mNone#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
        instant = timing.Instant()#x1B[90m#x1B[39;49;00m
        #x1B[94mtry#x1B[39;49;00m:#x1B[90m#x1B[39;49;00m
>           result: TResult | #x1B[94mNone#x1B[39;49;00m = func()#x1B[90m#x1B[39;49;00m
                                     ^^^^^^#x1B[90m#x1B[39;49;00m

cls        = <class '_pytest.runner.CallInfo'>
duration   = Duration(start=Instant(time=1759516510.3307438, perf_count=1452.1852499), stop=Instant(time=1759516510.3965516, perf_count=1452.2510585))
excinfo    = <ExceptionInfo PytestUnraisableExceptionWarning('Exception ignored while calling deallocator <function IOBase.__del__ at 0x0000013451FDA8D0>: None\n') tblen=13>
func       = <function call_and_report.<locals>.<lambda> at 0x000001345615B950>
instant    = Instant(time=1759516510.3307438, perf_count=1452.1852499)
reraise    = (<class '_pytest.outcomes.Exit'>, <class 'KeyboardInterrupt'>)
result     = None
when       = 'teardown'

#x1B[1m#x1B[31m.tox\py\Lib\site-packages\_pytest\runner.py#x1B[0m:344: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
#x1B[1m#x1B[31m.tox\py\Lib\site-packages\_pytest\runner.py#x1B[0m:246: in <lambda>
    #x1B[0m#x1B[94mlambda#x1B[39;49;00m: runtest_hook(item=item, **kwds), when=when, reraise=reraise#x1B[90m#x1B[39;49;00m
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^#x1B[90m#x1B[39;49;00m
        item       = <Function test_http_over_https_error[::-builtin]>
        kwds       = {'nextitem': None}
        runtest_hook = <HookCaller 'pytest_runtest_teardown'>
#x1B[1m#x1B[31m.tox\py\Lib\site-packages\pluggy\_hooks.py#x1B[0m:512: in __call__
    #x1B[0m#x1B[94mreturn#x1B[39;49;00m #x1B[96mself#x1B[39;49;00m._hookexec(#x1B[96mself#x1B[39;49;00m.name, #x1B[96mself#x1B[39;49;00m._hookimpls.copy(), kwargs, firstresult)#x1B[90m#x1B[39;49;00m
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^#x1B[90m#x1B[39;49;00m
        firstresult = False
        kwargs     = {'item': <Function test_http_over_https_error[::-builtin]>, 'nextitem': None}
        self       = <HookCaller 'pytest_runtest_teardown'>
#x1B[1m#x1B[31m.tox\py\Lib\site-packages\pluggy\_manager.py#x1B[0m:120: in _hookexec
    #x1B[0m#x1B[94mreturn#x1B[39;49;00m #x1B[96mself#x1B[39;49;00m._inner_hookexec(hook_name, methods, kwargs, firstresult)#x1B[90m#x1B[39;49;00m
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^#x1B[90m#x1B[39;49;00m
        firstresult = False
        hook_name  = 'pytest_runtest_teardown'
        kwargs     = {'item': <Function test_http_over_https_error[::-builtin]>, 'nextitem': None}
        methods    = [<HookImpl plugin_name='threadexception', plugin=<module '_pytest.threadexception' from 'D:\\a\\cheroot\\cheroot\\.tox...tIOWrapper name='nul' mode='r' encoding='utf-8'>> _state='suspended' _in_suspended=False> _capture_fixture=None>>, ...]
        self       = <_pytest.config.PytestPluginManager object at 0x0000013450AABB60>
#x1B[1m#x1B[31m.tox\py\Lib\site-packages\_pytest\logging.py#x1B[0m:858: in pytest_runtest_teardown
    #x1B[0m#x1B[94myield#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
        item       = <Function test_http_over_https_error[::-builtin]>
        self       = <_pytest.logging.LoggingPlugin object at 0x00000134521EF620>
#x1B[1m#x1B[31m.tox\py\Lib\site-packages\_pytest\capture.py#x1B[0m:905: in pytest_runtest_teardown
    #x1B[0m#x1B[94mreturn#x1B[39;49;00m (#x1B[94myield#x1B[39;49;00m)#x1B[90m#x1B[39;49;00m
            ^^^^^#x1B[90m#x1B[39;49;00m
        item       = <Function test_http_over_https_error[::-builtin]>
        self       = <CaptureManager _method='fd' _global_capturing=<MultiCapture out=<FDCapture 1 oldfd=8 _state='suspended' tmpfile=<Enco..._io.TextIOWrapper name='nul' mode='r' encoding='utf-8'>> _state='suspended' _in_suspended=False> _capture_fixture=None>
#x1B[1m#x1B[31m.tox\py\Lib\site-packages\_pytest\unraisableexception.py#x1B[0m:163: in pytest_runtest_teardown
    #x1B[0mcollect_unraisable(item.config)#x1B[90m#x1B[39;49;00m
        item       = <Function test_http_over_https_error[::-builtin]>
#x1B[1m#x1B[31m.tox\py\Lib\site-packages\_pytest\unraisableexception.py#x1B[0m:79: in collect_unraisable
    #x1B[0m#x1B[94mraise#x1B[39;49;00m errors[#x1B[94m0#x1B[39;49;00m]#x1B[90m#x1B[39;49;00m
        config     = <_pytest.config.Config object at 0x0000013451425160>
        msg        = 'Exception ignored while calling deallocator <function IOBase.__del__ at 0x0000013451FDA8D0>: None\n\nTraceback (most ...        ~~~~~~~~~~~~~~~^^^\nOSError: [WinError 10038] An operation was attempted on something that is not a socket\n\n'
        pop_unraisable = <built-in method pop of collections.deque object at 0x00000134521CE020>
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

config = <_pytest.config.Config object at 0x0000013451425160>

    #x1B[0m#x1B[94mdef#x1B[39;49;00m#x1B[90m #x1B[39;49;00m#x1B[92mcollect_unraisable#x1B[39;49;00m(config: Config) -> #x1B[94mNone#x1B[39;49;00m:#x1B[90m#x1B[39;49;00m
        pop_unraisable = config.stash[unraisable_exceptions].pop#x1B[90m#x1B[39;49;00m
        errors: #x1B[96mlist#x1B[39;49;00m[pytest.PytestUnraisableExceptionWarning | #x1B[96mRuntimeError#x1B[39;49;00m] = []#x1B[90m#x1B[39;49;00m
        meta = #x1B[94mNone#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
        hook_error = #x1B[94mNone#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
        #x1B[94mtry#x1B[39;49;00m:#x1B[90m#x1B[39;49;00m
            #x1B[94mwhile#x1B[39;49;00m #x1B[94mTrue#x1B[39;49;00m:#x1B[90m#x1B[39;49;00m
                #x1B[94mtry#x1B[39;49;00m:#x1B[90m#x1B[39;49;00m
                    meta = pop_unraisable()#x1B[90m#x1B[39;49;00m
                #x1B[94mexcept#x1B[39;49;00m #x1B[96mIndexError#x1B[39;49;00m:#x1B[90m#x1B[39;49;00m
                    #x1B[94mbreak#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
    #x1B[90m#x1B[39;49;00m
                #x1B[94mif#x1B[39;49;00m #x1B[96misinstance#x1B[39;49;00m(meta, #x1B[96mBaseException#x1B[39;49;00m):#x1B[90m#x1B[39;49;00m
                    hook_error = #x1B[96mRuntimeError#x1B[39;49;00m(#x1B[33m"#x1B[39;49;00m#x1B[33mFailed to process unraisable exception#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m)#x1B[90m#x1B[39;49;00m
                    hook_error.__cause__ = meta#x1B[90m#x1B[39;49;00m
                    errors.append(hook_error)#x1B[90m#x1B[39;49;00m
                    #x1B[94mcontinue#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
    #x1B[90m#x1B[39;49;00m
                msg = meta.msg#x1B[90m#x1B[39;49;00m
                #x1B[94mtry#x1B[39;49;00m:#x1B[90m#x1B[39;49;00m
>                   warnings.warn(pytest.PytestUnraisableExceptionWarning(msg))#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[31mE                   pytest.PytestUnraisableExceptionWarning: Exception ignored while calling deallocator <function IOBase.__del__ at 0x0000013451FDA8D0>: None#x1B[0m

config     = <_pytest.config.Config object at 0x0000013451425160>
msg        = 'Exception ignored while calling deallocator <function IOBase.__del__ at 0x0000013451FDA8D0>: None\n\nTraceback (most ...        ~~~~~~~~~~~~~~~^^^\nOSError: [WinError 10038] An operation was attempted on something that is not a socket\n\n'
pop_unraisable = <built-in method pop of collections.deque object at 0x00000134521CE020>

#x1B[1m#x1B[31m.tox\py\Lib\site-packages\_pytest\unraisableexception.py#x1B[0m:67: PytestUnraisableExceptionWarning

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 webknjaz added bug Something is broken regression Something that worked earlier got broken in new release labels Oct 3, 2025
Copy link
Member

@webknjaz webknjaz left a comment

Choose a reason for hiding this comment

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

@itamarst since you submitted the original PR, may I ask you to check this bug fix?

@manthey could you add a bugfix change note and maybe a regression test? https://cheroot.cherrypy.dev/en/latest/contributing/guidelines/#adding-change-notes-with-your-prs

@itamarst
Copy link

itamarst commented Oct 3, 2025

I will try to look at it on Monday, if I don't do it then, remind me again.

@martin-vi
Copy link

martin-vi commented Oct 7, 2025

Sorry but daemon=True does not solve the issue here for Python 3.12. In particular, when cheroot is used in test cases, more and more threads accumulate.

Here is a somewhat simple test case to illustrate the problem. It is best to run it without xdist to see it better:

diff --git a/cheroot/test/test_server.py b/cheroot/test/test_server.py
index fb5c5468..5c1251d6 100644
--- a/cheroot/test/test_server.py
+++ b/cheroot/test/test_server.py
@@ -611,3 +611,17 @@ def test_overload_results_in_suitable_http_error(request):

     response = requests.get(f'http://{localhost}:{port}', timeout=20)
     assert response.status_code == HTTPStatus.SERVICE_UNAVAILABLE
+
+
+def test_unservicable_threadleak():
+    before_serv = threading.active_count()
+    # assert before_serv == 1 # <- does not work with xdist ..
+    for _ in range(10):
+        httpserver = HTTPServer(
+                bind_addr=(ANY_INTERFACE_IPV4, EPHEMERAL_PORT),
+                gateway=Gateway,
+        )
+        with httpserver._run_in_thread() as server:
+            import time; time.sleep(0.5)
+
+    assert threading.active_count() == before_serv

Run pytest with --pdb and inspect threads with threading.enumerate() you will spot all the pilled up _serve_unservicable threads that are never tidied up.

@webknjaz
Copy link
Member

@manthey could you include Martin's test case, please?

@itamarst do you have more ideas on improving this?

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

Labels

bug Something is broken regression Something that worked earlier got broken in new release

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Should the new _serve_unservicable thread be daemon?

4 participants