Skip to content

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

@nickodell

Description

@nickodell

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.

Metadata

Metadata

Assignees

Labels

bugIndicates an unexpected problem or an unintended behavior.needs-triageThe issue has just been created and it has not been reviewed by the team.

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions