Skip to content

Commit 9524a9a

Browse files
authored
Merge pull request #716 from seratch/issue-712
Fix #712 by adding timeout arg support in sync WebClient/WebhookClient
2 parents af377a5 + d0e5b8a commit 9524a9a

File tree

6 files changed

+63
-8
lines changed

6 files changed

+63
-8
lines changed

slack/web/base_client.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -558,7 +558,9 @@ def _perform_urllib_http_request(
558558
f"Invalid proxy detected: {self.proxy} must be a str value"
559559
)
560560

561-
resp: HTTPResponse = urlopen(req, context=self.ssl)
561+
resp: HTTPResponse = urlopen(
562+
req, context=self.ssl, timeout=self.timeout
563+
)
562564
charset = resp.headers.get_content_charset()
563565
body: str = resp.read().decode(charset) # read the response body here
564566
return {"status": resp.code, "headers": resp.headers, "body": body}

slack/webhook/client.py

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import json
22
import logging
3+
import re
34
from http.client import HTTPResponse
5+
from ssl import SSLContext
46
from typing import Dict, Union, List, Optional
57
from urllib.error import HTTPError
68
from urllib.request import Request, urlopen
@@ -16,13 +18,24 @@ class WebhookClient:
1618
logger = logging.getLogger(__name__)
1719

1820
def __init__(
19-
self, url: str, default_headers: Dict[str, str] = {},
21+
self,
22+
url: str,
23+
timeout: int = 30,
24+
ssl: Optional[SSLContext] = None,
25+
proxy: Optional[str] = None,
26+
default_headers: Dict[str, str] = {},
2027
):
2128
"""API client for Incoming Webhooks and response_url
2229
:param url: a complete URL to send data (e.g., https://hooks.slack.com/XXX)
30+
:param timeout: request timeout (in seconds)
31+
:param ssl: ssl.SSLContext to use for requests
32+
:param proxy: proxy URL (e.g., localhost:9000, http://localhost:9000)
2333
:param default_headers: request headers to add to all requests
2434
"""
2535
self.url = url
36+
self.timeout = timeout
37+
self.ssl = ssl
38+
self.proxy = proxy
2639
self.default_headers = default_headers
2740

2841
def send(
@@ -65,11 +78,11 @@ def send_dict(
6578
body = convert_bool_to_0_or_1(body)
6679
self._parse_web_class_objects(body)
6780
return self._perform_http_request(
68-
url=self.url, body=body, headers=self._build_request_headers(headers),
81+
body=body, headers=self._build_request_headers(headers)
6982
)
7083

7184
def _perform_http_request(
72-
self, *, url: str, body: Dict[str, any], headers: Dict[str, str]
85+
self, *, body: Dict[str, any], headers: Dict[str, str]
7386
) -> WebhookResponse:
7487
"""Performs an HTTP request and parses the response.
7588
:param url: a complete URL to send data (e.g., https://hooks.slack.com/XXX)
@@ -85,19 +98,24 @@ def _perform_http_request(
8598
f"Sending a request - url: {self.url}, body: {body}, headers: {headers}"
8699
)
87100
try:
101+
url = self.url
88102
# for security
89103
if url.lower().startswith("http"):
90104
req = Request(
91105
method="POST", url=url, data=body.encode("utf-8"), headers=headers
92106
)
107+
if self.proxy is not None:
108+
host = re.sub("^https?://", "", self.proxy)
109+
req.set_proxy(host, "http")
110+
req.set_proxy(host, "https")
93111
else:
94112
raise SlackRequestError(f"Invalid URL detected: {url}")
95113

96-
resp: HTTPResponse = urlopen(req)
114+
resp: HTTPResponse = urlopen(req, context=self.ssl, timeout=self.timeout)
97115
charset: str = resp.headers.get_content_charset() or "utf-8"
98116
response_body: str = resp.read().decode(charset)
99117
resp = WebhookResponse(
100-
url=self.url,
118+
url=url,
101119
status_code=resp.status,
102120
body=response_body,
103121
headers=resp.headers,
@@ -109,7 +127,7 @@ def _perform_http_request(
109127
charset: str = e.headers.get_content_charset() or "utf-8"
110128
response_body: str = resp.read().decode(charset)
111129
resp = WebhookResponse(
112-
url=self.url, status_code=e.code, body=response_body, headers=e.headers,
130+
url=url, status_code=e.code, body=response_body, headers=e.headers,
113131
)
114132
if e.code == 429:
115133
# for backward-compatibility with WebClient (v.2.5.0 or older)

tests/web/mock_web_api_server.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import logging
33
import re
44
import threading
5+
import time
56
from http import HTTPStatus
67
from http.server import HTTPServer, SimpleHTTPRequestHandler
78
from typing import Type
@@ -90,6 +91,13 @@ def _handle(self):
9091
self.wfile.close()
9192
return
9293

94+
if pattern == "timeout":
95+
time.sleep(2)
96+
self.send_response(200)
97+
self.wfile.write("""{"ok":true}""".encode("utf-8"))
98+
self.wfile.close()
99+
return
100+
93101
if request_body and "cursor" in request_body:
94102
page = request_body["cursor"]
95103
pattern = f"{pattern}_{page}"

tests/web/test_web_client.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import asyncio
22
import re
3+
import socket
34
import unittest
45

56
import slack.errors as err
@@ -211,3 +212,14 @@ async def test_token_param_async(self):
211212
self.assertIsNone(resp["error"])
212213
with self.assertRaises(err.SlackApiError):
213214
await client.users_list()
215+
216+
def test_timeout_issue_712(self):
217+
client = WebClient(base_url="http://localhost:8888", timeout=1)
218+
with self.assertRaises(socket.timeout):
219+
client.users_list(token="xoxb-timeout")
220+
221+
@async_test
222+
async def test_timeout_issue_712_async(self):
223+
client = WebClient(base_url="http://localhost:8888", timeout=1, run_async=True)
224+
with self.assertRaises(asyncio.TimeoutError):
225+
await client.users_list(token="xoxb-timeout")

tests/webhook/mock_web_api_server.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import json
21
import logging
32
import re
43
import threading
4+
import time
55
from http import HTTPStatus
66
from http.server import HTTPServer, SimpleHTTPRequestHandler
77
from typing import Type
@@ -28,6 +28,9 @@ def set_common_headers(self):
2828

2929
def do_POST(self):
3030
try:
31+
if self.path == "/timeout":
32+
time.sleep(2)
33+
3134
body = "ok"
3235

3336
self.send_response(HTTPStatus.OK)

tests/webhook/test_webhook.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import unittest
2+
import socket
3+
import urllib
24

35
from slack.web.classes.attachments import Attachment, AttachmentField
46
from slack.web.classes.blocks import SectionBlock, ImageBlock
@@ -154,3 +156,13 @@ def test_send_dict(self):
154156
resp: WebhookResponse = client.send_dict({"text": "hello!"})
155157
self.assertEqual(200, resp.status_code)
156158
self.assertEqual("ok", resp.body)
159+
160+
def test_timeout_issue_712(self):
161+
client = WebhookClient(url="http://localhost:8888/timeout", timeout=1)
162+
with self.assertRaises(socket.timeout):
163+
client.send_dict({"text": "hello!"})
164+
165+
def test_proxy_issue_714(self):
166+
client = WebhookClient(url="http://localhost:8888", proxy="http://invalid-host:9999")
167+
with self.assertRaises(urllib.error.URLError):
168+
client.send_dict({"text": "hello!"})

0 commit comments

Comments
 (0)