diff --git a/RELEASE.md b/RELEASE.md new file mode 100644 index 0000000000..8d597c725b --- /dev/null +++ b/RELEASE.md @@ -0,0 +1,3 @@ +Release type: patch + +Update federation entity resolver exception handling to set the result to the original error instead of a `GraphQLError`, which obscured the original message and meta-fields. diff --git a/strawberry/federation/schema.py b/strawberry/federation/schema.py index 5ab21fcaa2..e9cbf1625e 100644 --- a/strawberry/federation/schema.py +++ b/strawberry/federation/schema.py @@ -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, @@ -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 @@ -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) diff --git a/tests/federation/test_entities.py b/tests/federation/test_entities.py index 46e4ae49ec..4793c0aeb0 100644 --- a/tests/federation/test_entities.py +++ b/tests/federation/test_entities.py @@ -1,6 +1,9 @@ import typing +from graphql import located_error + import strawberry +from strawberry.types import Info def test_fetch_entities(): @@ -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: @@ -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():