httpx icon indicating copy to clipboard operation
httpx copied to clipboard

deque mutated / dictionary changed during iteration

Open robpats opened this issue 6 months ago • 0 comments
trafficstars

Discussions #3190 #3279

Errors occurred while downloading a lot of small (~2KB) files with 8 threads on both secure and clear text HTTP/2 but not HTTP/1.1. The following statistics of downloading from Azure Storage give some ideas of the rarity of errors.

1200 of 30597 failed

Count: Exception
   11: RuntimeError('deque mutated during iteration')
    2: RuntimeError('dictionary changed size during iteration')
    1: RuntimeError('dictionary keys changed during iteration')

   25: KeyError
   34: LocalProtocolError('Invalid input ConnectionInputs.RECV_DATA in state ConnectionState.CLOSED')
   15: LocalProtocolError('Invalid input ConnectionInputs.RECV_HEADERS in state ConnectionState.CLOSED')
   15: LocalProtocolError('Invalid input StreamInputs.SEND_HEADERS in state 5')
    1: LocalProtocolError('StreamIDTooLowError: 19 is lower than 31')
    1: LocalProtocolError('StreamIDTooLowError: 399 is lower than 409')
    1: LocalProtocolError('StreamIDTooLowError: 781 is lower than 793')
    1: LocalProtocolError('StreamIDTooLowError: 95 is lower than 101')
  248: RemoteProtocolError('<ConnectionTerminated error_code:1>')
   40: RemoteProtocolError('<ConnectionTerminated error_code:9>')
  774: RemoteProtocolError('Server disconnected')
   31: WriteError('EOF occurred in violation of protocol (_ssl.c:2427)')

Traceback

RuntimeError: deque mutated during iteration

Traceback (most recent call last):
  File "R:\httpxdltest.py", line 26, in download
    with contextlib.closing(client.send(req, stream=True)) as response:
                            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\User\AppData\Roaming\Python\Python312\site-packages\httpx\_client.py", line 914, in send
    response = self._send_handling_auth(
               ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\User\AppData\Roaming\Python\Python312\site-packages\httpx\_client.py", line 942, in _send_handling_auth
    response = self._send_handling_redirects(
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\User\AppData\Roaming\Python\Python312\site-packages\httpx\_client.py", line 979, in _send_handling_redirects
    response = self._send_single_request(request)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\User\AppData\Roaming\Python\Python312\site-packages\httpx\_client.py", line 1014, in _send_single_request
    response = transport.handle_request(request)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\User\AppData\Roaming\Python\Python312\site-packages\httpx\_transports\default.py", line 250, in handle_request
    resp = self._pool.handle_request(req)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\User\AppData\Roaming\Python\Python312\site-packages\httpcore\_sync\connection_pool.py", line 256, in handle_request
    raise exc from None
  File "C:\Users\User\AppData\Roaming\Python\Python312\site-packages\httpcore\_sync\connection_pool.py", line 236, in handle_request
    response = connection.handle_request(
               ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\User\AppData\Roaming\Python\Python312\site-packages\httpcore\_sync\connection.py", line 103, in handle_request
    return self._connection.handle_request(request)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\User\AppData\Roaming\Python\Python312\site-packages\httpcore\_sync\http2.py", line 187, in handle_request
    raise exc
  File "C:\Users\User\AppData\Roaming\Python\Python312\site-packages\httpcore\_sync\http2.py", line 144, in handle_request
    self._send_request_headers(request=request, stream_id=stream_id)
  File "C:\Users\User\AppData\Roaming\Python\Python312\site-packages\httpcore\_sync\http2.py", line 249, in _send_request_headers
    self._h2_state.send_headers(stream_id, headers, end_stream=end_stream)
  File "C:\Users\User\AppData\Roaming\Python\Python312\site-packages\h2\connection.py", line 806, in send_headers
    frames.extend(stream.send_headers(
                  ^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\User\AppData\Roaming\Python\Python312\site-packages\h2\stream.py", line 894, in send_headers
    frames = self._build_headers_frames(
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\User\AppData\Roaming\Python\Python312\site-packages\h2\stream.py", line 1298, in _build_headers_frames
    encoded_headers = encoder.encode(headers)
                      ^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\User\AppData\Roaming\Python\Python312\site-packages\hpack\hpack.py", line 276, in encode
    header_block.append(self.add(new_header, sensitive, huffman))
                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\User\AppData\Roaming\Python\Python312\site-packages\hpack\hpack.py", line 301, in add
    match = self.header_table.search(name, value)
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\User\AppData\Roaming\Python\Python312\site-packages\hpack\table.py", line 186, in search
    for (i, (n, v)) in enumerate(self.dynamic_entries):
                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
RuntimeError: deque mutated during iteration

RuntimeError: dictionary changed size during iteration

Traceback (most recent call last):
  File "R:\httpxdltest.py", line 26, in download
    with contextlib.closing(client.send(req, stream=True)) as response:
                            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\User\AppData\Roaming\Python\Python312\site-packages\httpx\_client.py", line 914, in send
    response = self._send_handling_auth(
               ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\User\AppData\Roaming\Python\Python312\site-packages\httpx\_client.py", line 942, in _send_handling_auth
    response = self._send_handling_redirects(
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\User\AppData\Roaming\Python\Python312\site-packages\httpx\_client.py", line 979, in _send_handling_redirects
    response = self._send_single_request(request)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\User\AppData\Roaming\Python\Python312\site-packages\httpx\_client.py", line 1014, in _send_single_request
    response = transport.handle_request(request)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\User\AppData\Roaming\Python\Python312\site-packages\httpx\_transports\default.py", line 250, in handle_request
    resp = self._pool.handle_request(req)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\User\AppData\Roaming\Python\Python312\site-packages\httpcore\_sync\connection_pool.py", line 256, in handle_request
    raise exc from None
  File "C:\Users\User\AppData\Roaming\Python\Python312\site-packages\httpcore\_sync\connection_pool.py", line 236, in handle_request
    response = connection.handle_request(
               ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\User\AppData\Roaming\Python\Python312\site-packages\httpcore\_sync\connection.py", line 103, in handle_request
    return self._connection.handle_request(request)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\User\AppData\Roaming\Python\Python312\site-packages\httpcore\_sync\http2.py", line 187, in handle_request
    raise exc
  File "C:\Users\User\AppData\Roaming\Python\Python312\site-packages\httpcore\_sync\http2.py", line 144, in handle_request
    self._send_request_headers(request=request, stream_id=stream_id)
  File "C:\Users\User\AppData\Roaming\Python\Python312\site-packages\httpcore\_sync\http2.py", line 249, in _send_request_headers
    self._h2_state.send_headers(stream_id, headers, end_stream=end_stream)
  File "C:\Users\User\AppData\Roaming\Python\Python312\site-packages\h2\connection.py", line 796, in send_headers
    if (self.open_outbound_streams + 1) > max_open_streams:
        ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\User\AppData\Roaming\Python\Python312\site-packages\h2\connection.py", line 451, in open_outbound_streams
    return self._open_streams(outbound_numbers)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\User\AppData\Roaming\Python\Python312\site-packages\h2\connection.py", line 433, in _open_streams
    for stream_id, stream in self.streams.items():
                             ^^^^^^^^^^^^^^^^^^^^
RuntimeError: dictionary changed size during iteration

RuntimeError: dictionary keys changed during iteration

Traceback (most recent call last):
  File "R:\httpxdltest.py", line 26, in download
    with contextlib.closing(client.send(req, stream=True)) as response:
                            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\User\AppData\Roaming\Python\Python312\site-packages\httpx\_client.py", line 914, in send
    response = self._send_handling_auth(
               ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\User\AppData\Roaming\Python\Python312\site-packages\httpx\_client.py", line 942, in _send_handling_auth
    response = self._send_handling_redirects(
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\User\AppData\Roaming\Python\Python312\site-packages\httpx\_client.py", line 979, in _send_handling_redirects
    response = self._send_single_request(request)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\User\AppData\Roaming\Python\Python312\site-packages\httpx\_client.py", line 1014, in _send_single_request
    response = transport.handle_request(request)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\User\AppData\Roaming\Python\Python312\site-packages\httpx\_transports\default.py", line 250, in handle_request
    resp = self._pool.handle_request(req)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\User\AppData\Roaming\Python\Python312\site-packages\httpcore\_sync\connection_pool.py", line 256, in handle_request
    raise exc from None
  File "C:\Users\User\AppData\Roaming\Python\Python312\site-packages\httpcore\_sync\connection_pool.py", line 236, in handle_request
    response = connection.handle_request(
               ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\User\AppData\Roaming\Python\Python312\site-packages\httpcore\_sync\connection.py", line 103, in handle_request
    return self._connection.handle_request(request)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\User\AppData\Roaming\Python\Python312\site-packages\httpcore\_sync\http2.py", line 187, in handle_request
    raise exc
  File "C:\Users\User\AppData\Roaming\Python\Python312\site-packages\httpcore\_sync\http2.py", line 144, in handle_request
    self._send_request_headers(request=request, stream_id=stream_id)
  File "C:\Users\User\AppData\Roaming\Python\Python312\site-packages\httpcore\_sync\http2.py", line 249, in _send_request_headers
    self._h2_state.send_headers(stream_id, headers, end_stream=end_stream)
  File "C:\Users\User\AppData\Roaming\Python\Python312\site-packages\h2\connection.py", line 796, in send_headers
    if (self.open_outbound_streams + 1) > max_open_streams:
        ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\User\AppData\Roaming\Python\Python312\site-packages\h2\connection.py", line 451, in open_outbound_streams
    return self._open_streams(outbound_numbers)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\User\AppData\Roaming\Python\Python312\site-packages\h2\connection.py", line 433, in _open_streams
    for stream_id, stream in self.streams.items():
                             ^^^^^^^^^^^^^^^^^^^^
RuntimeError: dictionary keys changed during iteration

They are reproducible in the following environment.

Server

Azure Storage x-ms-version 2009-09-19 / fastapi 0.115.12 on hypercorn 0.17.3

fastapi's "Hello world" code run on hypercorn

from typing import Union
from fastapi import FastAPI


app = FastAPI()


@app.get("/items/{item_id}")
async def read_item(item_id: int, q: Union[str, None] = None):
    return {"item_id": item_id, "q": q}

Client

Windows 7 / 10 Python 3.8.10 / 3.12.10 httpx 0.28.1 httpcore 1.0.9 h2 4.2.0

Test

#! /usr/bin/env python3
import os
import logging
import contextlib
import concurrent.futures

import httpx


logger = logging.getLogger(__name__)
logger.addHandler(logging.NullHandler())
traceback_logger = logging.getLogger(__name__ + '_traceback')
traceback_logger.addHandler(logging.NullHandler())


def download(client, url, timeout=30):
    try:
        req = client.build_request('GET', url, timeout=timeout)
        with contextlib.closing(client.send(req, stream=True)) as response:
            for chunk in response.iter_bytes():
                pass
    except BaseException as e:
        logger.error('{!r} at "{}"'.format(e, url))
        traceback_logger.error('{!r} at "{}"'.format(e, url), exc_info=e)
        raise


def test(urls, max_workers=os.cpu_count() or 1, timeout=30):
    with httpx.Client(http1=False, http2=True, timeout=timeout) as client,\
            concurrent.futures.ThreadPoolExecutor(max_workers) as executor:
        not_done = set()
        for url in urls:
            not_done.add(executor.submit(download, client, url, timeout))
        done, not_done = concurrent.futures.wait(not_done)

        errors = {}
        for future in done:
            if future.exception() is not None:
                errors[repr(future.exception())] = errors.get(repr(future.exception()), 0) + 1
        stat = ['{} of {} failed'.format(sum(errors.values()), len(done))]
        if errors:
            stat.append('Count: Exception')
            for k in sorted(errors):
                stat.append('{: >5}: {}'.format(errors[k], k))
        logger.info('\n'.join(stat))


if __name__ == '__main__':
    import sys

    if len(sys.argv) != 2:
        print('Usage: {} <concurrent count>'.format(sys.argv[0]))
        sys.exit(1)

    logger.setLevel(logging.DEBUG)
    traceback_logger.setLevel(logging.DEBUG)
    formatter = logging.Formatter('{asctime}: TID {thread}: {levelname}: {message}', style='{')
    handler = logging.StreamHandler(sys.stderr)
    handler.setLevel(logging.DEBUG)
    handler.setFormatter(formatter)
    logger.addHandler(handler)
    handler = logging.FileHandler(os.path.splitext(sys.argv[0])[0] + '.log', encoding='utf-8')
    handler.setLevel(logging.DEBUG)
    handler.setFormatter(formatter)
    logger.addHandler(handler)
    handler = logging.FileHandler(os.path.splitext(sys.argv[0])[0] + '_traceback.log', encoding='utf-8')
    handler.setLevel(logging.DEBUG)
    handler.setFormatter(formatter)
    traceback_logger.addHandler(handler)

    urls = ('http://127.0.0.1:8000/items/' + str(id) for id in range(100000))
    test(urls, int(sys.argv[1]) or os.cpu_count() or 1)

robpats avatar May 09 '25 07:05 robpats