diff --git a/docs/advanced/error_handling.rst b/docs/advanced/error_handling.rst index 4e6618c9..458f2667 100644 --- a/docs/advanced/error_handling.rst +++ b/docs/advanced/error_handling.rst @@ -46,6 +46,11 @@ Here are the possible Transport Errors: If you don't need the schema, you can try to create the client with :code:`fetch_schema_from_transport=False` +- :class:`TransportConnectionFailed `: + This exception is generated when an unexpected Exception is received from the + transport dependency when trying to connect or to send the request. + For example in case of an SSL error, or if a websocket connection suddenly fails. + - :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/gql.py b/gql/gql.py index f4cd3aea..8a5a1b32 100644 --- a/gql/gql.py +++ b/gql/gql.py @@ -3,15 +3,14 @@ def gql(request_string: str) -> GraphQLRequest: """Given a string containing a GraphQL request, - parse it into a Document and put it into a GraphQLRequest object + parse it into a Document and put it into a GraphQLRequest object. :param request_string: the GraphQL request as a String :return: a :class:`GraphQLRequest ` which can be later executed or subscribed by a - :class:`Client `, by an - :class:`async session ` or by a - :class:`sync session ` - + :class:`Client `, by an + :class:`async session ` or by a + :class:`sync session ` :raises graphql.error.GraphQLError: if a syntax error is encountered. """ return GraphQLRequest(request_string) diff --git a/gql/graphql_request.py b/gql/graphql_request.py index fe3523a9..5e6f3ee4 100644 --- a/gql/graphql_request.py +++ b/gql/graphql_request.py @@ -14,22 +14,19 @@ def __init__( variable_values: Optional[Dict[str, Any]] = None, operation_name: Optional[str] = None, ): - """ - Initialize a GraphQL request. + """Initialize a GraphQL request. :param request: GraphQL request as DocumentNode object or as a string. If string, it will be converted to DocumentNode. :param variable_values: Dictionary of input parameters (Default: None). :param operation_name: Name of the operation that shall be executed. Only required in multi-operation documents (Default: None). - :return: a :class:`GraphQLRequest ` which can be later executed or subscribed by a - :class:`Client `, by an - :class:`async session ` or by a - :class:`sync session ` + :class:`Client `, by an + :class:`async session ` or by a + :class:`sync session ` :raises graphql.error.GraphQLError: if a syntax error is encountered. - """ if isinstance(request, str): source = Source(request, "GraphQL request") diff --git a/gql/transport/aiohttp.py b/gql/transport/aiohttp.py index 40e212cf..e3bfdb3b 100644 --- a/gql/transport/aiohttp.py +++ b/gql/transport/aiohttp.py @@ -31,6 +31,8 @@ from .exceptions import ( TransportAlreadyConnected, TransportClosed, + TransportConnectionFailed, + TransportError, TransportProtocolError, TransportServerError, ) @@ -377,6 +379,10 @@ async def execute( try: async with self.session.post(self.url, ssl=self.ssl, **post_args) as resp: return await self._prepare_result(resp) + except TransportError: + raise + except Exception as e: + raise TransportConnectionFailed(str(e)) from e finally: if upload_files: close_files(list(self.files.values())) @@ -407,8 +413,13 @@ async def execute_batch( extra_args, ) - async with self.session.post(self.url, ssl=self.ssl, **post_args) as resp: - return await self._prepare_batch_result(reqs, resp) + try: + async with self.session.post(self.url, ssl=self.ssl, **post_args) as resp: + return await self._prepare_batch_result(reqs, resp) + except TransportError: + raise + except Exception as e: + raise TransportConnectionFailed(str(e)) from e def subscribe( self, diff --git a/gql/transport/exceptions.py b/gql/transport/exceptions.py index 3e63f0bc..0049d5c2 100644 --- a/gql/transport/exceptions.py +++ b/gql/transport/exceptions.py @@ -62,9 +62,10 @@ class TransportClosed(TransportError): class TransportConnectionFailed(TransportError): - """Transport adapter connection closed. + """Transport connection failed. - This exception is by the connection adapter code when a connection closed. + This exception is by the connection adapter code when a connection closed + or if an unexpected Exception was received when trying to send a request. """ diff --git a/gql/transport/httpx.py b/gql/transport/httpx.py index 7fe2a7db..0a338639 100644 --- a/gql/transport/httpx.py +++ b/gql/transport/httpx.py @@ -23,6 +23,7 @@ from .exceptions import ( TransportAlreadyConnected, TransportClosed, + TransportConnectionFailed, TransportProtocolError, TransportServerError, ) @@ -262,6 +263,8 @@ def execute( try: response = self.client.post(self.url, **post_args) + except Exception as e: + raise TransportConnectionFailed(str(e)) from e finally: if upload_files: close_files(list(self.files.values())) @@ -294,7 +297,10 @@ def execute_batch( extra_args=extra_args, ) - response = self.client.post(self.url, **post_args) + try: + response = self.client.post(self.url, **post_args) + except Exception as e: + raise TransportConnectionFailed(str(e)) from e return self._prepare_batch_result(reqs, response) @@ -354,6 +360,8 @@ async def execute( try: response = await self.client.post(self.url, **post_args) + except Exception as e: + raise TransportConnectionFailed(str(e)) from e finally: if upload_files: close_files(list(self.files.values())) @@ -386,7 +394,10 @@ async def execute_batch( extra_args=extra_args, ) - response = await self.client.post(self.url, **post_args) + try: + response = await self.client.post(self.url, **post_args) + except Exception as e: + raise TransportConnectionFailed(str(e)) from e return self._prepare_batch_result(reqs, response) diff --git a/gql/transport/requests.py b/gql/transport/requests.py index 17bf4695..a29f7f0f 100644 --- a/gql/transport/requests.py +++ b/gql/transport/requests.py @@ -29,6 +29,7 @@ from .exceptions import ( TransportAlreadyConnected, TransportClosed, + TransportConnectionFailed, TransportProtocolError, TransportServerError, ) @@ -289,6 +290,8 @@ def execute( # Using the created session to perform requests try: response = self.session.request(self.method, self.url, **post_args) + except Exception as e: + raise TransportConnectionFailed(str(e)) from e finally: if upload_files: close_files(list(self.files.values())) @@ -349,11 +352,14 @@ def execute_batch( extra_args=extra_args, ) - response = self.session.request( - self.method, - self.url, - **post_args, - ) + try: + response = self.session.request( + self.method, + self.url, + **post_args, + ) + except Exception as e: + raise TransportConnectionFailed(str(e)) from e return self._prepare_batch_result(reqs, response) diff --git a/tests/test_aiohttp.py b/tests/test_aiohttp.py index e3ac08c4..506b04f4 100644 --- a/tests/test_aiohttp.py +++ b/tests/test_aiohttp.py @@ -11,6 +11,7 @@ from gql.transport.exceptions import ( TransportAlreadyConnected, TransportClosed, + TransportConnectionFailed, TransportProtocolError, TransportQueryError, TransportServerError, @@ -1455,7 +1456,6 @@ async def handler(request): async def test_aiohttp_query_https_self_cert_fail(ssl_aiohttp_server): """By default, we should verify the ssl certificate""" from aiohttp import web - from aiohttp.client_exceptions import ClientConnectorCertificateError from gql.transport.aiohttp import AIOHTTPTransport @@ -1472,16 +1472,22 @@ async def handler(request): transport = AIOHTTPTransport(url=url, timeout=10) - with pytest.raises(ClientConnectorCertificateError) as exc_info: - async with Client(transport=transport) as session: - query = gql(query1_str) + query = gql(query1_str) - # Execute query asynchronously + expected_error = "certificate verify failed: self-signed certificate" + + with pytest.raises(TransportConnectionFailed) as exc_info: + async with Client(transport=transport) as session: await session.execute(query) - expected_error = "certificate verify failed: self-signed certificate" + assert expected_error in str(exc_info.value) + + with pytest.raises(TransportConnectionFailed) as exc_info: + async with Client(transport=transport) as session: + await session.execute_batch([query]) assert expected_error in str(exc_info.value) + assert transport.session is None diff --git a/tests/test_httpx.py b/tests/test_httpx.py index 3a424355..0411294b 100644 --- a/tests/test_httpx.py +++ b/tests/test_httpx.py @@ -7,6 +7,7 @@ from gql.transport.exceptions import ( TransportAlreadyConnected, TransportClosed, + TransportConnectionFailed, TransportProtocolError, TransportQueryError, TransportServerError, @@ -150,7 +151,6 @@ async def test_httpx_query_https_self_cert_fail( ): """By default, we should verify the ssl certificate""" from aiohttp import web - from httpx import ConnectError from gql.transport.httpx import HTTPXTransport @@ -180,15 +180,19 @@ def test_code(): **extra_args, ) - with pytest.raises(ConnectError) as exc_info: - with Client(transport=transport) as session: + query = gql(query1_str) - query = gql(query1_str) + expected_error = "certificate verify failed: self-signed certificate" - # Execute query synchronously + with pytest.raises(TransportConnectionFailed) as exc_info: + with Client(transport=transport) as session: session.execute(query) - expected_error = "certificate verify failed: self-signed certificate" + assert expected_error in str(exc_info.value) + + with pytest.raises(TransportConnectionFailed) as exc_info: + with Client(transport=transport) as session: + session.execute_batch([query]) assert expected_error in str(exc_info.value) diff --git a/tests/test_httpx_async.py b/tests/test_httpx_async.py index 25fd27aa..690b3ee7 100644 --- a/tests/test_httpx_async.py +++ b/tests/test_httpx_async.py @@ -9,6 +9,7 @@ from gql.transport.exceptions import ( TransportAlreadyConnected, TransportClosed, + TransportConnectionFailed, TransportProtocolError, TransportQueryError, TransportServerError, @@ -1155,7 +1156,6 @@ async def handler(request): @pytest.mark.parametrize("verify_https", ["explicitely_enabled", "default"]) async def test_httpx_query_https_self_cert_fail(ssl_aiohttp_server, verify_https): from aiohttp import web - from httpx import ConnectError from gql.transport.httpx import HTTPXAsyncTransport @@ -1177,15 +1177,19 @@ async def handler(request): transport = HTTPXAsyncTransport(url=url, timeout=10, **extra_args) - with pytest.raises(ConnectError) as exc_info: - async with Client(transport=transport) as session: + query = gql(query1_str) - query = gql(query1_str) + expected_error = "certificate verify failed: self-signed certificate" - # Execute query asynchronously + with pytest.raises(TransportConnectionFailed) as exc_info: + async with Client(transport=transport) as session: await session.execute(query) - expected_error = "certificate verify failed: self-signed certificate" + assert expected_error in str(exc_info.value) + + with pytest.raises(TransportConnectionFailed) as exc_info: + async with Client(transport=transport) as session: + await session.execute_batch([query]) assert expected_error in str(exc_info.value) diff --git a/tests/test_requests.py b/tests/test_requests.py index 45901875..fe57f5e3 100644 --- a/tests/test_requests.py +++ b/tests/test_requests.py @@ -8,6 +8,7 @@ from gql.transport.exceptions import ( TransportAlreadyConnected, TransportClosed, + TransportConnectionFailed, TransportProtocolError, TransportQueryError, TransportServerError, @@ -154,7 +155,6 @@ async def test_requests_query_https_self_cert_fail( ): """By default, we should verify the ssl certificate""" from aiohttp import web - from requests.exceptions import SSLError from gql.transport.requests import RequestsHTTPTransport @@ -182,7 +182,7 @@ def test_code(): **extra_args, ) - with pytest.raises(SSLError) as exc_info: + with pytest.raises(TransportConnectionFailed) as exc_info: with Client(transport=transport) as session: query = gql(query1_str)