Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions requirements/optional.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,5 @@ SQLAlchemy>=1.4,<3
# websockets 9 is not compatible with Python 3.10
websockets>=9.1,<16
websocket-client>=1,<2
# SSL certificate bundle for certificate verification issues
certifi
3 changes: 2 additions & 1 deletion slack_sdk/web/async_base_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
_get_url,
get_user_agent,
)
from .ssl_utils import create_ssl_context_with_certifi_fallback
from ..proxy_env_variable_loader import load_http_proxy_from_env

from slack_sdk.http_retry.builtin_async_handlers import async_default_handlers
Expand Down Expand Up @@ -55,7 +56,7 @@ def __init__(
"""The maximum number of seconds the client will wait
to connect and receive a response from Slack.
Default is 30 seconds."""
self.ssl = ssl
self.ssl = create_ssl_context_with_certifi_fallback(ssl)
"""An [`ssl.SSLContext`](https://docs.python.org/3/library/ssl.html#ssl.SSLContext)
instance, helpful for specifying your own custom
certificate chain."""
Expand Down
3 changes: 2 additions & 1 deletion slack_sdk/web/base_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
_build_unexpected_body_error_message,
_upload_file_via_v2_url,
)
from .ssl_utils import create_ssl_context_with_certifi_fallback
from .slack_response import SlackResponse
from slack_sdk.http_retry import default_retry_handlers
from slack_sdk.http_retry.handler import RetryHandler
Expand Down Expand Up @@ -67,7 +68,7 @@ def __init__(
"""The maximum number of seconds the client will wait
to connect and receive a response from Slack.
Default is 30 seconds."""
self.ssl = ssl
self.ssl = create_ssl_context_with_certifi_fallback(ssl)
"""An [`ssl.SSLContext`](https://docs.python.org/3/library/ssl.html#ssl.SSLContext)
instance, helpful for specifying your own custom
certificate chain."""
Expand Down
34 changes: 34 additions & 0 deletions slack_sdk/web/ssl_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import os
import ssl
from ssl import SSLContext
from typing import Optional


def has_ssl_env_vars() -> bool:
"""Check if SSL-related environment variables are set"""
ssl_env_vars = ["SSL_CERT_FILE", "SSL_CERT_DIR", "REQUESTS_CA_BUNDLE", "CURL_CA_BUNDLE"]
return any(os.environ.get(var) for var in ssl_env_vars)


def create_ssl_context_with_certifi_fallback(custom_ssl: Optional[SSLContext] = None) -> Optional[SSLContext]:
"""Create SSL context with certifi fallback for certificate issues

Priority:
1. If custom_ssl is provided or SSL env vars are set -> return custom_ssl
2. If certifi is available -> use certifi's certificate bundle
3. Otherwise -> return custom_ssl (usually None)

This helps resolve SSL certificate issues on Windows by using certifi's
curated certificate bundle when no explicit SSL configuration is provided.
"""
# Use custom_ssl if provided, or respect SSL environment variables
if custom_ssl is not None or has_ssl_env_vars():
return custom_ssl

# Fall back to certifi if available (helps with Windows SSL issues)
try:
import certifi

return ssl.create_default_context(cafile=certifi.where())
except ImportError:
return custom_ssl
18 changes: 10 additions & 8 deletions tests/slack_sdk/web/test_web_client_logger.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import logging
import unittest
from unittest.mock import patch

from slack_sdk import WebClient
from slack_sdk.web import base_client
Expand Down Expand Up @@ -39,11 +40,12 @@ def test_logger_property_has_no_setter(self):
client.logger = self.test_logger

def test_ensure_web_client_with_logger_is_copyable(self):
client = WebClient(
base_url="http://localhost:8888",
token="xoxb-api_test",
logger=self.test_logger,
)
client_copy = create_copy(client)
self.assertEqual(client.logger, self.test_logger)
self.assertEqual(client_copy.logger, self.test_logger)
with patch("slack_sdk.web.base_client.create_ssl_context_with_certifi_fallback", return_value=None):
client = WebClient(
base_url="http://localhost:8888",
token="xoxb-api_test",
logger=self.test_logger,
)
client_copy = create_copy(client)
self.assertEqual(client.logger, self.test_logger)
self.assertEqual(client_copy.logger, self.test_logger)
18 changes: 10 additions & 8 deletions tests/slack_sdk_async/web/test_async_web_client_logger.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import logging
import unittest
from unittest.mock import patch

from slack_sdk.web.async_client import AsyncWebClient
from slack_sdk.web import async_base_client
Expand Down Expand Up @@ -39,11 +40,12 @@ def test_logger_property_has_no_setter(self):
client.logger = self.test_logger

def test_ensure_async_web_client_with_logger_is_copyable(self):
client = AsyncWebClient(
base_url="http://localhost:8888",
token="xoxb-api_test",
logger=self.test_logger,
)
client_copy = create_copy(client)
self.assertEqual(client.logger, self.test_logger)
self.assertEqual(client_copy.logger, self.test_logger)
with patch("slack_sdk.web.async_base_client.create_ssl_context_with_certifi_fallback", return_value=None):
client = AsyncWebClient(
base_url="http://localhost:8888",
token="xoxb-api_test",
logger=self.test_logger,
)
client_copy = create_copy(client)
self.assertEqual(client.logger, self.test_logger)
self.assertEqual(client_copy.logger, self.test_logger)