Repository: dbt-labs/dbt-core

Issue:
Non-JSON response from Snowflake connection attempt results is not handled
It is possible via the Snowflake OAuth/SSO pathway that our attempt to obtain an access_token will fail for a variety of reasons.  The resultant response may or may not be in JSON but this code assumes it always will be:

https://github.com/fishtown-analytics/dbt/blob/d8c261ffcfab0da7974310760a74d7858756c3ed/plugins/snowflake/dbt/adapters/snowflake/connections.py#L128-L132

and errors out with `Expecting value: line 1 column 1 (char 0)` when receiving a non-JSON response.


Discussion:
Thanks for opening @barberscott, and for pointing to the relevant spot in the code!

I bet this is an easy thing to check and handle, the question will be doing it as quickly and reliably as possible. This is hardly my area of expertise, but from reading a bit online (i.e. [this SO post](https://stackoverflow.com/questions/44012811/python-requests-quickly-know-if-response-is-json-parsable)), it sounds like we _might_ be able to check the `requests` response metadata to see if the content type is JSON:
```python
result = requests.post(token_url, headers=headers, data=data) 
if result.headers.get('content-type') == 'application/json':
  result_json = result.json()
```
If the response isn't JSON, we should call `_get_access_token` again, until we have a response that is.

Any chance this is a fix you'd be interested in contributing? :)

Resolving patch:
diff --git a/plugins/snowflake/dbt/adapters/snowflake/connections.py b/plugins/snowflake/dbt/adapters/snowflake/connections.py
--- a/plugins/snowflake/dbt/adapters/snowflake/connections.py
+++ b/plugins/snowflake/dbt/adapters/snowflake/connections.py
@@ -5,6 +5,7 @@
 from contextlib import contextmanager
 from dataclasses import dataclass
 from io import StringIO
+from time import sleep
 from typing import Optional
 
 from cryptography.hazmat.backends import default_backend
@@ -106,6 +107,7 @@ def _get_access_token(self) -> str:
                 'need a client ID a client secret, and a refresh token to get '
                 'an access token'
             )
+
         # should the full url be a config item?
         token_url = _TOKEN_REQUEST_URL.format(self.account)
         # I think this is only used to redirect on success, which we ignore
@@ -125,10 +127,25 @@ def _get_access_token(self) -> str:
             'Authorization': f'Basic {auth}',
             'Content-type': 'application/x-www-form-urlencoded;charset=utf-8'
         }
-        result = requests.post(token_url, headers=headers, data=data)
-        result_json = result.json()
-        if 'access_token' not in result_json:
-            raise DatabaseException(f'Did not get a token: {result_json}')
+
+        result_json = None
+        max_iter = 20
+        # Attempt to obtain JSON for 1 second before throwing an error
+        for i in range(max_iter):
+            result = requests.post(token_url, headers=headers, data=data)
+            try:
+                result_json = result.json()
+                break
+            except ValueError as e:
+                message = result.text
+                logger.debug(f"Got a non-json response ({result.status_code}): \
+                              {e}, message: {message}")
+                sleep(0.05)
+
+        if result_json is None:
+            raise DatabaseException(f"""Did not receive valid json with access_token.
+                                        Showing json response: {result_json}""")
+
         return result_json['access_token']
 
     def _get_private_key(self):


Test patch: