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

fix: on_operation hooks unable to modify result on error. #3629

Merged
merged 13 commits into from
Sep 12, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
79 changes: 40 additions & 39 deletions strawberry/schema/execute.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,52 +156,53 @@ async def execute(
middleware_manager: MiddlewareManager,
execution_context_class: Optional[Type[GraphQLExecutionContext]] = None,
) -> ExecutionResult | PreExecutionError:
try:
async with extensions_runner.operation():
# Note: In graphql-core the schema would be validated here but in
# Strawberry we are validating it at initialisation time instead
try:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (complexity): Consider restructuring the code to reduce nesting and improve readability.

The restructuring has increased code complexity by adding an extra level of nesting. While this change might have been intended to ensure proper closure of the context manager, it can be achieved without increasing the nesting level. Consider using a try-finally block to maintain the original structure while ensuring the context manager is always exited properly:

async def execute(...) -> ExecutionResult | PreExecutionError:
    try:
        async with extensions_runner.operation():
            # Note: In graphql-core the schema would be validated here but in
            # Strawberry we are validating it at initialisation time instead

            if errors := await _parse_and_validate_async(
                execution_context, extensions_runner
            ):
                return await _handle_execution_result(
                    execution_context, errors, extensions_runner, process_errors
                )

            assert execution_context.graphql_document
            result = None
            async with extensions_runner.executing():
                if not execution_context.result:
                    result = await await_maybe(
                        original_execute(
                            schema,
                            execution_context.graphql_document,
                            root_value=execution_context.root_value,
                            middleware=middleware_manager,
                            variable_values=execution_context.variables,
                            operation_name=execution_context.operation_name,
                            context_value=execution_context.context,
                            execution_context_class=execution_context_class,
                        )
                    )
                else:
                    result = execution_context.result

            # return results after all the operation completed.
            return await _handle_execution_result(
                execution_context, result, extensions_runner, process_errors
            )
    except (MissingQueryError, InvalidOperationTypeError) as e:
        raise e
    except Exception as exc:
        return await _handle_execution_result(
            execution_context,
            PreExecutionError(data=None, errors=[_coerce_error(exc)]),
            extensions_runner,
            process_errors,
        )
    finally:
        await extensions_runner.operation().__aexit__(None, None, None)

This approach maintains the original structure, reduces nesting, and ensures proper closure of the context manager in all cases, including when exceptions occur.


if errors := await _parse_and_validate_async(
execution_context, extensions_runner
):
return await _handle_execution_result(
execution_context, errors, extensions_runner, process_errors
)
# Note: In graphql-core the schema would be validated here but in
# Strawberry we are validating it at initialisation time instead

assert execution_context.graphql_document
async with extensions_runner.executing():
if not execution_context.result:
res = await await_maybe(
original_execute(
schema,
execution_context.graphql_document,
root_value=execution_context.root_value,
middleware=middleware_manager,
variable_values=execution_context.variables,
operation_name=execution_context.operation_name,
context_value=execution_context.context,
execution_context_class=execution_context_class,
)
if errors := await _parse_and_validate_async(
execution_context, extensions_runner
):
return await _handle_execution_result(
execution_context, errors, extensions_runner, process_errors
)

else:
res = execution_context.result
except (MissingQueryError, InvalidOperationTypeError) as e:
raise e
except Exception as exc:
return await _handle_execution_result(
execution_context,
PreExecutionError(data=None, errors=[_coerce_error(exc)]),
extensions_runner,
process_errors,
)
assert execution_context.graphql_document
async with extensions_runner.executing():
if not execution_context.result:
result = await await_maybe(
original_execute(
schema,
execution_context.graphql_document,
root_value=execution_context.root_value,
middleware=middleware_manager,
variable_values=execution_context.variables,
operation_name=execution_context.operation_name,
context_value=execution_context.context,
execution_context_class=execution_context_class,
)
)

# return results after all the operation completed.
return await _handle_execution_result(
execution_context, res, extensions_runner, process_errors
)
else:
result = execution_context.result


except (MissingQueryError, InvalidOperationTypeError) as e:
raise e
except Exception as exc:
return await _handle_execution_result(
execution_context,
PreExecutionError(data=None, errors=[_coerce_error(exc)]),
extensions_runner,
process_errors,
)
# return results after all the operation completed.
return await _handle_execution_result(
execution_context, result, extensions_runner, process_errors
)

def execute_sync(
schema: GraphQLSchema,
*,
Expand Down Expand Up @@ -298,4 +299,4 @@ def execute_sync(
)


__all__ = ["execute", "execute_sync"]
__all__ = ["execute", "execute_sync"]
22 changes: 22 additions & 0 deletions tests/schema/extensions/test_mask_errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,28 @@ def hidden_error(self) -> str:
]



async def test_mask_all_errors_async():
@strawberry.type
class Query:
@strawberry.field
def hidden_error(self) -> str:
raise KeyError("This error is not visible")

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

query = "query { hiddenError }"

result = await schema.execute(query)
assert result.errors is not None
formatted_errors = [err.formatted for err in result.errors]
assert formatted_errors == [
{
"locations": [{"column": 9, "line": 1}],
"message": "Unexpected error.",
"path": ["hiddenError"],
}
]
patrick91 marked this conversation as resolved.
Show resolved Hide resolved
def test_mask_some_errors():
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Consider adding an async version of test_mask_some_errors

To maintain consistency and ensure both sync and async paths are tested equally, it would be beneficial to add an async version of the test_mask_some_errors test case.

def test_mask_some_errors():
    # ... existing code ...

import pytest

@pytest.mark.asyncio
async def test_mask_some_errors_async():
    # ... async version of the test ...

class VisibleError(Exception):
pass
Expand Down
Loading