message event in one app being sent to Request URL in another app
I have two Slack apps under the same team / workspace:
- App 1. Production app that is set to
POSTevents to a Request URL / HTTPS endpoint - App 2. Development app that is set to use WebSockets
My goal is to develop under App 2 before deploying to production under App 1. I have App 1 deployed on a k8s cluster in AWS that receives message events with Bolt just fine, and events are POSTed to my request URL.
If I do a simple request with App 2's tokens and channel IDs with chat.postMessage/test endpoint here, it successfully delivers the message to App 2's channel, but I see from App 1's production logs that it also attempts to send a POST request to App 1's Request URL configured. I ensured that App 1 and App 2 tokens are not being mixed up. My assumption was that if I configured my Bolt app with App 2's tokens, then the apps would be isolated (i.e. a message sent to App 2 is not sent to App 1)
Reproducible in:
The slack_bolt version
# pip freeze | grep slack
slack-bolt==1.18.0
slack-sdk==3.26.1
Python runtime version
# python --version
Python 3.11.7
OS info
# uname -v
#1 SMP PREEMPT Mon Nov 8 11:22:26 UTC 202
I'm running Slack bolt app inside a docker container. The above is running the command inside a running container.
Steps to reproduce:
Configure two apps in the same workspace team
- One with Socket Mode off and
POSTs requests to the Request URL - One with Socket Mode on
- Install both apps in the same workspace (the two apps are listed under the
Appssection of the sidebar in the Slack desktop app - Visit chat.postMessage/test
- Post a message using App 2's Bot token and channel ID obtained from App 2 in the
Appssection of the sidebar - Observe that the message was sent to App 2's channel, but it also sent a payload to App 1's endpoint
Expected result:
Message from chat.postMessage/test is sent only to App 2's channel and payload is NOT sent to App1's Request URL
Actual result:
The chat.postMessage/test message was sent to App 2's channel successfully, but it also sent a payload to App 1's Request URL.
Here's a snippet of App 1's logs
[2023-12-27 11:58:39] {__init__.py:202} DEBUG - Installation data missing for enterprise: none, team: <redacted>: [Errno 2] No such file or directory: '/mnt/efs/slack/installations/none-<redacted>/installer-<redacted>-latest'
[2023-12-27 11:58:39] {base_client.py:251} DEBUG - Sending a request - url: https://www.slack.com/api/auth.test, query_params: {}, body_params: {'team_id': '<redacted>'}, files: {}, json_body: None, headers: {'Content-Type': 'application/x-www-form-urlencoded', 'Authorization': '(redacted)', 'User-Agent': 'Python/3.11.7 slackclient/3.26.1 Linux/5.10.201-191.748.amzn2.x86_64'}
[2023-12-27 11:58:39] {base_client.py:542} DEBUG - Received the following response - status: 200, headers: {'date': 'Wed, 27 Dec 2023 11:58:39 GMT', 'server': 'Apache', 'vary': 'Accept-Encoding', 'x-slack-req-id': 'a714a5f3b034471fea55b601b501685a', 'x-content-type-options': 'nosniff', 'x-xss-protection': '0', 'pragma': 'no-cache', 'cache-control': 'private, no-cache, no-store, must-revalidate', 'expires': 'Sat, 26 Jul 1997 05:00:00 GMT', 'content-type': 'application/json; charset=utf-8', 'x-oauth-scopes': 'channels:read,groups:read,chat:write,app_mentions:read,channels:history,commands,groups:history,im:history,incoming-webhook,mpim:history,users.profile:read,reactions:read', 'access-control-expose-headers': 'x-slack-req-id, retry-after', 'access-control-allow-headers': 'slack-route, x-slack-version-ts, x-b3-traceid, x-b3-spanid, x-b3-parentspanid, x-b3-sampled, x-b3-flags', 'strict-transport-security': 'max-age=31536000; includeSubDomains; preload', 'referrer-policy': 'no-referrer', 'x-slack-unique-id': 'ZYwRbyiCpuimxPjc3FTrSQAAECE', 'x-slack-backend': 'r', 'access-control-allow-origin': '*', 'content-length': '196', 'via': '1.1 slack-prod.tinyspeck.com, envoy-www-iad-movnsdlb, envoy-edge-pdx-bivryhjg', 'x-envoy-attempt-count': '1', 'x-envoy-upstream-service-time': '153', 'x-backend': 'main_normal main_canary_with_overflow main_control_with_overflow', 'x-server': 'slack-www-hhvm-main-iad-pxeh', 'x-slack-shared-secret-outcome': 'no-match', 'x-edge-backend': 'envoy-www', 'x-slack-edge-shared-secret-outcome': 'no-match', 'connection': 'close'}, body: {"ok":true,"url":"https:\/\/<redacted>.slack.com\/","team":"<redacted>","user":"<redacted>","team_id":"<redacted>","user_id":"<redacted>","bot_id":"<redacted>","is_enterprise_install":false}
[2023-12-27 11:58:39] {listener_error_handler.py:67} ERROR - Failed to run listener function (error: 1 validation error for SlackMessagePayload
event -> client_msg_id
field required (type=value_error.missing))
Traceback (most recent call last):
File "/usr/local/lib/python3.11/site-packages/slack_bolt/listener/thread_runner.py", line 120, in run_ack_function_asynchronously
listener.run_ack_function(request=request, response=response)
File "/usr/local/lib/python3.11/site-packages/slack_bolt/listener/custom_listener.py", line 50, in run_ack_function
return self.ack_function(
^^^^^^^^^^^^^^^^^^
File "/var/www/app/routers/router.py", line 80, in handle_message
message = SlackMessagePayload(**body)
^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "pydantic/main.py", line 341, in pydantic.main.BaseModel.__init__
pydantic.error_wrappers.ValidationError: 1 validation error for SlackMessagePayload
event -> client_msg_id
field required (type=value_error.missing)
INFO: 172.16.1.83:29506 - "POST /app1/endpoint HTTP/1.1" 200 OK # the endpoint configured in App 1
The Exception above is expected, because my production app is receiving a payload that it's not expecting. This is how the Bolt app is configured in production
oauth_settings = OAuthSettings(
client_id=SLACK_CLIENT_ID,
client_secret=SLACK_CLIENT_SECRET,
scopes=oauth_required_scopes,
installation_store=FileInstallationStore(base_dir="/mnt/efs/slack/installations"),
state_store=FileOAuthStateStore(expiration_seconds=600, base_dir="/mnt/efs/slack/states")
)
bolt_app = App(
signing_secret=SLACK_SIGNING_SECRET,
oauth_settings=oauth_settings
)
bolt_app_handler = SlackRequestHandler(bolt_app)
@bolt_app.event("message")
def handle_message(body, say, logger):
...
## Requirements
Please read the [Contributing guidelines](https://github.com/slackapi/bolt-python/blob/main/.github/contributing.md) and [Code of Conduct](https://slackhq.github.io/code-of-conduct) before creating this issue or pull request. By submitting, you are agreeing to those rules.
Hi, @christopherhan! Thanks so much for submitting this question and sorry to hear that you're running into some trouble.
Also, thanks for the detailed responses! Just to get a better idea of the issue - if you attempt to make a request through that same link for App 1, does App 2 receive the request also, or is it solely just when you use App 2's credentials that App 1 also receives the request payload?
@hello-ashleyintech It only seems to be when I use App 2's credentials.
If I used App 1's bot token and channel ID in the chat.postMessage/test, App 1 receives the payload successfully and posts the message in App 1's channel. App 2 doesn't seem to receive the request. I tested again with App 2's bot token and channel, and App 1 is also receiving the request.
@christopherhan Thanks so much for verifying that for me! 🙌 Would you be able to share your app config file (excluding any sensitive information) for App 2?
@hello-ashleyintech . This is the manifest from App2. Is this what you're asking?
display_information:
name: App2
features:
app_home:
home_tab_enabled: true
messages_tab_enabled: false
messages_tab_read_only_enabled: false
bot_user:
display_name: App2
always_online: false
oauth_config:
scopes:
user:
- im:history
bot:
- im:write
- im:read
- chat:write
- links:write
- groups:read
- mpim:read
- mpim:history
- commands
- files:read
- files:write
- team:read
- incoming-webhook
- app_mentions:read
settings:
event_subscriptions:
user_events:
- message.im
bot_events:
- app_mention
interactivity:
is_enabled: true
org_deploy_enabled: false
socket_mode_enabled: true
token_rotation_enabled: false
Thanks for sharing @christopherhan! 🙌 Apologies for the confusion - by app config, I meant the app.py file that initializes your app. An example can be found here.
Let me know if you have further questions!
@hello-ashleyintech I'm running a FastAPI project. Here's my main.py in the project root. This is what's running in production. Note, you won't see any WebSockets related code/config here, so messages are no longer get posted to App2's Slack channel, but payloads are still sent to App1.
import os
import logging
from logging.config import dictConfig
from fastapi import FastAPI, Request, Response
from fastapi.middleware.cors import CORSMiddleware
from starlette.background import BackgroundTask
from starlette.types import Message
from api.conf.log import log_config
from api.routers import (
slack_router,
)
from api.db.connection import engine
from api.db.models import *
dictConfig(log_config)
logger = logging.getLogger(__name__)
APP_ENV = os.getenv('APP_ENV', 'development')
if APP_ENV == 'production':
origins = [
"https://api.app.com",
"https://app.app.com",
]
else:
origins = [
"http://localhost:8080",
"http://localhost:5100",
]
app = FastAPI()
def log_info(req_body, res_body):
logging.info(req_body)
logging.info(res_body)
async def set_body(request: Request, body: bytes):
async def receive() -> Message:
return {'type': 'http.request', 'body': body}
request._receive = receive
@app.middleware('http')
async def LogMiddleware(request: Request, call_next):
req_body = await request.body()
await set_body(request, req_body)
response = await call_next(request)
res_body = b''
async for chunk in response.body_iterator:
res_body += chunk
task = BackgroundTask(log_info, req_body, res_body)
return Response(content=res_body, status_code=response.status_code,
headers=dict(response.headers), media_type=response.media_type, background=task)
app.include_router(slack_router.router)
Base.metadata.create_all(bind=engine)
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["GET", "POST"],
allow_headers=["*"],
)
@app.get("/")
async def root():
return {"message": "Hello, World!"}
The bolt app is initialized in another file, routers/slack_router.py
import os
import logging
from fastapi import APIRouter, Request
from api.models.slack import SlackMessagePayload
from slack_bolt import App
from slack_bolt.adapter.fastapi import SlackRequestHandler
from slack_bolt.oauth.oauth_settings import OAuthSettings
from slack_sdk.oauth.installation_store import FileInstallationStore
from slack_sdk.oauth.state_store import FileOAuthStateStore
logger = logging.getLogger(__name__)
SLACK_CLIENT_ID = os.getenv('SLACK_CLIENT_ID')
SLACK_CLIENT_SECRET = os.getenv('SLACK_CLIENT_SECRET')
SLACK_SIGNING_SECRET = os.getenv('SLACK_SIGNING_SECRET')
# These need to align with the Slack events we're receiving
oauth_required_scopes = [
"channels:read",
"groups:read",
"chat:write",
"app_mentions:read",
"reactions:read"
]
oauth_settings = OAuthSettings(
client_id=SLACK_CLIENT_ID,
client_secret=SLACK_CLIENT_SECRET,
scopes=oauth_required_scopes,
installation_store=FileInstallationStore(base_dir="/mnt/efs/slack/installations"),
state_store=FileOAuthStateStore(expiration_seconds=600, base_dir="/mnt/efs/slack/states")
)
bolt_app = App(
signing_secret=SLACK_SIGNING_SECRET,
oauth_settings=oauth_settings
)
bolt_app_handler = SlackRequestHandler(bolt_app)
router = APIRouter()
@router.get("/slack/install")
async def install(req: Request):
return await bolt_app_handler.handle(req)
@router.get("/slack/oauth_redirect")
async def oauth_redirect(req: Request):
return await bolt_app_handler.handle(req)
@router.post("/api/v1/slack/message")
async def endpoint(req: Request):
return await bolt_app_handler.handle(req)
@bolt_app.event("message")
def handle_message(body, say, logger):
if 'type' in body:
event_type = body['type']
if event_type == 'url_verification':
say(body['challenge'])
if event_type == 'event_callback':
message = SlackMessagePayload(**body)
say('Hello World')
@bolt_app.event("app_mention")
def handle_app_mention(body, say):
message = SlackMessagePayload(**body)
say('Hello World')
When building the Bolt app with FastAPI, I referenced the examples here
Hi, @christopherhan! Apologies for the delayed reply, I just returned from some time off and am getting around to catching up on messages!
I'm taking a look at your implementation and also the provided examples, will drop back in if I find anything! In the meantime, just wanted to check in and see if you had any updates or if you were still dealing with this issue!
@hello-ashleyintech still having the same issue. no changes since my last message. thanks for taking a look.
If the App 1's bot user is a member of the channel you're using for App 2 test, this behavior should not be surprising. Since App 1 subscribes to all message events that occur in places where its bot user has the access.
@seratch hmm, I'm using App 2's bot token in the tester, and the message event payload is being posted to App1's Request URL (I see in my server logs). So App1 and not a member of any channels of App2
Alternatively, if I message App2 in it's channel, then the payload gets sent to App1's Request URL
I mean receiving message events is not connected to what token is used for posting a message. Again, the Request URL just receives any message events that the app's bot user has the access. What happens if you post a message as a human in the channel? If the App 1's bot user is in the channel, App 1's message listener will be executed as you observe with App 2's bot user message. If you use the App 1 itself's bot token, the Request URL still receives the message event, but Bolt framework skips executing the message event listener for you to prevent infinite loop of responding to its own message.
👋 It looks like this issue has been open for 30 days with no activity. We'll mark this as stale for now, and wait 10 days for an update or for further comment before closing this issue out. If you think this issue needs to be prioritized, please comment to get the thread going again! Maintainers also review issues marked as stale on a regular basis and comment or adjust status if the issue needs to be reprioritized.
As this issue has been inactive for more than one month, we will be closing it. Thank you to all the participants! If you would like to raise a related issue, please create a new issue which includes your specific details and references this issue number.