Skip to content

[8.1.0] _run_async_function returns unawaited Future when event loop is running #971

@mahmoud

Description

@mahmoud

Hey folks! Longtime user via pytest-recording, first time issue-filer. Big fan of v8 because of #943 , allowed me to remove a bunch of monkeypatched conftest.py code. Unfortunately today I found myself adding some more monkeypatching back in, due to some interaction with httpx via dspy/litellm.

When using VCR with libraries that use httpx/httpcore internally (like litellm/DSPy), requests fail with RuntimeError: await wasn't used with future when there's already a running event loop.

For me, this happens when:

  1. Running tests with pytest (which may have an event loop)
  2. Making HTTP requests through a library that uses httpx internally
  3. VCR intercepts the request via httpcore stubs

Repro / Traceback

For example:

import dspy

@pytest.mark.vcr
def test_llm_call():
    lm = dspy.LM("gemini/gemini-2.0-flash", api_key="...")
    with dspy.context(lm=lm):
        result = my_dspy_module(input="test")  # Fails

And the stack:

litellm.exceptions.APIConnectionError: litellm.APIConnectionError: await wasn't used with future
Traceback (most recent call last):
  File "/opt/app/venv/lib/python3.11/site-packages/litellm/main.py", line 3113, in completion
    response = vertex_chat_completion.completion(
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/app/venv/lib/python3.11/site-packages/litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py", line 2599, in completion
    response = client.post(url=url, headers=headers, json=data, logging_obj=logging_obj)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/app/venv/lib/python3.11/site-packages/litellm/llms/custom_httpx/http_handler.py", line 960, in post
    response = self.client.send(req, stream=stream)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/app/venv/lib/python3.11/site-packages/httpx/_client.py", line 926, in send
    response = self._send_handling_auth(
               ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/app/venv/lib/python3.11/site-packages/httpx/_client.py", line 1027, in _send_single_request
    response = transport.handle_request(request)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/app/venv/lib/python3.11/site-packages/httpx/_transports/default.py", line 236, in handle_request
    resp = self._pool.handle_request(req)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/app/venv/lib/python3.11/site-packages/vcr/stubs/httpcore_stubs.py", line 213, in _inner_handle_request
    return _vcr_handle_request(cassette, real_handle_request, self, real_request)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/app/venv/lib/python3.11/site-packages/vcr/stubs/httpcore_stubs.py", line 195, in _vcr_handle_request
    vcr_request, vcr_response = _run_async_function(
    ^^^^^^^^^^^^^^^^^^^^^^^^^
RuntimeError: await wasn't used with future

I think this is because in vcr/stubs/httpcore_stubs.py, the _run_async_function does this:

def _run_async_function(sync_func, *args, **kwargs):
"""
Safely run an asynchronous function from a synchronous context.
Handles both cases:
- An event loop is already running.
- No event loop exists yet.
"""
try:
asyncio.get_running_loop()
except RuntimeError:
return asyncio.run(sync_func(*args, **kwargs))
else:
# If inside a running loop, create a task and wait for it
return asyncio.ensure_future(sync_func(*args, **kwargs))

The problem is in the else branch: asyncio.ensure_future() returns a Future object, but doesn't wait for it. The caller expects the actual result, not a Future.

Note that I know this worked with v7 + an earlier version of litellm, but can't honestly say this is for sure the root cause.

Workaround

For now I just throw a thread around it (via monkeypatch in conftest.py):

import asyncio
import threading

def _patched_run_async_function(async_func, *args, **kwargs):
    try:
        asyncio.get_running_loop()
    except RuntimeError:
        return asyncio.run(async_func(*args, **kwargs))
    else:
        # Run in separate thread with its own event loop
        result = None
        exception = None
        
        def run_in_thread():
            nonlocal result, exception
            try:
                loop = asyncio.new_event_loop()
                asyncio.set_event_loop(loop)
                try:
                    result = loop.run_until_complete(async_func(*args, **kwargs))
                finally:
                    loop.close()
                    asyncio.set_event_loop(None)
            except Exception as e:
                exception = e
        
        thread = threading.Thread(target=run_in_thread)
        thread.start()
        thread.join()
        
        if exception is not None:
            raise exception
        return result

# Apply patch
import vcr.stubs.httpcore_stubs as httpcore_stubs
httpcore_stubs._run_async_function = _patched_run_async_function

cc @jairhenrique / @seowalex bc #943

Thanks for all your hard work!

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions