Skip to content

Commit

Permalink
Separate errors to report to user from unexpected exception (issue gr…
Browse files Browse the repository at this point in the history
  • Loading branch information
ods committed Apr 6, 2020
1 parent d69324c commit 9b33d5e
Show file tree
Hide file tree
Showing 15 changed files with 119 additions and 61 deletions.
13 changes: 7 additions & 6 deletions graphql/error/tests/test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from promise import Promise

from graphql.error import GraphQLError
from graphql.execution import execute
from graphql.language.parser import parse
from graphql.type import GraphQLField, GraphQLObjectType, GraphQLSchema, GraphQLString
Expand All @@ -22,7 +23,7 @@ def test_raise():

def resolver(context, *_):
# type: (Optional[Any], *ResolveInfo) -> None
raise Exception("Failed")
raise GraphQLError("Failed")

Type = GraphQLObjectType(
"Type", {"a": GraphQLField(GraphQLString, resolver=resolver)}
Expand All @@ -38,14 +39,14 @@ def test_reraise():

def resolver(context, *_):
# type: (Optional[Any], *ResolveInfo) -> None
raise Exception("Failed")
raise GraphQLError("Failed")

Type = GraphQLObjectType(
"Type", {"a": GraphQLField(GraphQLString, resolver=resolver)}
)

result = execute(GraphQLSchema(Type), ast)
with pytest.raises(Exception) as exc_info:
with pytest.raises(GraphQLError) as exc_info:
result.errors[0].reraise()

extracted = traceback.extract_tb(exc_info.tb)
Expand All @@ -59,7 +60,7 @@ def resolver(context, *_):
"return executor.execute(resolve_fn, source, info, **args)",
),
("execute", "return fn(*args, **kwargs)"),
("resolver", 'raise Exception("Failed")'),
("resolver", 'raise GraphQLError("Failed")'),
]

assert str(exc_info.value) == "Failed"
Expand All @@ -71,7 +72,7 @@ def test_reraise_from_promise():
ast = parse("query Example { a }")

def fail():
raise Exception("Failed")
raise GraphQLError("Failed")

def resolver(context, *_):
# type: (Optional[Any], *ResolveInfo) -> None
Expand All @@ -95,7 +96,7 @@ def resolver(context, *_):
("test_reraise_from_promise", "result.errors[0].reraise()"),
("_resolve_from_executor", "executor(resolve, reject)"),
("<lambda>", "return Promise(lambda resolve, reject: resolve(fail()))"),
("fail", 'raise Exception("Failed")'),
("fail", 'raise GraphQLError("Failed")'),
]

assert str(exc_info.value) == "Failed"
18 changes: 11 additions & 7 deletions graphql/execution/executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,10 @@ def promise_executor(v):

def on_rejected(error):
# type: (Exception) -> None
exe_context.errors.append(error)
return None
if isinstance(error, GraphQLError):
exe_context.errors.append(error)
return None
return Promise.rejected(error)

def on_resolve(data):
# type: (Union[None, Dict, Observable]) -> Union[ExecutionResult, Observable]
Expand Down Expand Up @@ -271,9 +273,6 @@ def subscribe_fields(
# type: (...) -> Observable
subscriber_exe_context = SubscriberExecutionContext(exe_context)

def on_error(error):
subscriber_exe_context.report_error(error)

def map_result(data):
# type: (Dict[str, Any]) -> ExecutionResult
if subscriber_exe_context.errors:
Expand Down Expand Up @@ -482,14 +481,16 @@ def complete_value_catching_error(

def handle_error(error):
# type: (Union[GraphQLError, GraphQLLocatedError]) -> Optional[Any]
if not isinstance(error, GraphQLError):
return Promise.rejected(error)
traceback = completed._traceback # type: ignore
exe_context.report_error(error, traceback)
return None

return completed.catch(handle_error)

return completed
except Exception as e:
except GraphQLError as e:
traceback = sys.exc_info()[2]
exe_context.report_error(e, traceback)
return None
Expand Down Expand Up @@ -533,12 +534,15 @@ def complete_value(
GraphQLLocatedError( # type: ignore
field_asts, original_error=error, path=path
)
if isinstance(error, GraphQLError) else error
),
)

# print return_type, type(result)
if isinstance(result, Exception):
if isinstance(result, GraphQLError):
raise GraphQLLocatedError(field_asts, original_error=result, path=path)
if isinstance(result, Exception):
raise result

if isinstance(return_type, GraphQLNonNull):
return complete_nonnull_value(
Expand Down
44 changes: 35 additions & 9 deletions graphql/execution/tests/test_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,7 @@ def ok(self):

def error(self):
# type: () -> NoReturn
raise Exception("Error getting error")
raise GraphQLError("Error getting error")

doc_ast = parse(doc)

Expand Down Expand Up @@ -567,32 +567,58 @@ def test_fails_to_execute_a_query_containing_a_type_definition():
assert isinstance(nodes[0], ObjectTypeDefinition)


def test_exceptions_are_reraised_if_specified(mocker):
# type: (MockFixture) -> None

logger = mocker.patch("graphql.execution.executor.logger")
def test_exceptions_are_reraised():
# type: () -> None

query = parse(
"""
{ foo }
"""
)

class Error(Exception):
pass

def resolver(*_):
# type: (*Any) -> NoReturn
raise Exception("UH OH!")
raise Error("UH OH!")

schema = GraphQLSchema(
GraphQLObjectType(
name="Query", fields={"foo": GraphQLField(GraphQLString, resolver=resolver)}
)
)

execute(schema, query)
logger.exception.assert_called_with(
"An error occurred while resolving field Query.foo"
with raises(Error):
execute(schema, query)


def test_exceptions_are_reraised_promise():
# type: () -> None

query = parse(
"""
{ foo }
"""
)

class Error(Exception):
pass

@Promise.promisify
def resolver(*_):
# type: (*Any) -> NoReturn
raise Error("UH OH!")

schema = GraphQLSchema(
GraphQLObjectType(
name="Query", fields={"foo": GraphQLField(GraphQLString, resolver=resolver)}
)
)

with raises(Error):
execute(schema, query)


def test_executor_properly_propogates_path_data(mocker):
# type: (MockFixture) -> None
Expand Down
26 changes: 24 additions & 2 deletions graphql/execution/tests/test_executor_asyncio.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

asyncio = pytest.importorskip("asyncio")

from graphql.error import format_error
from graphql.error import format_error, GraphQLError
from graphql.execution import execute
from graphql.language.parser import parse
from graphql.type import GraphQLField, GraphQLObjectType, GraphQLSchema, GraphQLString
Expand Down Expand Up @@ -95,7 +95,7 @@ def resolver(context, *_):
def resolver_2(context, *_):
# type: (Optional[Any], *ResolveInfo) -> NoReturn
asyncio.sleep(0.003)
raise Exception("resolver_2 failed!")
raise GraphQLError("resolver_2 failed!")

Type = GraphQLObjectType(
"Type",
Expand All @@ -117,6 +117,28 @@ def resolver_2(context, *_):
assert result.data == {"a": "hey", "b": None}


def test_asyncio_executor_exceptions_reraised():
# type: () -> None
ast = parse("query Example { a }")

class Error(Exception):
pass

def resolver(context, *_):
# type: (Optional[Any], *ResolveInfo) -> str
raise Error("UH OH!")

Type = GraphQLObjectType(
"Type",
{
"a": GraphQLField(GraphQLString, resolver=resolver),
},
)

with pytest.raises(Error):
execute(GraphQLSchema(Type), ast, executor=AsyncioExecutor())


def test_evaluates_mutations_serially():
# type: () -> None
assert_evaluate_mutations_serially(executor=AsyncioExecutor())
4 changes: 2 additions & 2 deletions graphql/execution/tests/test_executor_gevent.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

gevent = pytest.importorskip("gevent")

from graphql.error import format_error
from graphql.error import format_error, GraphQLError
from graphql.execution import execute
from graphql.language.location import SourceLocation
from graphql.language.parser import parse
Expand Down Expand Up @@ -54,7 +54,7 @@ def resolver(context, *_):

def resolver_2(context, *_):
gevent.sleep(0.003)
raise Exception("resolver_2 failed!")
raise GraphQLError("resolver_2 failed!")

Type = GraphQLObjectType(
"Type",
Expand Down
16 changes: 8 additions & 8 deletions graphql/execution/tests/test_executor_thread.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# type: ignore
from graphql.error import format_error
from graphql.error import format_error, GraphQLError
from graphql.execution import execute
from graphql.language.parser import parse
from graphql.type import (
Expand Down Expand Up @@ -191,19 +191,19 @@ def sync(self):

def syncError(self):
# type: () -> NoReturn
raise Exception("Error getting syncError")
raise GraphQLError("Error getting syncError")

def syncReturnError(self):
# type: () -> Exception
return Exception("Error getting syncReturnError")
return GraphQLError("Error getting syncReturnError")

def syncReturnErrorList(self):
# type: () -> List[Union[Exception, str]]
return [
"sync0",
Exception("Error getting syncReturnErrorList1"),
GraphQLError("Error getting syncReturnErrorList1"),
"sync2",
Exception("Error getting syncReturnErrorList3"),
GraphQLError("Error getting syncReturnErrorList3"),
]

def asyncBasic(self):
Expand All @@ -212,15 +212,15 @@ def asyncBasic(self):

def asyncReject(self):
# type: () -> Promise
return rejected(Exception("Error getting asyncReject"))
return rejected(GraphQLError("Error getting asyncReject"))

def asyncEmptyReject(self):
# type: () -> Promise
return rejected(Exception("An unknown error occurred."))
return rejected(GraphQLError("An unknown error occurred."))

def asyncReturnError(self):
# type: () -> Promise
return resolved(Exception("Error getting asyncReturnError"))
return resolved(GraphQLError("Error getting asyncReturnError"))

schema = GraphQLSchema(
query=GraphQLObjectType(
Expand Down
18 changes: 9 additions & 9 deletions graphql/execution/tests/test_lists.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# type: ignore
from collections import namedtuple

from graphql.error import format_error
from graphql.error import format_error, GraphQLError
from graphql.execution import execute
from graphql.language.parser import parse
from graphql.type import (
Expand Down Expand Up @@ -66,7 +66,7 @@ class Test_ListOfT_Promise_Array_T: # [T] Promise<Array<T>>
)
test_returns_null = check(resolved(None), {"data": {"nest": {"test": None}}})
test_rejected = check(
lambda: rejected(Exception("bad")),
lambda: rejected(GraphQLError("bad")),
{
"data": {"nest": {"test": None}},
"errors": [
Expand All @@ -91,7 +91,7 @@ class Test_ListOfT_Array_Promise_T: # [T] Array<Promise<T>>
{"data": {"nest": {"test": [1, None, 2]}}},
)
test_contains_reject = check(
lambda: [resolved(1), rejected(Exception("bad")), resolved(2)],
lambda: [resolved(1), rejected(GraphQLError("bad")), resolved(2)],
{
"data": {"nest": {"test": [1, None, 2]}},
"errors": [
Expand Down Expand Up @@ -149,7 +149,7 @@ class Test_NotNullListOfT_Promise_Array_T: # [T]! Promise<Array<T>>>
)

test_rejected = check(
lambda: rejected(Exception("bad")),
lambda: rejected(GraphQLError("bad")),
{
"data": {"nest": None},
"errors": [
Expand All @@ -173,7 +173,7 @@ class Test_NotNullListOfT_Array_Promise_T: # [T]! Promise<Array<T>>>
{"data": {"nest": {"test": [1, None, 2]}}},
)
test_contains_reject = check(
lambda: [resolved(1), rejected(Exception("bad")), resolved(2)],
lambda: [resolved(1), rejected(GraphQLError("bad")), resolved(2)],
{
"data": {"nest": {"test": [1, None, 2]}},
"errors": [
Expand Down Expand Up @@ -228,7 +228,7 @@ class TestListOfNotNullT_Promise_Array_T: # [T!] Promise<Array<T>>
test_returns_null = check(resolved(None), {"data": {"nest": {"test": None}}})

test_rejected = check(
lambda: rejected(Exception("bad")),
lambda: rejected(GraphQLError("bad")),
{
"data": {"nest": {"test": None}},
"errors": [
Expand Down Expand Up @@ -262,7 +262,7 @@ class TestListOfNotNullT_Array_Promise_T: # [T!] Array<Promise<T>>
},
)
test_contains_reject = check(
lambda: [resolved(1), rejected(Exception("bad")), resolved(2)],
lambda: [resolved(1), rejected(GraphQLError("bad")), resolved(2)],
{
"data": {"nest": {"test": None}},
"errors": [
Expand Down Expand Up @@ -341,7 +341,7 @@ class TestNotNullListOfNotNullT_Promise_Array_T: # [T!]! Promise<Array<T>>
)

test_rejected = check(
lambda: rejected(Exception("bad")),
lambda: rejected(GraphQLError("bad")),
{
"data": {"nest": None},
"errors": [
Expand Down Expand Up @@ -375,7 +375,7 @@ class TestNotNullListOfNotNullT_Array_Promise_T: # [T!]! Array<Promise<T>>
},
)
test_contains_reject = check(
lambda: [resolved(1), rejected(Exception("bad")), resolved(2)],
lambda: [resolved(1), rejected(GraphQLError("bad")), resolved(2)],
{
"data": {"nest": None},
"errors": [
Expand Down
Loading

0 comments on commit 9b33d5e

Please sign in to comment.