feat: add mask_pre_execution_errors option to MaskErrors - #4577
Conversation
MaskErrors masks every error, including the syntax errors of a document and the validation errors against the schema. Clients that send an invalid query only get the generic message, so they cannot correct it. Add a mask_pre_execution_errors option. It defaults to True, which keeps the current behaviour. Set it to False to send parse errors and validation errors to the client while the errors that resolvers raise stay masked. The extension cannot tell which phase produced an error, so it now tracks the phase with the on_parse and on_execute hooks. The parse step and the validation step always run before execution in every path, so an operation that reaches on_execute did not fail in those steps. This behaves the same for sync, async, streaming and incremental execution. Closes strawberry-graphql#3326
|
Thanks for adding the Below is the changelog that will be used for the release. This release adds a
import strawberry
from strawberry.extensions import MaskErrors
@strawberry.type
class Query:
@strawberry.field
def hello(self) -> str:
return "world"
@strawberry.field
def hidden_error(self) -> str:
raise KeyError("This error will not be visible")
schema = strawberry.Schema(
Query,
extensions=[
lambda: MaskErrors(mask_pre_execution_errors=False),
],
)
# "Cannot query field 'helloo' on type 'Query'. Did you mean 'hello'?"
schema.execute_sync("{ helloo }")
# "Unexpected error."
schema.execute_sync("{ hiddenError }")The default value is This release also fixes an error leak in |
There was a problem hiding this comment.
Hey - I've left some high level feedback:
- The
_in_pre_execution_phaseflag is stored on the extension instance and toggled by lifecycle hooks; if an extension instance can be reused concurrently across operations (e.g., in async or threaded environments), consider making this phase-tracking state operation-scoped rather than instance-scoped to avoid race conditions and mis-masking in concurrent executions.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- The `_in_pre_execution_phase` flag is stored on the extension instance and toggled by lifecycle hooks; if an extension instance can be reused concurrently across operations (e.g., in async or threaded environments), consider making this phase-tracking state operation-scoped rather than instance-scoped to avoid race conditions and mis-masking in concurrent executions.Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
Greptile SummaryThe PR adds an opt-out for masking parse and validation errors while continuing to mask resolver failures. It introduces lifecycle-based phase tracking, documents the new constructor option, and adds sync, async, and streaming coverage.
Confidence Score: 3/5The PR is not safe to merge until phase tracking is isolated per operation so concurrent requests cannot expose resolver errors. The new masking decision depends on mutable extension-instance state, and the repository permits factories to return a shared instance across concurrent operations, allowing one request's parse phase to disable masking for another request's resolver failure. Files Needing Attention: strawberry/extensions/mask_errors.py and tests/schema/extensions/test_stream_result.py
|
| Filename | Overview |
|---|---|
| strawberry/extensions/mask_errors.py | Adds phase-sensitive masking, but stores per-operation phase on an instance that can be shared concurrently, allowing resolver errors to bypass masking. |
| tests/schema/extensions/test_mask_errors.py | Adds focused sync and async coverage for exposing parse and validation errors while masking resolver failures. |
| tests/schema/extensions/test_stream_result.py | Covers streaming and sequential shared-instance reuse but omits concurrent reuse, where the new phase state races. |
| docs/extensions/mask-errors.md | Clearly documents the option, default behavior, and schema-disclosure tradeoff. |
| RELEASE.md | Provides release metadata and an example of the new public option. |
Sequence Diagram
sequenceDiagram
participant A as Operation A
participant M as Shared MaskErrors
participant B as Operation B
A->>M: on_execute()
M->>M: "phase = execution"
A->>A: Resolver raises sensitive error
B->>M: on_parse()
M->>M: "phase = pre-execution"
A->>M: on_operation / on_stream_result
M-->>A: Skip masking due to B's phase
Reviews (1): Last reviewed commit: "fix extension passing format in example" | Re-trigger Greptile
|
I will resolve the comments from Sourcery/Greptile. |
Description
The original report – that
MaskErrorsdoes not mask validation errors – was already fixed by #3968 and #4330.MaskErrorsmasks parse errors and validation errors in every execution path today.What is still open is the option that @erikwrede and @patrick91 asked for in that thread. There are two separate concerns: hide what the resolvers raise, and hide the shape of the schema. You cannot ask for the first one alone, so a client that sends a malformed document only gets
Unexpected error.and cannot correct its query.This PR adds
mask_pre_execution_errorsto theMaskErrorsconstructor. It defaults toTrue, so the behaviour does not change for anybody.How it works
The extension masks the errors of
execution_context.resultand of each streamed frame. At that point it cannot tell which phase produced them. It now tracks the phase with the existing lifecycle hooks:on_parsesets a private flag andon_executeclears it. Every execution path entersparsing()beforeexecuting(), so an operation that reacheson_executedid not fail to parse or to validate.on_operationandon_stream_resultboth consult the flag, which keeps the sync, async, streaming, subscription and incremental paths consistent.Errors that occur before the parse step, a missing query for example, stay masked.
Schema.process_errorsstill receives the original errors, so logs do not change.Validation
uv run pytest– 5122 passed. The single failure,tests/typecheckers/test_pydantic.py::test_pydantic_type, also fails on a clean checkout here, because a local binary is missing.uv run mypy --config-file mypy.ini– clean, 240 source files.uv run ruff check .anduv run ruff format --check .– clean.RELEASE.mdincluded, release typeminor.Types of Changes
Issues Fixed or Closed by This PR
MaskErrorsdoes not mask validation errors #3326Checklist
Summary by Sourcery
Add a configurable option to the MaskErrors extension to control whether syntax and validation errors are masked before execution while keeping resolver-raised errors masked by default.
New Features:
Enhancements:
Documentation:
Tests:
Chores: