Using callable object instances as middleware does not work.
See original GitHub issueSummary
The various methods for registering middleware in Bolt ostensibly take any Callable
item. If the middleware is an instance of a callable class (i.e. a class that defines a __call__
method) rather than a function or method, and if the logging level is turned up DEBUG
, then Bolt throws an exception just before it would have called the middleware.
Registering a callable object as middleware is useful for a variety of activities such as authentication handlers that need to maintain context or connections to other services.
Reproducible in:
The slack_bolt
version
slack-bolt==1.2.1 slack-sdk==3.2.0
Python runtime version
Python 3.8.2
OS info
ProductName: Mac OS X ProductVersion: 10.15.7 BuildVersion: 19H2 Darwin Kernel Version 19.6.0: Mon Aug 31 22:12:52 PDT 2020; root:xnu-6153.141.2~1/RELEASE_X86_64
Steps to reproduce:
Create some callable class:
class MyMiddleware:
def __call__(self, body, context, logger):
user = context['foo'] = body["user_id"] if "user_id" in body else body["user"]["id"]
logger.debug(f"The user is: {user}")
Turn the logging level up to DEBUG
:
import logging
logging.basicConfig(level=logging.DEBUG)
Create an app that uses this middleware:
app = slack_bolt.App()
my_middleware = MyMiddleware()
app.use(my_middleware)
@app.command("/foobar")
def my_command(ack):
ack("Hello!")
Run the Bolt app and trigger a handler
Expected result:
The Middleware object’s __call__
method should be called prior to any handlers.
Actual result:
The Bolt framework throws an AttributeError
exception in middleware/custom_middleware.py
because, unlike a regular function or method, a callable object does not have a __name__
attribute.
Analysis
The problem is caused by debug logging in app/app.py trying to read the name
property of the CustomMiddleware
object, which naïvely attempts to read the __name__
attribute of the registered callable, but instances of callable classes do not have this attribute. The same bug is present in AsyncCustomMiddleware
.
It is possible that a related bug exists (without needing the logging level to be turned up) if a callable object instance is used for a “lazy function”, since multiple pieces of code in listener/thread_runner.py
and listener/asyncio_runner.py
attempt to check the lazy function __name__
attribute against request.lazy_function_name
.
The solution to this is likely to introduce a new utility function to find the name of a callable, e.g.:
def callable_name(func):
if hasattr(func, "__name__"):
return func.__name__
else:
return f"{func. __class__.__module__}.{func.__class__.__name__}"
and then use this wherever we need the name of a callable provided by the user.
Issue Analytics
- State:
- Created 3 years ago
- Comments:5 (5 by maintainers)
Hey @nickovs, thanks for catching this bug, providing a well thought out and detailed analysis, and suggesting an elegant solution. 👌🏻
Your suggestion appears sound to me, but I’m going to let @seratch step-in on this issue to discuss the solution before we open a pull request to solve.
Thanks for your contribution #216 🎉 I will release a new patch version shortly.