Skip to content

Commit

Permalink
Return all errors in TransportQueryError exception (graphql-python#96)
Browse files Browse the repository at this point in the history
  • Loading branch information
leszekhanusz committed Jul 5, 2020
1 parent 6220bff commit 385fb5b
Show file tree
Hide file tree
Showing 4 changed files with 89 additions and 22 deletions.
62 changes: 45 additions & 17 deletions gql/client.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import asyncio
from inspect import isawaitable
from typing import Any, AsyncGenerator, Dict, Generator, Optional, Union, cast
from typing import Any, AsyncGenerator, Dict, Generator, Optional, Union

from graphql import (
DocumentNode,
Expand Down Expand Up @@ -213,19 +212,22 @@ class SyncClientSession:
def __init__(self, client: Client):
self.client = client

def execute(self, document: DocumentNode, *args, **kwargs) -> Dict:
def _execute(self, document: DocumentNode, *args, **kwargs) -> ExecutionResult:

# Validate document
if self.client.schema:
self.client.validate(document)

result = self.transport.execute(document, *args, **kwargs)
return self.transport.execute(document, *args, **kwargs)

def execute(self, document: DocumentNode, *args, **kwargs) -> Dict:

assert not isawaitable(result), "Transport returned an awaitable result."
result = cast(ExecutionResult, result)
# Validate and execute on the transport
result = self._execute(document, *args, **kwargs)

# Raise an error if an error is returned in the ExecutionResult object
if result.errors:
raise TransportQueryError(str(result.errors[0]))
raise TransportQueryError(str(result.errors[0]), errors=result.errors)

assert (
result.data is not None
Expand Down Expand Up @@ -267,43 +269,69 @@ async def fetch_and_validate(self, document: DocumentNode):
if self.client.schema:
self.client.validate(document)

async def subscribe(
async def _subscribe(
self, document: DocumentNode, *args, **kwargs
) -> AsyncGenerator[Dict, None]:
) -> AsyncGenerator[ExecutionResult, None]:

# Fetch schema from transport if needed and validate document if possible
await self.fetch_and_validate(document)

# Subscribe to the transport and yield data or raise error
self._generator: AsyncGenerator[
# Subscribe to the transport
inner_generator: AsyncGenerator[
ExecutionResult, None
] = self.transport.subscribe(document, *args, **kwargs)

async for result in self._generator:
# Keep a reference to the inner generator to allow the user to call aclose()
# before a break if python version is too old (pypy3 py 3.6.1)
self._generator = inner_generator

async for result in inner_generator:
if result.errors:
# Note: we need to run generator.aclose() here or the finally block in
# transport.subscribe will not be reached in pypy3 (py 3.6.1)
await self._generator.aclose()
await inner_generator.aclose()

yield result

async def subscribe(
self, document: DocumentNode, *args, **kwargs
) -> AsyncGenerator[Dict, None]:

raise TransportQueryError(str(result.errors[0]))
# Validate and subscribe on the transport
async for result in self._subscribe(document, *args, **kwargs):

# Raise an error if an error is returned in the ExecutionResult object
if result.errors:
raise TransportQueryError(str(result.errors[0]), errors=result.errors)

elif result.data is not None:
yield result.data

async def execute(self, document: DocumentNode, *args, **kwargs) -> Dict:
async def _execute(
self, document: DocumentNode, *args, **kwargs
) -> ExecutionResult:

# Fetch schema from transport if needed and validate document if possible
await self.fetch_and_validate(document)

# Execute the query with the transport with a timeout
result = await asyncio.wait_for(
return await asyncio.wait_for(
self.transport.execute(document, *args, **kwargs),
self.client.execute_timeout,
)

async def execute(self, document: DocumentNode, *args, **kwargs) -> Dict:

# Validate and execute on the transport
result = await self._execute(document, *args, **kwargs)

# Raise an error if an error is returned in the ExecutionResult object
if result.errors:
raise TransportQueryError(str(result.errors[0]))
raise TransportQueryError(str(result.errors[0]), errors=result.errors)

assert (
result.data is not None
), "Transport returned an ExecutionResult without data or errors"

return result.data

Expand Down
11 changes: 10 additions & 1 deletion gql/transport/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
from typing import Any, List, Optional


class TransportError(Exception):
pass

Expand All @@ -22,9 +25,15 @@ class TransportQueryError(Exception):
This exception should not close the transport connection.
"""

def __init__(self, msg, query_id=None):
def __init__(
self,
msg: str,
query_id: Optional[int] = None,
errors: Optional[List[Any]] = None,
):
super().__init__(msg)
self.query_id = query_id
self.errors = errors


class TransportClosed(TransportError):
Expand Down
7 changes: 6 additions & 1 deletion gql/transport/websockets.py
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,9 @@ def _parse_answer(

elif answer_type == "error":

raise TransportQueryError(str(payload), query_id=answer_id)
raise TransportQueryError(
str(payload), query_id=answer_id, errors=[payload]
)

elif answer_type == "ka":
# KeepAlive message
Expand Down Expand Up @@ -333,6 +335,9 @@ async def _receive_data_loop(self) -> None:
# ==> Add an exception to this query queue
# The exception is raised for this specific query,
# but the transport is not closed.
assert isinstance(
e.query_id, int
), "TransportQueryError should have a query_id defined here"
try:
await self.listeners[e.query_id].set_exception(e)
except KeyError:
Expand Down
31 changes: 28 additions & 3 deletions tests/test_websocket_exceptions.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import asyncio
import json
import types
from typing import List

import pytest
import websockets
Expand Down Expand Up @@ -44,9 +45,17 @@ async def test_websocket_invalid_query(event_loop, client_and_server, query_str)

query = gql(query_str)

with pytest.raises(TransportQueryError):
with pytest.raises(TransportQueryError) as exc_info:
await session.execute(query)

exception = exc_info.value

assert isinstance(exception.errors, List)

error = exception.errors[0]

assert error["extensions"]["code"] == "INTERNAL_SERVER_ERROR"


invalid_subscription_str = """
subscription getContinents {
Expand Down Expand Up @@ -75,10 +84,18 @@ async def test_websocket_invalid_subscription(event_loop, client_and_server, que

query = gql(query_str)

with pytest.raises(TransportQueryError):
with pytest.raises(TransportQueryError) as exc_info:
async for result in session.subscribe(query):
pass

exception = exc_info.value

assert isinstance(exception.errors, List)

error = exception.errors[0]

assert error["extensions"]["code"] == "INTERNAL_SERVER_ERROR"


connection_error_server_answer = (
'{"type":"connection_error","id":null,'
Expand Down Expand Up @@ -170,9 +187,17 @@ async def monkey_patch_send_query(

query = gql(query_str)

with pytest.raises(TransportQueryError):
with pytest.raises(TransportQueryError) as exc_info:
await session.execute(query)

exception = exc_info.value

assert isinstance(exception.errors, List)

error = exception.errors[0]

assert error["message"] == "Must provide document"


not_json_answer = ["BLAHBLAH"]
missing_type_answer = ["{}"]
Expand Down

0 comments on commit 385fb5b

Please sign in to comment.