Repository: slackapi/python-slackclient (python)

Issue:
Raise a more relevant error when getting a JSONDecodeError for some reasons
### Description

Describe your issue here.

### What type of issue is this? (place an `x` in one of the `[ ]`)
- [X] bug
- [ ] enhancement (feature request)
- [ ] question
- [ ] documentation related
- [ ] testing related
- [ ] discussion

### Requirements (place an `x` in each of the `[ ]`)
* [X] I've read and understood the [Contributing guidelines](https://github.com/slackapi/python-slackclient/blob/master/.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 searched for any related issues and avoided creating a duplicate issue.

---

### Bug Report

#### Reproducible in:

* slackclient version: 2.6.2
* python version: 3.7
* OS version(s): Linux, Mac OSX

#### Steps to reproduce:

1. Make a call to a Slack python endpoint
2. Occasionally receive a JSONDecodeError 

#### Expected result:

Expect a `SlackApiError` instead of `JSONDecodeError`

#### Actual result:

```
File "/venv/lib/python3.7/site-packages/slack/web/client.py" line 1710 in team_info
File "/venv/lib/python3.7/site-packages/slack/web/base_client.py" line 200 in api_call
File "/venv/lib/python3.7/site-packages/slack/web/base_client.py" line 333 in _sync_send
File "/venv/lib/python3.7/site-packages/slack/web/base_client.py" line 437 in _urllib_api_call
    `response_body_data: dict = json.loads(response["body"])`
File "/usr/local/lib/python3.7/json/\_\_init\_\_.py" line 348 in loads
File "/usr/local/lib/python3.7/json/decoder.py" line 337 in decode
File "/usr/local/lib/python3.7/json/decoder.py" line 355 in raw_decode
    `raise JSONDecodeError("Expecting value", s, err.value) from None`

JSONDecodeError: Expecting value: line 1 column 1 (char 0)
```

Response from server:
```
"<!DOCTYPE HTML PUBLIC \"-//IETF//DTD HTML 2.0//EN\">\n<html><head>\n<title>404 Not Found</title>\n</head><body>\n<h1>Not Found</h1>\n<p>The requested URL /api/team.info was not found on this server.</p>\n</body></html>\n"
```


Pull request description:
Fix #718 by handling unexpected response body format
###  Summary

This pull request fixes #718 by adding the logic to handle non-JSON response body data in `WebClient` (both sync and async). Although the situation almost never happens, raising only the errors defined in this SDK is more developer-friendly. Refer to the issue for details and use cases.

### Requirements (place an `x` in each `[ ]`)

* [x] I've read and understood the [Contributing Guidelines](https://github.com/slackapi/python-slackclient/blob/master/.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).

Patch:
diff --git a/slack/web/base_client.py b/slack/web/base_client.py
index 58aac1b..00f3dcf 100644
--- a/slack/web/base_client.py
+++ b/slack/web/base_client.py
@@ -303,6 +303,10 @@ class BaseClient:
                     self._logger.debug(
                         f"No response data returned from the following API call: {api_url}."
                     )
+                except json.decoder.JSONDecodeError as e:
+                    message = f"Failed to parse the response body: {str(e)}"
+                    raise err.SlackApiError(message, res)
+
                 response = {
                     "data": data,
                     "headers": res.headers,
@@ -456,7 +460,11 @@ class BaseClient:
 
             response = self._perform_urllib_http_request(url=url, args=request_args)
             if response.get("body", None):
-                response_body_data: dict = json.loads(response["body"])
+                try:
+                    response_body_data: dict = json.loads(response["body"])
+                except json.decoder.JSONDecodeError as e:
+                    message = f"Failed to parse the response body: {str(e)}"
+                    raise err.SlackApiError(message, response)
             else:
                 response_body_data: dict = None
 


Test patch:
diff --git a/tests/web/mock_web_api_server.py b/tests/web/mock_web_api_server.py
index c70835d..6eef263 100644
--- a/tests/web/mock_web_api_server.py
+++ b/tests/web/mock_web_api_server.py
@@ -18,6 +18,8 @@ class MockHandler(SimpleHTTPRequestHandler):
     pattern_for_language = re.compile("python/(\\S+)", re.IGNORECASE)
     pattern_for_package_identifier = re.compile("slackclient/(\\S+)")
 
+    html_response_body = '<!DOCTYPE HTML PUBLIC \"-//IETF//DTD HTML 2.0//EN\">\n<html><head>\n<title>404 Not Found</title>\n</head><body>\n<h1>Not Found</h1>\n<p>The requested URL /api/team.info was not found on this server.</p>\n</body></html>\n'
+
     def is_valid_user_agent(self):
         user_agent = self.headers["User-Agent"]
         return self.pattern_for_language.search(user_agent) \
@@ -98,6 +100,13 @@ class MockHandler(SimpleHTTPRequestHandler):
                     self.wfile.close()
                     return
 
+                if pattern == "html_response":
+                    self.send_response(404)
+                    self.set_common_headers()
+                    self.wfile.write(self.html_response_body.encode("utf-8"))
+                    self.wfile.close()
+                    return
+
                 if request_body and "cursor" in request_body:
                     page = request_body["cursor"]
                     pattern = f"{pattern}_{page}"
diff --git a/tests/web/test_web_client.py b/tests/web/test_web_client.py
index b2d9b15..1b422d8 100644
--- a/tests/web/test_web_client.py
+++ b/tests/web/test_web_client.py
@@ -243,4 +243,23 @@ class TestWebClient(unittest.TestCase):
         loop.set_exception_handler(exception_handler)
         loop.run_until_complete(issue_645())
         gc.collect()  # force Python to gc unclosed client session
-        self.assertFalse(session_unclosed, "Unclosed client session")
\ No newline at end of file
+        self.assertFalse(session_unclosed, "Unclosed client session")
+
+    def test_html_response_body_issue_718(self):
+        client = WebClient(base_url="http://localhost:8888")
+        try:
+            client.users_list(token="xoxb-html_response")
+            self.fail("SlackApiError expected here")
+        except err.SlackApiError as e:
+            self.assertTrue(
+                str(e).startswith("Failed to parse the response body: Expecting value: line 1 column 1 (char 0)"), e)
+
+    @async_test
+    async def test_html_response_body_issue_718_async(self):
+        client = WebClient(base_url="http://localhost:8888", run_async=True)
+        try:
+            await client.users_list(token="xoxb-html_response")
+            self.fail("SlackApiError expected here")
+        except err.SlackApiError as e:
+            self.assertTrue(
+                str(e).startswith("Failed to parse the response body: Expecting value: line 1 column 1 (char 0)"), e)