diff --git a/docs/advanced/error_handling.rst b/docs/advanced/error_handling.rst index 2fd1e39b..4e6618c9 100644 --- a/docs/advanced/error_handling.rst +++ b/docs/advanced/error_handling.rst @@ -41,6 +41,11 @@ Here are the possible Transport Errors: The message of the exception contains the first error returned by the backend. All the errors messages are available in the exception :code:`errors` attribute. + If the error message begins with :code:`Error while fetching schema:`, it means + that gql was not able to get the schema from the backend. + If you don't need the schema, you can try to create the client with + :code:`fetch_schema_from_transport=False` + - :class:`TransportClosed `: This exception is generated when the client is trying to use the transport while the transport was previously closed. diff --git a/gql/client.py b/gql/client.py index c0972133..fdac4a36 100644 --- a/gql/client.py +++ b/gql/client.py @@ -1,12 +1,13 @@ import asyncio import sys import warnings -from typing import Any, AsyncGenerator, Dict, Generator, Optional, Union, overload +from typing import Any, AsyncGenerator, Dict, Generator, Optional, Union, cast, overload from graphql import ( DocumentNode, ExecutionResult, GraphQLSchema, + IntrospectionQuery, build_ast_schema, get_introspection_query, parse, @@ -55,7 +56,7 @@ class Client: def __init__( self, schema: Optional[Union[str, GraphQLSchema]] = None, - introspection=None, + introspection: Optional[IntrospectionQuery] = None, transport: Optional[Union[Transport, AsyncTransport]] = None, fetch_schema_from_transport: bool = False, execute_timeout: Optional[Union[int, float]] = 10, @@ -106,7 +107,7 @@ def __init__( self.schema: Optional[GraphQLSchema] = schema # Answer of the introspection query - self.introspection = introspection + self.introspection: Optional[IntrospectionQuery] = introspection # GraphQL transport chosen self.transport: Optional[Union[Transport, AsyncTransport]] = transport @@ -131,6 +132,22 @@ def validate(self, document: DocumentNode): if validation_errors: raise validation_errors[0] + def _build_schema_from_introspection(self, execution_result: ExecutionResult): + if execution_result.errors: + raise TransportQueryError( + ( + f"Error while fetching schema: {execution_result.errors[0]!s}\n" + "If you don't need the schema, you can try with: " + '"fetch_schema_from_transport=False"' + ), + errors=execution_result.errors, + data=execution_result.data, + extensions=execution_result.extensions, + ) + + self.introspection = cast(IntrospectionQuery, execution_result.data) + self.schema = build_client_schema(self.introspection) + @overload def execute_sync( self, @@ -802,8 +819,8 @@ def fetch_schema(self) -> None: Don't use this function and instead set the fetch_schema_from_transport attribute to True""" execution_result = self.transport.execute(parse(get_introspection_query())) - self.client.introspection = execution_result.data - self.client.schema = build_client_schema(self.client.introspection) + + self.client._build_schema_from_introspection(execution_result) @property def transport(self): @@ -1175,8 +1192,8 @@ async def fetch_schema(self) -> None: execution_result = await self.transport.execute( parse(get_introspection_query()) ) - self.client.introspection = execution_result.data - self.client.schema = build_client_schema(self.client.introspection) + + self.client._build_schema_from_introspection(execution_result) @property def transport(self): diff --git a/tests/test_aiohttp.py b/tests/test_aiohttp.py index 2535ddb3..a5a3127d 100644 --- a/tests/test_aiohttp.py +++ b/tests/test_aiohttp.py @@ -1190,3 +1190,46 @@ async def handler(request): africa = continents[0] assert africa["code"] == "AF" + + +@pytest.mark.asyncio +async def test_aiohttp_error_fetching_schema(event_loop, aiohttp_server): + from aiohttp import web + from gql.transport.aiohttp import AIOHTTPTransport + + error_answer = """ +{ + "errors": [ + { + "errorType": "UnauthorizedException", + "message": "Permission denied" + } + ] +} +""" + + async def handler(request): + return web.Response( + text=error_answer, + content_type="application/json", + ) + + app = web.Application() + app.router.add_route("POST", "/", handler) + server = await aiohttp_server(app) + + url = server.make_url("/") + + transport = AIOHTTPTransport(url=url, timeout=10) + + with pytest.raises(TransportQueryError) as exc_info: + async with Client(transport=transport, fetch_schema_from_transport=True): + pass + + expected_error = ( + "Error while fetching schema: " + "{'errorType': 'UnauthorizedException', 'message': 'Permission denied'}" + ) + + assert expected_error in str(exc_info.value) + assert transport.session is None diff --git a/tests/test_requests.py b/tests/test_requests.py index 7cd7f712..70fc337e 100644 --- a/tests/test_requests.py +++ b/tests/test_requests.py @@ -755,3 +755,52 @@ def test_code(): f2.close() await run_sync_test(event_loop, server, test_code) + + +@pytest.mark.aiohttp +@pytest.mark.asyncio +async def test_requests_error_fetching_schema( + event_loop, aiohttp_server, run_sync_test +): + from aiohttp import web + from gql.transport.requests import RequestsHTTPTransport + + error_answer = """ +{ + "errors": [ + { + "errorType": "UnauthorizedException", + "message": "Permission denied" + } + ] +} +""" + + async def handler(request): + return web.Response( + text=error_answer, + content_type="application/json", + ) + + app = web.Application() + app.router.add_route("POST", "/", handler) + server = await aiohttp_server(app) + + url = server.make_url("/") + + def test_code(): + transport = RequestsHTTPTransport(url=url) + + with pytest.raises(TransportQueryError) as exc_info: + with Client(transport=transport, fetch_schema_from_transport=True): + pass + + expected_error = ( + "Error while fetching schema: " + "{'errorType': 'UnauthorizedException', 'message': 'Permission denied'}" + ) + + assert expected_error in str(exc_info.value) + assert transport.session is None + + await run_sync_test(event_loop, server, test_code)