Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions RELEASE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
release type: patch
social_messages:
x: >-
{project_name} {version} is out! This release ensures `MaskErrors` also
masks parsing and validation errors during synchronous execution. πŸ“
https://strawberry.rocks/release/{version}
linkedin: >-
{project_name} {version} is out. This release fixes `MaskErrors` so
synchronous parsing and validation failures no longer expose their original
error details.
---

This release fixes `MaskErrors` leaking parsing and validation error details
during synchronous execution.
Comment thread
patrick91 marked this conversation as resolved.
Outdated

Synchronous execution now masks pre-execution errors consistently with
asynchronous execution, including when `ValidationCache` is enabled.
20 changes: 12 additions & 8 deletions strawberry/extensions/mask_errors.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from collections.abc import Callable, Iterator
from typing import Any
from typing import Protocol, runtime_checkable

from graphql import ExecutionResult as GraphQLExecutionResult
from graphql.error import GraphQLError
Expand All @@ -11,6 +11,11 @@
from strawberry.types.execution import StreamExecutionResult


@runtime_checkable
class _ResultWithErrors(Protocol):
errors: list[GraphQLError] | None


def default_should_mask_error(_: GraphQLError) -> bool:
# Mask all errors
return True
Expand Down Expand Up @@ -39,12 +44,7 @@ def anonymise_error(self, error: GraphQLError) -> GraphQLError:
original_error=None,
)

# TODO: proper typing
def _process_result(self, result: Any) -> None:
errors = getattr(result, "errors", None)
if not errors:
return

def _process_errors(self, errors: list[GraphQLError]) -> list[GraphQLError]:
processed_errors: list[GraphQLError] = []

for error in errors:
Expand All @@ -53,7 +53,11 @@ def _process_result(self, result: Any) -> None:
else:
processed_errors.append(error)

result.errors = processed_errors
return processed_errors

def _process_result(self, result: object) -> None:
if isinstance(result, _ResultWithErrors) and result.errors:
result.errors = self._process_errors(result.errors)

def _process_stream_result(self, result: StreamExecutionResult) -> None:
self._process_result(result)
Expand Down
3 changes: 3 additions & 0 deletions strawberry/schema/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -840,6 +840,9 @@ def execute_sync(
execution_context, extensions_runner
)
) is not None:
# Match the async path by exposing pre-execution results to
# operation extensions before their hooks unwind.
execution_context.result = pre_execution_result
return pre_execution_result

assert execution_context.graphql_document is not None
Expand Down
53 changes: 52 additions & 1 deletion tests/schema/extensions/test_mask_errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,58 @@
from graphql.error import GraphQLError

import strawberry
from strawberry.extensions import MaskErrors
from strawberry.extensions import MaskErrors, ValidationCache


@pytest.mark.parametrize(
"query",
[
"query { testField( }",
"query { missingField }",
],
)
def test_mask_pre_execution_errors_sync(query: str):
@strawberry.type
class Query:
@strawberry.field
def test_field(self) -> str:
return "TestField"

schema = strawberry.Schema(
query=Query,
extensions=[ValidationCache, MaskErrors],
)

result = schema.execute_sync(query)
Comment thread
patrick91 marked this conversation as resolved.
Comment thread
patrick91 marked this conversation as resolved.

assert result.errors is not None
assert [error.message for error in result.errors] == ["Unexpected error."]
Comment thread
patrick91 marked this conversation as resolved.


@pytest.mark.asyncio
@pytest.mark.parametrize(
"query",
[
"query { testField( }",
"query { missingField }",
],
)
async def test_mask_pre_execution_errors_async(query: str):
@strawberry.type
class Query:
@strawberry.field
def test_field(self) -> str:
return "TestField"

schema = strawberry.Schema(
query=Query,
extensions=[ValidationCache, MaskErrors],
)

result = await schema.execute(query)

assert result.errors is not None
assert [error.message for error in result.errors] == ["Unexpected error."]


def test_mask_all_errors():
Expand Down
Loading