Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: propagate entity resolver original error details to response #3144

3 changes: 3 additions & 0 deletions RELEASE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Release type: patch

Update entity resolver exception handling to set the result to the original error instead of a `GraphQLError`, which obscured the original message and meta-fields.
patrick91 marked this conversation as resolved.
Show resolved Hide resolved
33 changes: 14 additions & 19 deletions strawberry/federation/schema.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from collections import defaultdict
from functools import cached_property, partial
from functools import cached_property
from itertools import chain
from typing import (
TYPE_CHECKING,
Expand All @@ -17,8 +17,6 @@
cast,
)

from graphql import GraphQLError

from strawberry.annotation import StrawberryAnnotation
from strawberry.printer import print_schema
from strawberry.schema import Schema as BaseSchema
Expand Down Expand Up @@ -178,28 +176,25 @@ def entities_resolver(
if "info" in func_args:
kwargs["info"] = info

get_result = partial(resolve_reference, **kwargs)
try:
result = resolve_reference(**kwargs)
except Exception as e:
result = e
else:
from strawberry.types.arguments import convert_argument

config = info.schema.config
scalar_registry = info.schema.schema_converter.scalar_registry

get_result = partial(
convert_argument,
representation,
type_=definition.origin,
scalar_registry=scalar_registry,
config=config,
)

try:
result = get_result()
except Exception as e:
result = GraphQLError(
f"Unable to resolve reference for {definition.origin}",
original_error=e,
)
try:
result = convert_argument(
representation,
type_=definition.origin,
scalar_registry=scalar_registry,
config=config,
)
except Exception:
result = TypeError(f"Unable to resolve reference for {type_name}")

results.append(result)

Expand Down
196 changes: 195 additions & 1 deletion tests/federation/test_entities.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import typing

from graphql import located_error

import strawberry
from strawberry.types import Info


def test_fetch_entities():
Expand Down Expand Up @@ -173,6 +176,49 @@ def top_products(self, first: int) -> typing.List[Product]:
}


def test_fails_properly_when_wrong_key_is_passed():
@strawberry.type
class Something:
id: str

@strawberry.federation.type(keys=["upc"])
class Product:
upc: str
something: Something

@strawberry.federation.type(extend=True)
class Query:
@strawberry.field
def top_products(self, first: int) -> typing.List[Product]:
return []

schema = strawberry.federation.Schema(query=Query, enable_federation_2=True)

query = """
query ($representations: [_Any!]!) {
_entities(representations: $representations) {
... on Product {
upc
something {
id
}
}
}
}
"""

result = schema.execute_sync(
query,
variable_values={
"representations": [{"__typename": "Product", "not_upc": "B00005N5PF"}]
},
)

assert result.errors

assert result.errors[0].message == "Unable to resolve reference for Product"


def test_fails_properly_when_wrong_data_is_passed():
@strawberry.federation.type(keys=["id"])
class Something:
Expand Down Expand Up @@ -219,7 +265,155 @@ def top_products(self, first: int) -> typing.List[Product]:

assert result.errors

assert result.errors[0].message.startswith("Unable to resolve reference for")
assert result.errors[0].message == "Unable to resolve reference for Product"


def test_propagates_original_error_message_with_auto_graphql_error_metadata():
@strawberry.federation.type(keys=["id"])
class Product:
id: strawberry.ID

@classmethod
def resolve_reference(cls, id: strawberry.ID) -> "Product":
raise Exception("Foo bar")

@strawberry.federation.type(extend=True)
class Query:
@strawberry.field
def mock(self) -> typing.Optional[Product]:
return None

schema = strawberry.federation.Schema(query=Query, enable_federation_2=True)

query = """
query ($representations: [_Any!]!) {
_entities(representations: $representations) {
... on Product {
id
}
}
}
"""

result = schema.execute_sync(
query,
variable_values={
"representations": [
{
"__typename": "Product",
"id": "B00005N5PF",
}
]
},
)

assert len(result.errors) == 1
error = result.errors[0].formatted
assert error["message"] == "Foo bar"
assert error["path"] == ["_entities", 0]
assert error["locations"] == [{"column": 13, "line": 3}]
assert "extensions" not in error


def test_propagates_custom_type_error_message_with_auto_graphql_error_metadata():
class MyTypeError(TypeError):
pass

@strawberry.federation.type(keys=["id"])
class Product:
id: strawberry.ID

@classmethod
def resolve_reference(cls, id: strawberry.ID) -> "Product":
raise MyTypeError("Foo bar")

@strawberry.federation.type(extend=True)
class Query:
@strawberry.field
def mock(self) -> typing.Optional[Product]:
return None

schema = strawberry.federation.Schema(query=Query, enable_federation_2=True)

query = """
query ($representations: [_Any!]!) {
_entities(representations: $representations) {
... on Product {
id
}
}
}
"""

result = schema.execute_sync(
query,
variable_values={
"representations": [
{
"__typename": "Product",
"id": "B00005N5PF",
}
]
},
)

assert len(result.errors) == 1
error = result.errors[0].formatted
assert error["message"] == "Foo bar"
assert error["path"] == ["_entities", 0]
assert error["locations"] == [{"column": 13, "line": 3}]
assert "extensions" not in error


def test_propagates_original_error_message_and_graphql_error_metadata():
@strawberry.federation.type(keys=["id"])
class Product:
id: strawberry.ID

@classmethod
def resolve_reference(cls, info: Info, id: strawberry.ID) -> "Product":
exception = Exception("Foo bar")
exception.extensions = {"baz": "qux"}
raise located_error(
exception, nodes=info.field_nodes[0], path=["_entities_override", 0]
)

@strawberry.federation.type(extend=True)
class Query:
@strawberry.field
def mock(self) -> typing.Optional[Product]:
return None

schema = strawberry.federation.Schema(query=Query, enable_federation_2=True)

query = """
query ($representations: [_Any!]!) {
_entities(representations: $representations) {
... on Product {
id
}
}
}
"""

result = schema.execute_sync(
query,
variable_values={
"representations": [
{
"__typename": "Product",
"id": "B00005N5PF",
}
]
},
)

assert len(result.errors) == 1
error = result.errors[0].formatted
assert error["message"] == "Foo bar"
assert error["path"] == ["_entities_override", 0]
assert error["locations"] == [{"column": 13, "line": 3}]
assert error["extensions"] == {"baz": "qux"}


async def test_can_use_async_resolve_reference():
Expand Down
Loading