Skip to content

Commit f8af92a

Browse files
committed
Fix MaskErrors for synchronous pre-execution errors
1 parent e6ffbc3 commit f8af92a

4 files changed

Lines changed: 109 additions & 49 deletions

File tree

RELEASE.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
release type: patch
3+
social_messages:
4+
x: >-
5+
{project_name} {version} is out! This release ensures `MaskErrors` also
6+
masks parsing and validation errors during synchronous execution. 🍓
7+
https://strawberry.rocks/release/{version}
8+
linkedin: >-
9+
{project_name} {version} is out. This release fixes `MaskErrors` so
10+
synchronous parsing and validation failures no longer expose their original
11+
error details.
12+
---
13+
14+
This release fixes `MaskErrors` leaking parsing and validation error details
15+
during synchronous execution.
16+
17+
Synchronous execution now masks pre-execution errors consistently with
18+
asynchronous execution, including when `ValidationCache` is enabled.

strawberry/extensions/mask_errors.py

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,7 @@ def anonymise_error(self, error: GraphQLError) -> GraphQLError:
3939
original_error=None,
4040
)
4141

42-
# TODO: proper typing
43-
def _process_result(self, result: Any) -> None:
44-
errors = getattr(result, "errors", None)
45-
if not errors:
46-
return
47-
42+
def _process_errors(self, errors: list[GraphQLError]) -> list[GraphQLError]:
4843
processed_errors: list[GraphQLError] = []
4944

5045
for error in errors:
@@ -53,7 +48,13 @@ def _process_result(self, result: Any) -> None:
5348
else:
5449
processed_errors.append(error)
5550

56-
result.errors = processed_errors
51+
return processed_errors
52+
53+
# TODO: proper typing
54+
def _process_result(self, result: Any) -> None:
55+
errors = getattr(result, "errors", None)
56+
if errors:
57+
result.errors = self._process_errors(errors)
5758

5859
def _process_stream_result(self, result: StreamExecutionResult) -> None:
5960
self._process_result(result)
@@ -79,6 +80,11 @@ def on_operation(self) -> Iterator[None]:
7980
self._process_result(result)
8081
elif initial_result := getattr(result, "initial_result", None):
8182
self._process_result(initial_result)
83+
# Synchronous parsing and validation failures don't populate `result`.
84+
elif pre_execution_errors := self.execution_context.pre_execution_errors:
85+
self.execution_context.pre_execution_errors = self._process_errors(
86+
pre_execution_errors
87+
)
8288

8389
def on_stream_result(self, result: StreamExecutionResult) -> Iterator[None]:
8490
"""Mask errors before a streamed execution result reaches the client."""

strawberry/schema/schema.py

Lines changed: 52 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -831,54 +831,63 @@ def execute_sync(
831831
operation_extensions, sync=True
832832
)
833833

834+
pre_execution_result: ExecutionResult | None = None
835+
834836
try:
835837
with extensions_runner.operation():
836838
# Note: In graphql-core the schema would be validated here but in
837839
# Strawberry we are validating it at initialisation time instead
838-
if (
839-
pre_execution_result := self._prepare_operation_sync(
840-
execution_context, extensions_runner
841-
)
842-
) is not None:
843-
return pre_execution_result
844-
845-
assert execution_context.graphql_document is not None
846-
with extensions_runner.executing():
847-
if not execution_context.result:
848-
result = execute_function(
849-
self._schema,
850-
execution_context.graphql_document,
851-
root_value=execution_context.root_value,
852-
middleware=middleware_manager,
853-
variable_values=execution_context.variables,
854-
operation_name=execution_context.operation_name,
855-
context_value=execution_context.context,
856-
is_awaitable=optimized_is_awaitable,
857-
**execution_context_class_kwargs(
858-
self.execution_context_class
859-
),
860-
**custom_context_kwargs,
861-
)
840+
pre_execution_result = self._prepare_operation_sync(
841+
execution_context, extensions_runner
842+
)
862843

863-
if isawaitable(result):
864-
result = cast("Awaitable[GraphQLExecutionResult]", result)
865-
ensure_future(result).cancel()
866-
raise RuntimeError( # noqa: TRY301
867-
"GraphQL execution failed to complete synchronously."
844+
if pre_execution_result is None:
845+
assert execution_context.graphql_document is not None
846+
with extensions_runner.executing():
847+
if not execution_context.result:
848+
result = execute_function(
849+
self._schema,
850+
execution_context.graphql_document,
851+
root_value=execution_context.root_value,
852+
middleware=middleware_manager,
853+
variable_values=execution_context.variables,
854+
operation_name=execution_context.operation_name,
855+
context_value=execution_context.context,
856+
is_awaitable=optimized_is_awaitable,
857+
**execution_context_class_kwargs(
858+
self.execution_context_class
859+
),
860+
**custom_context_kwargs,
868861
)
869862

870-
result = cast("GraphQLExecutionResult", result)
871-
execution_context.result = result
872-
# Also set errors on the context so that it's easier
873-
# to access in extensions
874-
if result.errors:
875-
execution_context.pre_execution_errors = result.errors
876-
877-
# Run the `Schema.process_errors` function here before
878-
# extensions have a chance to modify them (see the MaskErrors
879-
# extension). That way we can log the original errors but
880-
# only return a sanitised version to the client.
881-
self._process_errors(result.errors, execution_context)
863+
if isawaitable(result):
864+
result = cast(
865+
"Awaitable[GraphQLExecutionResult]", result
866+
)
867+
ensure_future(result).cancel()
868+
raise RuntimeError( # noqa: TRY301
869+
"GraphQL execution failed to complete synchronously."
870+
)
871+
872+
result = cast("GraphQLExecutionResult", result)
873+
execution_context.result = result
874+
# Also set errors on the context so that it's easier
875+
# to access in extensions
876+
if result.errors:
877+
execution_context.pre_execution_errors = result.errors
878+
879+
# Run the `Schema.process_errors` function here before
880+
# extensions have a chance to modify them (see the
881+
# MaskErrors extension). That way we can log the original
882+
# errors but only return a sanitised version to the client.
883+
self._process_errors(result.errors, execution_context)
884+
885+
if pre_execution_result is not None:
886+
# Operation hooks may replace pre-execution errors (for example,
887+
# MaskErrors anonymises them), so finalise the returned result only
888+
# after those hooks have completed.
889+
pre_execution_result.errors = execution_context.pre_execution_errors
890+
return pre_execution_result
882891
except (
883892
MissingQueryError,
884893
CannotGetOperationTypeError,
@@ -894,6 +903,8 @@ def execute_sync(
894903
errors=errors,
895904
extensions=extensions_runner.get_extensions_results_sync(),
896905
)
906+
907+
assert execution_context.result is not None
897908
return ExecutionResult(
898909
data=execution_context.result.data,
899910
errors=execution_context.result.errors,

tests/schema/extensions/test_mask_errors.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,32 @@
44
from graphql.error import GraphQLError
55

66
import strawberry
7-
from strawberry.extensions import MaskErrors
7+
from strawberry.extensions import MaskErrors, ValidationCache
8+
9+
10+
@pytest.mark.parametrize(
11+
"query",
12+
[
13+
"query { testField( }",
14+
"query { missingField }",
15+
],
16+
)
17+
def test_mask_pre_execution_errors_sync(query: str):
18+
@strawberry.type
19+
class Query:
20+
@strawberry.field
21+
def test_field(self) -> str:
22+
return "TestField"
23+
24+
schema = strawberry.Schema(
25+
query=Query,
26+
extensions=[ValidationCache, MaskErrors],
27+
)
28+
29+
result = schema.execute_sync(query)
30+
31+
assert result.errors is not None
32+
assert [error.message for error in result.errors] == ["Unexpected error."]
833

934

1035
def test_mask_all_errors():

0 commit comments

Comments
 (0)