botbuilder-python icon indicating copy to clipboard operation
botbuilder-python copied to clipboard

Blocking HTTP call inside _OpenIdMetadata._refresh causes event loop to be blocked

Open nickodell opened this issue 7 months ago • 1 comments

Version

What package version of the SDK are you using.

I am using version 4.16.2 of the packages botbuilder-core, botbuilder-integration-aiohttp, botbuilder-schema, botframework-connector, and botframework-streaming.

Describe the bug

I am attempting to simultaneously do two things:

  1. Send the user a "Bot is typing" activity.
  2. Open a connection to OpenAI, send them the prompt and current conversation state, and allow them to start processing the user's request.

When using TurnContext.send_activity(), it is mostly implemented using an async http library. However, in the circumstance where it is attempting to authenticate for the first time, it uses the requests library to fetch the OIDC configuration and keys. Specifically, it fetches https://login.microsoftonline.com/botframework.com/v2.0/.well-known/openid-configuration and then https://login.microsoftonline.com/<uuid>/oauth2/v2.0/token.

Here is the specific code within botframework-connector that is at issue:

    async def _refresh(self):
        response = requests.get(self.url)
        response.raise_for_status()
        keys_url = response.json()["jwks_uri"]
        response_keys = requests.get(keys_url)
        response_keys.raise_for_status()
        self.last_updated = datetime.now()
        self.keys = response_keys.json()["keys"]

This fetch can take hundreds of milliseconds, so it would be nice if it allowed other coroutines to run while waiting for the HTTP request to come back. The current behavior means that sometimes TurnContext.send_activity will use async IO, and sometimes use sync IO, and there is no way to predict which one.

To Reproduce

I am attempting to achieve this parallelism by using asyncio.gather, like this:

import asyncio

from botbuilder.core.teams import TeamsActivityHandler
from botbuilder.schema import Activity, ActivityTypes

async def get_message_from_ai(message: str):
    # Get past messages in this conversation, open connection to OpenAI, etc.
    pass


class BotHandler(TeamsActivityHandler):

    async def on_message_activity(self, turn_context: TurnContext) -> None:

        # ... snipped for clarity ...

        # Send a typing indicator to Teams
        typing_activity = Activity(type=ActivityTypes.typing)
        typing_activity_coroutine = turn_context.send_activity(typing_activity)

        # Get AI reply
        response_data_coroutine = get_message_from_ai(message)

        # Wait for typing activity and AI reply to finish
        (_, response_data) = await asyncio.gather(typing_activity_coroutine, response_data_coroutine)

This works if the OpenID key has already been requested, but it requires synchronous IO if not.

Expected behavior

TurnContext.send_activity() should always use async IO.

Screenshots

N/A

Additional context

We are using a Django-based bot, using the guide here to integrate it with the rest of Django.

nickodell avatar Jul 08 '25 16:07 nickodell

@axelsrz - Can you take a look at this?

stevkan avatar Jul 08 '25 18:07 stevkan