Skip to content

get_thread_context returns None for assistant.user_message handlers when using custom authorize function #1368

Closed

Description

@mgallifrey-dropbox

Reproducible in:

slack bolt 1.24.0

Steps to reproduce:

import os, logging
from slack_bolt.async_app import AsyncApp, AsyncAssistant
from slack_bolt.authorization import AuthorizeResult
from slack_bolt.context.async_context import AsyncBoltContext
from slack_bolt.context.assistant.thread_context_store.default_async_store import (
    DefaultAsyncAssistantThreadContextStore,
)
from slack_sdk.web.async_client import AsyncWebClient

logging.basicConfig(level=logging.INFO)
log = logging.getLogger(__name__)

async def authorize(enterprise_id, team_id, user_id, client: AsyncWebClient) -> AuthorizeResult:
    # typical custom authorize: resolve identity, then set Authorization result
    auth = await client.auth_test(token=os.environ["SLACK_BOT_TOKEN"])
    return AuthorizeResult.from_auth_test_response(
        auth_test_response=auth, bot_token=os.environ["SLACK_BOT_TOKEN"]
    )

app = AsyncApp(authorize=authorize)
assistant = AsyncAssistant()
app.use(assistant)

@assistant.thread_started
async def started(
    say, get_thread_context, context: AsyncBoltContext,
):
     # This works (as long as you're coming from a DM or channel)
    tc = await get_thread_context()
    await say(
        ":wave: Hi! here's you're thread context: {tx}"
    )

@assistant.user_message
async def on_msg(
    say, context: AsyncBoltContext, get_thread_context,
):
    # Prove authorize() ran
    log.info(f"bot_user_id={context.bot_user_id}, channel={context.channel_id}, ts={context.thread_ts}")

    # 1) returns None (likely because context store was created pre-auth)
    tc_via_helper = await get_thread_context()
    log.info(f"get_thread_context() -> {tc_via_helper!r}")

    # 2) Fresh store built *now* (post-auth): returns context
    store = DefaultAsyncAssistantThreadContextStore(context)
    tc_via_store = await store.find(channel_id=context.channel_id, thread_ts=context.thread_ts)
    log.info(f"DefaultAsyncAssistantThreadContextStore.find(...) -> {tc_via_store!r}")

    await say(f"helper={tc_via_helper!r} | via_store={tc_via_store!r}")

Expected result:

get_thread_context should work in user_message threads regardless of authorization method used.

Actual result:

get_thread_context returns None in user messages if using a custom authorize

Analysis

This seems to happen because the DefaultAssistantThreadContextStore is initialized with a context that does not have a bot_user_id in it. This happens because init_context runs before middlewares (of which the custom authorize is one), runs.

#1346 seems like it may be related

Requirements

Please read the Contributing guidelines and Code of Conduct before creating this issue or pull request. By submitting, you are agreeing to those rules.

(Tell what actually happened with logs, screenshots)

Requirements

Please read the Contributing guidelines and Code of Conduct before creating this issue or pull request. By submitting, you are agreeing to those rules.

Activity

  1. WilliamBergamin commented on Sep 15, 2025

    @WilliamBergamin
    Contributor

    Hi @mgallifrey-dropbox thanks for bringing this up and providing a detailed description 💯

    This seems like a bug! I spent some time trying to figure out where this behavior originates from, but no luck. We will need to allocate more resources in order to result this issue

    I'm vaguely aware that the event handling around @assistant.user_message is different from other event handlers due to its similarities with the standard message payload. I suspect this may cause the handler to execute before the authorize handler

  2. mgallifrey-dropbox commented on Sep 15, 2025

    @mgallifrey-dropbox
    Author

    Thanks for taking a look!

    I suspect this may cause the handler to execute before the authorize handler

    For what it's worth, the handler runs after the authorize middleware (this is why DefaultAsyncAssistantThreadContextStore(context).find in the body of the handler works); it's the initialization of DefaultAsyncAssistantThreadContextStore and the BoltContext it uses that happens before the authorization middleware.

  3. mgallifrey-dropbox commented on Sep 16, 2025

    @mgallifrey-dropbox
    Author

    In case anyone else encounters this an needs a workaround, you can define the following private methods and call them from your handler instead of the kwargs provided methods:

    async def _get_thread_context(context: AsyncBoltContext) -> Optional[AssistantThreadContext]:
        store = DefaultAsyncAssistantThreadContextStore(context)
        return await store.find(channel_id=context.channel_id, thread_ts=context.thread_ts)
    
    async def _save_thread_context(context: AsyncBoltContext, thread_context: dict[str, str]) -> None:
        store = DefaultAsyncAssistantThreadContextStore(context)
        return await store.save(channel_id=context.channel_id, thread_ts=context.thread_ts, context=thread_context)
    

    This works because DefaultAsyncAssistantThreadContextStore can be initialized with an AsyncBoltContext that has been fully populated.

  4. Cr1stal commented on May 30, 2026

    @Cr1stal

    I took another look at this and verified the behavior against the old and current code paths.

    This does reproduce on v1.24.0: the default assistant thread context store is initialized before custom authorize has populated context.bot_user_id, so get_thread_context() can miss the parent bot reply and return None.

    This appears to have been fixed by #1456 (785b813, fix(assistant): improve middleware dispatch and inject kwargs in middleware), which is included in v1.28.0. After that change, assistant conversation kwargs such as get_thread_context / save_thread_context are injected from middleware after authorization has run, so the default store sees the populated bot_user_id.

    So the practical fix should be to upgrade to slack-bolt>=1.28.0. I confirmed the same regression scenario passes on current main, and git tag --contains 785b813 shows v1.28.0 as the released version containing the fix.

  5. WilliamBergamin commented on Jul 28, 2026

    @WilliamBergamin
    Contributor

    Closing since #1456 is the fix

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions