fix(gateway): keep plugin action wrapper signature to (ack, body, action)

The previous implementation captured loop vars via default arguments::

    async def _wrapped(ack, body, action, _cb=_cb, _plugin_name=_plugin_name):

slack_bolt's ``kwargs_injection`` introspects each listener's signature
via ``inspect.signature`` and passes ``None`` for any parameter name it
doesn't recognise (see ``slack_bolt/kwargs_injection/async_utils.py``
``build_async_required_kwargs``). That clobbered ``_cb`` to ``None`` at
dispatch time, so the wrapped plugin handler became ``NoneType`` —
``await _cb(...)`` then raised ``'NoneType' object is not callable`` and
no plugin action handler ever fired.

Replace the default-arg trick with a small closure factory so the
wrapper's public signature is exactly ``(ack, body, action)``. Add a
regression test that introspects the wrapped function's signature.

Found via real Slack click on a Block Kit button registered through
``ctx.register_slack_action_handler`` — gateway log showed
``[Slack] Plugin 'None' action handler raised: 'NoneType' object is
not callable`` despite the registration log line confirming the
handler was wired.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Brad Smith 2026-05-06 09:20:34 -05:00 committed by kshitijk4poor
parent 62e937bf2b
commit 08e8bedae8
2 changed files with 42 additions and 6 deletions

View file

@ -968,22 +968,30 @@ class SlackAdapter(BasePlatformAdapter):
)
_plugin_handlers = []
for _action_id, _cb, _plugin_name in _plugin_handlers:
# Capture loop vars per iteration via default args.
async def _wrapped(ack, body, action, _cb=_cb, _plugin_name=_plugin_name):
# Closure factory — keeps the wrapper's signature limited to
# ``(ack, body, action)``. slack_bolt inspects listener
# signatures via ``inspect.signature`` and passes ``None`` for
# any parameter name it doesn't recognise, so capturing loop
# vars as default args (``_cb=_cb`` etc.) silently clobbers
# them at dispatch time.
def _make_wrapper(cb, plugin_name):
async def _wrapped(ack, body, action):
try:
await _cb(ack, body, action)
await cb(ack, body, action)
except Exception as exc: # pragma: no cover - defensive
logger.error(
"[Slack] Plugin '%s' action handler raised: %s",
_plugin_name, exc, exc_info=True,
plugin_name, exc, exc_info=True,
)
# Best-effort ack so Slack doesn't retry the click.
try:
await ack()
except Exception:
pass
self._app.action(_action_id)(_wrapped)
return _wrapped
for _action_id, _cb, _plugin_name in _plugin_handlers:
self._app.action(_action_id)(_make_wrapper(_cb, _plugin_name))
logger.debug(
"[Slack] Registered plugin action handler %s (from %s)",
_action_id, _plugin_name,