Repository: slackapi/python-slack-sdk
Pull request #1594 (2024-11-19 02:34:19)

add trailing slash to the end of the url
## Summary
This PR addresses issue #1541 by ensuring that a trailing `/` is appended to the end of the URL in base_url if it is not already provided. This change ensures consistent URL formatting and avoids potential issues caused by missing trailing slashes in API calls.

### Testing

1. Unit Tests
Run the following command to ensure all relevant test cases pass:
```bash
./scripts/run_unit_tests.sh tests/slack_sdk/web/test_web_client.py
```
2. Manual Testing
- Use a custom `base_url` without a trailing `/` in a sample implementation. Confirm that the URL is automatically appended with a `/ `during API calls.
- Use a `base_url` with a trailing `/` and verify that no additional `/` is appended.

### Category <!-- place an `x` in each of the `[ ]`  -->

- [x ] **slack_sdk.web.WebClient (sync/async)** (Web API client)
- [ ] **slack_sdk.webhook.WebhookClient (sync/async)** (Incoming Webhook, response_url sender)
- [ ] **slack_sdk.socket_mode** (Socket Mode client)
- [ ] **slack_sdk.signature** (Request Signature Verifier)
- [ ] **slack_sdk.oauth** (OAuth Flow Utilities)
- [ ] **slack_sdk.models** (UI component builders)
- [ ] **slack_sdk.scim** (SCIM API client)
- [ ] **slack_sdk.audit_logs** (Audit Logs API client)
- [ ] **slack_sdk.rtm_v2** (RTM client)
- [ ] `/docs` (Documents)
- [ ] `/tutorial` (PythOnBoardingBot tutorial)
- [ ] `tests`/`integration_tests` (Automated tests for this library)

## Requirements <!-- place an `x` in each `[ ]` -->

- [x ] I've read and understood the [Contributing Guidelines](https://github.com/slackapi/python-slack-sdk/blob/main/.github/contributing.md) and have done my best effort to follow them.
- [x ] I've read and agree to the [Code of Conduct](https://slackhq.github.io/code-of-conduct).
- [x ] I've run `python3 -m venv .venv && source .venv/bin/activate && ./scripts/run_validation.sh` after making the changes.


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/slackapi/python-slack-sdk/pull/1594?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=slackapi) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 84.89%. Comparing base [(`d1dffb7`)](https://app.codecov.io/gh/slackapi/python-slack-sdk/commit/d1dffb773b0d1b7bb263e0a82039134200df5038?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=slackapi) to head [(`b491388`)](https://app.codecov.io/gh/slackapi/python-slack-sdk/commit/b491388ffb4998ca4b28d2314a94040b4996f1a7?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=slackapi).

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main    #1594      +/-   ##
==========================================
- Coverage   84.91%   84.89%   -0.03%     
==========================================
  Files         113      113              
  Lines       12622    12628       +6     
==========================================
+ Hits        10718    10720       +2     
- Misses       1904     1908       +4     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/slackapi/python-slack-sdk/pull/1594?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=slackapi).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=slackapi).

----
🚨 Try these New Features:

- [Flaky Tests Detection](https://docs.codecov.com/docs/test-result-ingestion-beta) - Detect and resolve failed and flaky tests

Patch:
diff --git a/slack_sdk/web/async_base_client.py b/slack_sdk/web/async_base_client.py
index 2418b08..65f852b 100644
--- a/slack_sdk/web/async_base_client.py
+++ b/slack_sdk/web/async_base_client.py
@@ -46,6 +46,8 @@ class AsyncBaseClient:
     ):
         self.token = None if token is None else token.strip()
         """A string specifying an `xoxp-*` or `xoxb-*` token."""
+        if not base_url.endswith("/"):
+            base_url += "/"
         self.base_url = base_url
         """A string representing the Slack API base URL.
         Default is `'https://slack.com/api/'`."""
diff --git a/slack_sdk/web/base_client.py b/slack_sdk/web/base_client.py
index e112d35..4ce67b0 100644
--- a/slack_sdk/web/base_client.py
+++ b/slack_sdk/web/base_client.py
@@ -59,6 +59,8 @@ class BaseClient:
     ):
         self.token = None if token is None else token.strip()
         """A string specifying an `xoxp-*` or `xoxb-*` token."""
+        if not base_url.endswith("/"):
+            base_url += "/"
         self.base_url = base_url
         """A string representing the Slack API base URL.
         Default is `'https://slack.com/api/'`."""
diff --git a/slack_sdk/web/legacy_base_client.py b/slack_sdk/web/legacy_base_client.py
index c58e062..44d4997 100644
--- a/slack_sdk/web/legacy_base_client.py
+++ b/slack_sdk/web/legacy_base_client.py
@@ -62,6 +62,8 @@ class LegacyBaseClient:
     ):
         self.token = None if token is None else token.strip()
         """A string specifying an `xoxp-*` or `xoxb-*` token."""
+        if not base_url.endswith("/"):
+            base_url += "/"
         self.base_url = base_url
         """A string representing the Slack API base URL.
         Default is `'https://slack.com/api/'`."""


Test patch:
diff --git a/tests/slack_sdk/web/test_web_client.py b/tests/slack_sdk/web/test_web_client.py
index 024915a..e6c6697 100644
--- a/tests/slack_sdk/web/test_web_client.py
+++ b/tests/slack_sdk/web/test_web_client.py
@@ -229,3 +229,11 @@ class TestWebClient(unittest.TestCase):
             user_auth_blocks=[DividerBlock(), DividerBlock()],
         )
         self.assertIsNone(new_message.get("error"))
+
+    def test_base_url_appends_trailing_slash_issue_15141(self):
+        client = self.client
+        self.assertEqual(client.base_url, "http://localhost:8888/")
+
+    def test_base_url_preserves_trailing_slash_issue_15141(self):
+        client = WebClient(base_url="http://localhost:8888/")
+        self.assertEqual(client.base_url, "http://localhost:8888/")