diff --git a/slack_sdk/web/async_client.py b/slack_sdk/web/async_client.py index 2840b75f7..db7d89ef0 100644 --- a/slack_sdk/web/async_client.py +++ b/slack_sdk/web/async_client.py @@ -33,6 +33,7 @@ _update_call_participants, _validate_for_legacy_client, _warn_if_message_text_content_is_missing, + _warn_if_unknown_kwargs, ) @@ -3292,6 +3293,27 @@ async def chat_postMessage( https://docs.slack.dev/reference/methods/chat.postMessage """ + known_params = { + "channel", + "text", + "as_user", + "attachments", + "blocks", + "thread_ts", + "reply_broadcast", + "unfurl_links", + "unfurl_media", + "container_id", + "icon_emoji", + "icon_url", + "mrkdwn", + "link_names", + "username", + "parse", + "metadata", + "markdown_text", + } + _warn_if_unknown_kwargs("chat.postMessage", kwargs, known_params) kwargs.update( { "channel": channel, diff --git a/slack_sdk/web/client.py b/slack_sdk/web/client.py index 9f4c7fa16..523502ae9 100644 --- a/slack_sdk/web/client.py +++ b/slack_sdk/web/client.py @@ -23,6 +23,7 @@ _update_call_participants, _validate_for_legacy_client, _warn_if_message_text_content_is_missing, + _warn_if_unknown_kwargs, ) @@ -3282,6 +3283,27 @@ def chat_postMessage( https://docs.slack.dev/reference/methods/chat.postMessage """ + known_params = { + "channel", + "text", + "as_user", + "attachments", + "blocks", + "thread_ts", + "reply_broadcast", + "unfurl_links", + "unfurl_media", + "container_id", + "icon_emoji", + "icon_url", + "mrkdwn", + "link_names", + "username", + "parse", + "metadata", + "markdown_text", + } + _warn_if_unknown_kwargs("chat.postMessage", kwargs, known_params) kwargs.update( { "channel": channel, diff --git a/slack_sdk/web/internal_utils.py b/slack_sdk/web/internal_utils.py index 3a6db7203..1414757ed 100644 --- a/slack_sdk/web/internal_utils.py +++ b/slack_sdk/web/internal_utils.py @@ -9,7 +9,7 @@ from http.client import HTTPResponse from io import IOBase from ssl import SSLContext -from typing import Any, Dict, Optional, Sequence, Union +from typing import Any, Dict, Optional, Sequence, Set, Union from urllib.parse import urljoin from urllib.request import HTTPSHandler, OpenerDirector, ProxyHandler, Request, urlopen @@ -321,6 +321,28 @@ def _build_unexpected_body_error_message(body: str) -> str: return message +def _warn_if_unknown_kwargs(endpoint: str, kwargs: Dict[str, Any], known_params: Set[str]) -> None: + """Warn when **kwargs carries keys that are not declared parameters. + + WebClient methods keep **kwargs as an escape hatch for undocumented API + fields, but typos such as thread_id (instead of thread_ts) are otherwise + forwarded silently. See https://github.com/slackapi/python-slack-sdk/issues/1923 + """ + # for unit tests etc. + if os.environ.get("SKIP_SLACK_SDK_WARNING"): + return + + unknown = sorted(key for key in kwargs if key not in known_params) + for key in unknown: + warnings.warn( + f"The `{key}` argument is not a recognized parameter of `{endpoint}` " + f"and will be forwarded to the Slack API as-is. " + f"If this was a typo, the intended parameter may be missing from the request.", + UserWarning, + stacklevel=3, + ) + + def _remove_none_values(d: dict) -> dict: # To avoid having null values in JSON (Slack API does not work with null in many situations) # diff --git a/slack_sdk/web/legacy_client.py b/slack_sdk/web/legacy_client.py index 5c4fa90c1..563e4c346 100644 --- a/slack_sdk/web/legacy_client.py +++ b/slack_sdk/web/legacy_client.py @@ -34,6 +34,7 @@ _update_call_participants, _validate_for_legacy_client, _warn_if_message_text_content_is_missing, + _warn_if_unknown_kwargs, ) @@ -3293,6 +3294,27 @@ def chat_postMessage( https://docs.slack.dev/reference/methods/chat.postMessage """ + known_params = { + "channel", + "text", + "as_user", + "attachments", + "blocks", + "thread_ts", + "reply_broadcast", + "unfurl_links", + "unfurl_media", + "container_id", + "icon_emoji", + "icon_url", + "mrkdwn", + "link_names", + "username", + "parse", + "metadata", + "markdown_text", + } + _warn_if_unknown_kwargs("chat.postMessage", kwargs, known_params) kwargs.update( { "channel": channel, diff --git a/tests/slack_sdk/web/test_web_client_unknown_kwargs_warnings.py b/tests/slack_sdk/web/test_web_client_unknown_kwargs_warnings.py new file mode 100644 index 000000000..ab582c1a8 --- /dev/null +++ b/tests/slack_sdk/web/test_web_client_unknown_kwargs_warnings.py @@ -0,0 +1,47 @@ +import unittest +import warnings + +from slack_sdk import WebClient +from tests.web.mock_web_api_handler import MockHandler +from tests.mock_web_api_server import setup_mock_web_api_server, cleanup_mock_web_api_server + + +class TestWebClientUnknownKwargsWarnings(unittest.TestCase): + def setUp(self): + setup_mock_web_api_server(self, MockHandler) + + def tearDown(self): + cleanup_mock_web_api_server(self) + + def test_warns_on_thread_id_typo_chat_postMessage(self): + client = WebClient(base_url="http://localhost:8888", token="xoxb-api_test") + with self.assertWarnsRegex(UserWarning, "`thread_id` argument is not a recognized parameter"): + resp = client.chat_postMessage( + channel="C111", + text="hi", + thread_id="1750000000.0001", + ) + self.assertIsNone(resp["error"]) + + def test_warns_on_unfurl_link_typo_chat_postMessage(self): + client = WebClient(base_url="http://localhost:8888", token="xoxb-api_test") + with self.assertWarnsRegex(UserWarning, "`unfurl_link` argument is not a recognized parameter"): + resp = client.chat_postMessage( + channel="C111", + text="hi", + unfurl_link=False, + ) + self.assertIsNone(resp["error"]) + + def test_no_warning_for_known_thread_ts(self): + client = WebClient(base_url="http://localhost:8888", token="xoxb-api_test") + with warnings.catch_warnings(record=True) as warning_list: + warnings.simplefilter("always") + resp = client.chat_postMessage( + channel="C111", + text="hi", + thread_ts="1750000000.0001", + ) + unknown = [w for w in warning_list if "not a recognized parameter" in str(w.message)] + self.assertEqual(unknown, []) + self.assertIsNone(resp["error"])