Repository: graphql-python/gql

Issue:
Client crashes on 429 HTTP response code
**Describe the bug**
This bug occurred as I was fetching data from the Santiment API (https://api.santiment.net/graphiql). If no API key is supplied, rate limiting kicks in after a while and the client gets a 429 HTTP response code (nr. of tries remaining are in the headers).
It seems like the gql client does not handle this response properly, as it tries to output an execution error that does not exist:

```python
File "/home/developer/.local/share/virtualenvs/app-4PlAip0Q/lib/python3.8/site-packages/gql/client.py", line 702, in __enter__
    return self.connect_sync()
  File "/home/developer/.local/share/virtualenvs/app-4PlAip0Q/lib/python3.8/site-packages/gql/client.py", line 686, in connect_sync
    self.session.fetch_schema()
  File "/home/developer/.local/share/virtualenvs/app-4PlAip0Q/lib/python3.8/site-packages/gql/client.py", line 883, in fetch_schema
    self.client._build_schema_from_introspection(execution_result)
  File "/home/developer/.local/share/virtualenvs/app-4PlAip0Q/lib/python3.8/site-packages/gql/client.py", line 155, in _build_schema_from_introspection
    f"Error while fetching schema: {execution_result.errors[0]!s}\n"
KeyError: 0
```
I'm sorry I can't provide more information, like debug logs. One of the IPs in my cluster got rate limited by Santiment and I could only reproduce the issue once.

**To Reproduce**

1. Set up a client that talks to the Santiment API without a key
2. Run many queries until your IP gets blocked
3. Read out the error message

Alternatively, you can set up a custom server that always returns a 429 code, though I don't know if the response structure has to be exactly like the one from Santiment.

**Expected behavior**
An error is still thrown, but the client logs the HTTP response code.

**System info (please complete the following information):**
 - OS: Debian 11
 - Python version: 3.9
 - gql version: 3.4.0
 - graphql-core version: 3.2.1

_I'm just opening this issue for documentation sake. If I have time I might even make a PR, though I can't promise anything._


Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/graphql-python/gql/pull/359?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=graphql-python) Report
> Merging [#359](https://codecov.io/gh/graphql-python/gql/pull/359?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=graphql-python) (4e16cfb) into [master](https://codecov.io/gh/graphql-python/gql/commit/5a9c0cca271b23915a621339a28004dc457a4bdc?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=graphql-python) (5a9c0cc) will **not change** coverage.
> The diff coverage is `100.00%`.

```diff
@@            Coverage Diff            @@
##            master      #359   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           26        26           
  Lines         2243      2250    +7     
=========================================
+ Hits          2243      2250    +7     
```


| [Impacted Files](https://codecov.io/gh/graphql-python/gql/pull/359?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=graphql-python) | Coverage Δ | |
|---|---|---|
| [gql/client.py](https://codecov.io/gh/graphql-python/gql/pull/359/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=graphql-python#diff-Z3FsL2NsaWVudC5weQ==) | `100.00% <100.00%> (ø)` | |
| [gql/utils.py](https://codecov.io/gh/graphql-python/gql/pull/359/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=graphql-python#diff-Z3FsL3V0aWxzLnB5) | `100.00% <100.00%> (ø)` | |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=graphql-python). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=graphql-python)


Patch:
diff --git a/gql/client.py b/gql/client.py
index 6a6f5a6..69804fa 100644
--- a/gql/client.py
+++ b/gql/client.py
@@ -34,6 +34,7 @@ from .transport.transport import Transport
 from .utilities import build_client_schema
 from .utilities import parse_result as parse_result_fn
 from .utilities import serialize_variable_values
+from .utils import str_first_element
 
 """
 Load the appropriate instance of the Literal type
@@ -152,7 +153,8 @@ class Client:
         if execution_result.errors:
             raise TransportQueryError(
                 (
-                    f"Error while fetching schema: {execution_result.errors[0]!s}\n"
+                    "Error while fetching schema: "
+                    f"{str_first_element(execution_result.errors)}\n"
                     "If you don't need the schema, you can try with: "
                     '"fetch_schema_from_transport=False"'
                 ),
@@ -858,7 +860,7 @@ class SyncClientSession:
         # Raise an error if an error is returned in the ExecutionResult object
         if result.errors:
             raise TransportQueryError(
-                str(result.errors[0]),
+                str_first_element(result.errors),
                 errors=result.errors,
                 data=result.data,
                 extensions=result.extensions,
@@ -1066,7 +1068,7 @@ class AsyncClientSession:
                 # Raise an error if an error is returned in the ExecutionResult object
                 if result.errors:
                     raise TransportQueryError(
-                        str(result.errors[0]),
+                        str_first_element(result.errors),
                         errors=result.errors,
                         data=result.data,
                         extensions=result.extensions,
@@ -1229,7 +1231,7 @@ class AsyncClientSession:
         # Raise an error if an error is returned in the ExecutionResult object
         if result.errors:
             raise TransportQueryError(
-                str(result.errors[0]),
+                str_first_element(result.errors),
                 errors=result.errors,
                 data=result.data,
                 extensions=result.extensions,
diff --git a/gql/utils.py b/gql/utils.py
index 3edb086..b4265ce 100644
--- a/gql/utils.py
+++ b/gql/utils.py
@@ -1,6 +1,6 @@
 """Utilities to manipulate several python objects."""
 
-from typing import Any, Dict, Tuple, Type
+from typing import Any, Dict, List, Tuple, Type
 
 
 # From this response in Stackoverflow
@@ -47,3 +47,12 @@ def extract_files(
     nulled_variables = recurse_extract("variables", variables)
 
     return nulled_variables, files
+
+
+def str_first_element(errors: List) -> str:
+    try:
+        first_error = errors[0]
+    except (KeyError, TypeError):
+        first_error = errors
+
+    return str(first_error)


Test patch:
diff --git a/tests/test_aiohttp.py b/tests/test_aiohttp.py
index 3a84d21..f1a3cdf 100644
--- a/tests/test_aiohttp.py
+++ b/tests/test_aiohttp.py
@@ -199,18 +199,21 @@ async def test_aiohttp_error_code_500(event_loop, aiohttp_server):
         assert "500, message='Internal Server Error'" in str(exc_info.value)
 
 
-query1_server_error_answer = '{"errors": ["Error 1", "Error 2"]}'
+transport_query_error_responses = [
+    '{"errors": ["Error 1", "Error 2"]}',
+    '{"errors": {"error_1": "Something"}}',
+    '{"errors": 5}',
+]
 
 
 @pytest.mark.asyncio
-async def test_aiohttp_error_code(event_loop, aiohttp_server):
+@pytest.mark.parametrize("query_error", transport_query_error_responses)
+async def test_aiohttp_error_code(event_loop, aiohttp_server, query_error):
     from aiohttp import web
     from gql.transport.aiohttp import AIOHTTPTransport
 
     async def handler(request):
-        return web.Response(
-            text=query1_server_error_answer, content_type="application/json"
-        )
+        return web.Response(text=query_error, content_type="application/json")
 
     app = web.Application()
     app.router.add_route("POST", "/", handler)