Skip to content

Feat #4329: Add on_subscription_result hook to SchemaExtension#4330

Open
Ladol wants to merge 9 commits intostrawberry-graphql:mainfrom
Ladol:sub-extension-fix
Open

Feat #4329: Add on_subscription_result hook to SchemaExtension#4330
Ladol wants to merge 9 commits intostrawberry-graphql:mainfrom
Ladol:sub-extension-fix

Conversation

@Ladol
Copy link
Copy Markdown

@Ladol Ladol commented Mar 26, 2026

Description

Previously, SchemaExtension hooks only wrapped the initial setup phase of a GraphQL subscription, leaving extensions completely disconnected from the actual stream of yielded events.

This commit introduces the on_subscription_result hook to the base SchemaExtension class and triggers it inside the schema._subscribe generator. This allows extensions to safely mutate streamed data before it reaches the transport layer.

Additionally, the MaskErrors extension has been updated to use this new hook, fixing an issue where sensitive errors were leaking unmasked over WebSocket connections.

Types of Changes

  • Core
  • Bugfix
  • New feature
  • Enhancement/optimization
  • Documentation

Issues Fixed or Closed by This PR

#3680
#4329

Checklist

  • My code follows the code style of this project.
  • My change requires a change to the documentation.
  • I have updated the documentation accordingly.
  • I have read the CONTRIBUTING document.
  • I have added tests to cover my changes.
  • I have tested the changes and verified that they work and don't break anything (as well as I can manage).

Summary by Sourcery

Add a subscription-result lifecycle hook to schema extensions and ensure subscription errors are properly masked.

New Features:

  • Introduce an on_subscription_result hook on SchemaExtension that runs for every yielded subscription event, allowing extensions to inspect or mutate each result.

Bug Fixes:

  • Ensure the MaskErrors extension masks exceptions emitted from subscription streams so sensitive error details are not leaked over WebSocket connections.

Enhancements:

  • Wire the new subscription-result hook through the extension runner and subscription execution path so all active extensions can participate in streaming results.

Documentation:

  • Document the new subscription-result hook and behavior change in the release notes.

Tests:

  • Add subscription-focused tests covering custom extensions mutating streamed data and MaskErrors correctly masking subscription errors.

…hemaExtension

Fixes strawberry-graphql#4329
Fixes strawberry-graphql#3680

Previously, `SchemaExtension` hooks only wrapped the initial setup phase
of a GraphQL subscription, leaving extensions completely disconnected from
the actual stream of yielded events.

This commit introduces the `on_subscription_result` hook to the base
`SchemaExtension` class and triggers it inside the `schema._subscribe`
generator. This allows extensions to safely mutate streamed data before
it reaches the transport layer.

Additionally, the `MaskErrors` extension has been updated to use this
new hook, fixing an issue where sensitive errors were leaking unmasked
over WebSocket connections.
@sourcery-ai
Copy link
Copy Markdown
Contributor

sourcery-ai bot commented Mar 26, 2026

Reviewer's Guide

Adds a new on_subscription_result lifecycle hook to schema extensions so they can inspect/mutate each subscription event, wires it into the subscription execution pipeline, updates MaskErrors to use it for streamed results, and adds tests and release notes.

File-Level Changes

Change Details Files
Wire a per-event extension hook into the subscription execution pipeline.
  • Capture the execution result for each subscription event, invoke extension hooks, then yield the (possibly mutated) result.
  • Do the same for exceptions raised during subscription execution, wrapping them into an execution result first.
  • Ensure the hook runs immediately before each result is sent to the client in the _subscribe generator.
strawberry/schema/schema.py
Introduce on_subscription_result to the extension base API and runner.
  • Extend the base SchemaExtension interface with an on_subscription_result method accepting an ExecutionResult.
  • Update typing imports on the base extension to include ExecutionResult.
  • Add an async on_subscription_result method on the extensions runner that iterates extensions and awaits their hook if implemented.
strawberry/extensions/base_extension.py
strawberry/extensions/runner.py
Update the MaskErrors extension to mask errors in subscription streams via the new hook.
  • Add an on_subscription_result implementation that delegates to the existing _process_result method to scrub errors.
  • Preserve existing behavior for non-subscription operations while extending coverage to streaming results.
strawberry/extensions/mask_errors.py
Add tests and release notes for the new subscription result hook behavior.
  • Introduce a dummy StreamModifierExtension and a simple subscription schema to verify that extensions can mutate streamed data.
  • Add a test ensuring MaskErrors correctly masks exceptions raised mid-subscription without leaking sensitive messages.
  • Document the change and its impact in a new RELEASE.md with a minor release note.
tests/extensions/test_subscription_hook.py
RELEASE.md

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@botberry
Copy link
Copy Markdown
Member

botberry commented Mar 26, 2026

Thanks for adding the RELEASE.md file!

Here's a preview of the changelog:


Adds a new on_subscription_result hook to SchemaExtension that allows extensions to interact with and mutate the stream of events yielded by GraphQL subscriptions.

Previously, extensions were only triggered during the initial setup phase of a subscription, meaning transport layers (like WebSockets) bypassed them during the actual data streaming phase. This new hook solves this by executing right before each result is yielded to the client.

As part of this architectural update, the built-in MaskErrors extension has been updated to use this new hook, ensuring that sensitive exceptions are now correctly masked during WebSocket subscriptions.

Here's the tweet text:

🆕 Release (next) is out! Thanks to Ladol for the PR 👏

Get it here 👉 https://strawberry.rocks/release/(next)

Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 1 issue, and left some high level feedback:

  • In ExtensionsRunner.on_subscription_result, the getattr(extension, "on_subscription_result", None) check is redundant because SchemaExtension already defines this method; you can simplify the loop to call extension.on_subscription_result(result) directly and rely on await_maybe to handle sync vs async implementations.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `ExtensionsRunner.on_subscription_result`, the `getattr(extension, "on_subscription_result", None)` check is redundant because `SchemaExtension` already defines this method; you can simplify the loop to call `extension.on_subscription_result(result)` directly and rely on `await_maybe` to handle sync vs async implementations.

## Individual Comments

### Comment 1
<location path="tests/extensions/test_subscription_hook.py" line_range="12-16" />
<code_context>
+
+
+# Dummy extension that uses the new hook
+class StreamModifierExtension(SchemaExtension):
+    def on_subscription_result(self, result: ExecutionResult) -> None:
+        if result.data and "count" in result.data:
+            # Mutate the outgoing data stream
+            result.data["count"] = f"Modified: {result.data['count']}"
+
+
</code_context>
<issue_to_address>
**suggestion (testing):** Consider adding a test that covers an async `on_subscription_result` hook implementation.

Current tests only cover the synchronous implementation. Since `extensions_runner.on_subscription_result` uses `await_maybe`, please add a test with an extension whose `on_subscription_result` is `async def` and awaits a side effect before mutating the result, to confirm async hooks are correctly awaited and safely integrate with async dependencies.

Suggested implementation:

```python
import asyncio

import pytest

import strawberry
from strawberry.extensions import SchemaExtension
from strawberry.extensions.mask_errors import MaskErrors
from strawberry.types import ExecutionResult


# Dummy extension that uses the new hook
class StreamModifierExtension(SchemaExtension):
    def on_subscription_result(self, result: ExecutionResult) -> None:
        if result.data and "count" in result.data:
            # Mutate the outgoing data stream
            result.data["count"] = f"Modified: {result.data['count']}"


class AsyncStreamModifierExtension(SchemaExtension):
    side_effect_ran = False

    async def _side_effect(self) -> None:
        # Simulate an async dependency / side effect
        await asyncio.sleep(0)
        AsyncStreamModifierExtension.side_effect_ran = True

    async def on_subscription_result(self, result: ExecutionResult) -> None:
        # This should be awaited by extensions_runner.on_subscription_result
        await self._side_effect()

        if result.data and "count" in result.data:
            # Mutate the outgoing data stream after the async side effect
            result.data["count"] = f"Modified: {result.data['count']}"


@strawberry.type
class Query:
    # Minimal Query type; field is unused but required by Strawberry
    example: str = "example"


@strawberry.type
class Subscription:
    @strawberry.subscription
    async def count(self) -> int:
        for i in range(3):
            yield i


@pytest.mark.asyncio
async def test_async_on_subscription_result_is_awaited() -> None:
    # Reset class-level flag before running the subscription
    AsyncStreamModifierExtension.side_effect_ran = False

    schema = strawberry.Schema(
        query=Query,
        subscription=Subscription,
        extensions=[AsyncStreamModifierExtension],
    )

    results = schema.subscribe("subscription { count }")

    # Consume first result from the async iterator
    first_result = await results.__anext__()

    assert first_result.errors is None
    assert first_result.data == {"count": "Modified: 0"}
    assert AsyncStreamModifierExtension.side_effect_ran is True

```

If this file already defines `Query` / `Subscription` types or other subscription tests, you may want to:
1. Reuse existing `Query`/`Subscription` instead of defining new ones here, to keep the test suite DRY.
2. Adjust the subscription field name or expected payload in the test to match any shared schema definitions (e.g., if an existing subscription already yields a `count` field).
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +12 to +16
class StreamModifierExtension(SchemaExtension):
def on_subscription_result(self, result: ExecutionResult) -> None:
if result.data and "count" in result.data:
# Mutate the outgoing data stream
result.data["count"] = f"Modified: {result.data['count']}"
Copy link
Copy Markdown
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 a test that covers an async on_subscription_result hook implementation.

Current tests only cover the synchronous implementation. Since extensions_runner.on_subscription_result uses await_maybe, please add a test with an extension whose on_subscription_result is async def and awaits a side effect before mutating the result, to confirm async hooks are correctly awaited and safely integrate with async dependencies.

Suggested implementation:

import asyncio

import pytest

import strawberry
from strawberry.extensions import SchemaExtension
from strawberry.extensions.mask_errors import MaskErrors
from strawberry.types import ExecutionResult


# Dummy extension that uses the new hook
class StreamModifierExtension(SchemaExtension):
    def on_subscription_result(self, result: ExecutionResult) -> None:
        if result.data and "count" in result.data:
            # Mutate the outgoing data stream
            result.data["count"] = f"Modified: {result.data['count']}"


class AsyncStreamModifierExtension(SchemaExtension):
    side_effect_ran = False

    async def _side_effect(self) -> None:
        # Simulate an async dependency / side effect
        await asyncio.sleep(0)
        AsyncStreamModifierExtension.side_effect_ran = True

    async def on_subscription_result(self, result: ExecutionResult) -> None:
        # This should be awaited by extensions_runner.on_subscription_result
        await self._side_effect()

        if result.data and "count" in result.data:
            # Mutate the outgoing data stream after the async side effect
            result.data["count"] = f"Modified: {result.data['count']}"


@strawberry.type
class Query:
    # Minimal Query type; field is unused but required by Strawberry
    example: str = "example"


@strawberry.type
class Subscription:
    @strawberry.subscription
    async def count(self) -> int:
        for i in range(3):
            yield i


@pytest.mark.asyncio
async def test_async_on_subscription_result_is_awaited() -> None:
    # Reset class-level flag before running the subscription
    AsyncStreamModifierExtension.side_effect_ran = False

    schema = strawberry.Schema(
        query=Query,
        subscription=Subscription,
        extensions=[AsyncStreamModifierExtension],
    )

    results = schema.subscribe("subscription { count }")

    # Consume first result from the async iterator
    first_result = await results.__anext__()

    assert first_result.errors is None
    assert first_result.data == {"count": "Modified: 0"}
    assert AsyncStreamModifierExtension.side_effect_ran is True

If this file already defines Query / Subscription types or other subscription tests, you may want to:

  1. Reuse existing Query/Subscription instead of defining new ones here, to keep the test suite DRY.
  2. Adjust the subscription field name or expected payload in the test to match any shared schema definitions (e.g., if an existing subscription already yields a count field).

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 26, 2026

Greptile Summary

This PR introduces the on_subscription_result hook to SchemaExtension, allowing extensions to inspect and mutate each event emitted by a GraphQL subscription before it reaches the transport layer. The hook is wired into SchemaExtensionsRunner and invoked at every yield site inside Schema._subscribe, including pre-execution error paths. MaskErrors is updated to use this hook, fixing the known gap where subscription errors leaked unmasked over WebSocket connections.

The architecture is sound: SubscriptionResultContextManager correctly threads the per-event ExecutionResult through context-manager entry/exit, and both sync-generator and async-generator hook forms are handled by the existing get_hook machinery in ExtensionContextManagerBase.

  • New on_subscription_result hook added to SchemaExtension and wired through SchemaExtensionsRunner and every yield site in _subscribe
  • MaskErrors updated to delegate per-event masking to on_subscription_result, fixing the WebSocket error-leaking issue
  • P1 regression in MaskErrors.on_operation: the except RuntimeError: return block intended for subscription parse errors also silently skips masking for regular query/mutation parse errors; no test in this PR covers this regression path

Confidence Score: 4/5

Safe to merge after fixing the MaskErrors parse-error regression in on_operation.

The new hook and all its plumbing are correct and well-tested for subscription paths. One P1 regression in MaskErrors.on_operation would cause query and mutation parse errors to be returned unmasked to callers relying on MaskErrors for error sanitisation.

strawberry/extensions/mask_errors.py lines 61-65 — the except RuntimeError: return block.

Important Files Changed

Filename Overview
RELEASE.md New release notes documenting the on_subscription_result hook as a minor feature; purely informational.
strawberry/extensions/base_extension.py Adds on_subscription_result hook with default no-op body, widens Hook type union, and registers the hook in HOOK_METHODS — additive and consistent with existing patterns.
strawberry/extensions/context.py Adds SubscriptionResultContextManager that correctly threads result into from_callable, __enter__, and __aenter__; __exit__/__aexit__ are correctly inherited from the base class.
strawberry/extensions/mask_errors.py P1 regression: except RuntimeError: return in on_operation silently skips error masking for all parse failures (including regular queries/mutations), not just subscriptions.
strawberry/extensions/runner.py Adds on_subscription_result factory method to SchemaExtensionsRunner following the identical pattern as existing lifecycle methods.
strawberry/schema/schema.py Wraps every yield site in _subscribe with on_subscription_result, providing comprehensive coverage of pre-execution errors, stream results, and runtime exceptions.
tests/extensions/test_subscription_hook.py Tests cover the happy path, async hooks, and MaskErrors on subscription errors, but no test covers the regression where non-subscription parse errors are no longer masked.

Sequence Diagram

sequenceDiagram
    participant Client
    participant Schema
    participant ExtRunner as ExtensionsRunner
    participant MaskErrors
    participant GQLCore as graphql-core

    Client->>Schema: subscribe(query, ...)
    Schema->>Schema: _create_execution_context()
    Schema->>ExtRunner: create_extensions_runner(ctx, extensions)
    Schema->>Schema: _subscribe(ctx, extensions_runner, ...)
    activate Schema
    ExtRunner->>MaskErrors: on_operation() [enter]
    Schema->>Schema: _parse_and_validate_async(ctx, extensions_runner)

    alt Parse or validation error
        Schema-->>ExtRunner: initial_error (PreExecutionError)
        Schema->>Schema: _handle_execution_result(ctx, initial_error)
        ExtRunner->>MaskErrors: on_subscription_result(result) [enter]
        MaskErrors->>MaskErrors: _process_result(result)
        MaskErrors-->>ExtRunner: yield
        Schema-->>Client: yield execution_result
        Schema-->>Schema: return
    else Successful parse and validation
        Schema->>GQLCore: subscribe(schema, document, ...)
        GQLCore-->>Schema: async iterator
        loop For each streamed result
            GQLCore-->>Schema: GraphQLExecutionResult
            Schema->>Schema: _handle_execution_result(ctx, result)
            ExtRunner->>MaskErrors: on_subscription_result(extension_result) [enter]
            MaskErrors->>MaskErrors: _process_result(result)
            MaskErrors-->>ExtRunner: yield
            Schema-->>Client: yield extension_result
        end
    end

    ExtRunner->>MaskErrors: on_operation() [exit]
    note over ExtRunner,MaskErrors: on_operation skips masking for subscriptions
    deactivate Schema
Loading

Reviews (8): Last reviewed commit: "Fix: Return on RuntimeError in MaskError..." | Re-trigger Greptile

Comment on lines +43 to +49
async def on_subscription_result(self, result: ExecutionResult) -> None:
"""Run the subscription result hook across all active extensions."""
for extension in self.extensions:
# Check if the extension implemented the new hook
hook = getattr(extension, "on_subscription_result", None)
if hook:
await await_maybe(hook(result))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 getattr guard is always truthy

The comment says "Check if the extension implemented the new hook", but since SchemaExtension now defines on_subscription_result with a default no-op body, every extension will always carry this attribute. The if hook: branch is therefore always taken, making the check misleading without being harmful (the base-class call simply returns None which await_maybe handles fine).

Consider simplifying to remove the dead guard:

Suggested change
async def on_subscription_result(self, result: ExecutionResult) -> None:
"""Run the subscription result hook across all active extensions."""
for extension in self.extensions:
# Check if the extension implemented the new hook
hook = getattr(extension, "on_subscription_result", None)
if hook:
await await_maybe(hook(result))
async def on_subscription_result(self, result: ExecutionResult) -> None:
"""Run the subscription result hook across all active extensions."""
for extension in self.extensions:
await await_maybe(extension.on_subscription_result(result))

@Ladol
Copy link
Copy Markdown
Author

Ladol commented Mar 26, 2026

@greptileai

@Ladol
Copy link
Copy Markdown
Author

Ladol commented Mar 26, 2026

@greptileai

@Ladol
Copy link
Copy Markdown
Author

Ladol commented Mar 26, 2026

@greptileai

@Ladol
Copy link
Copy Markdown
Author

Ladol commented Mar 26, 2026

I've pushed updates addressing the feedback from the bots automatic reviews.

The bot suggested adding the new hook to the HOOK_METHODS set in base_extension.py, but when this hook was there it recommended removing it, so I'm unsure of what the best practice would be.

Looking forward for a review.

"""Called before and after the execution step."""
yield None

def on_subscription_result(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think we should follow the same patterns as the others here, using yield None, which would allow extensions to do something before and after

Wdyt?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Hi.
I agree. I think it makes sense to follow the patterns as the other methods, and this change allows for more flexibility for the extensions.
I have just made a new commit and pushed the updates to implement this. Thank you for your review.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a new subscription lifecycle hook so SchemaExtension implementations can inspect/mutate each streamed subscription ExecutionResult, fixing MaskErrors not masking errors yielded during subscription streaming (e.g., over WebSockets).

Changes:

  • Introduces SchemaExtension.on_subscription_result(result) (sync or async) and wires it through SchemaExtensionsRunner.
  • Invokes the new hook for each yielded subscription result inside Schema._subscribe.
  • Updates MaskErrors to mask errors for streamed subscription results and adds targeted subscription tests + release note.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
strawberry/extensions/base_extension.py Adds the on_subscription_result hook to the base extension API.
strawberry/extensions/runner.py Adds a runner method to invoke the hook across all active extensions (awaiting async hooks).
strawberry/schema/schema.py Calls on_subscription_result for initial, pre-execution, per-event, and exception-derived subscription results.
strawberry/extensions/mask_errors.py Uses the new hook to mask subscription-stream errors.
tests/extensions/test_subscription_hook.py Adds tests for stream mutation, async hook awaiting, and MaskErrors masking on subscriptions.
RELEASE.md Documents the new hook and the MaskErrors behavior change.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

execution_context, initial_error, extensions_runner
)
await extensions_runner.on_subscription_result(execution_result)
yield execution_result
Copy link

Copilot AI Apr 5, 2026

Choose a reason for hiding this comment

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

When _parse_and_validate_async returns an initial_error, _subscribe yields it but then continues into the execution path. For parse errors this can hit the assert execution_context.graphql_document is not None and yield an additional (unrelated) error; for validation errors it can also lead to duplicate error results from subscribe(...). Consider returning immediately after yielding the initial error (or wrapping the rest of the function in an else) so subscription setup stops cleanly on pre-execution failures.

Suggested change
yield execution_result
yield execution_result
return

Copilot uses AI. Check for mistakes.
elif result:
self._process_result(result.initial_result)

def on_subscription_result(self, result: StrawberryExecutionResult) -> None:
Copy link

Copilot AI Apr 5, 2026

Choose a reason for hiding this comment

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

MaskErrors.on_subscription_result narrows the parameter type to StrawberryExecutionResult, but the base SchemaExtension.on_subscription_result is typed as taking ExecutionResult. This narrower override can trigger type-checking issues (it’s not substitutable if the runner passes a base ExecutionResult). Prefer matching the base signature (ExecutionResult) here.

Suggested change
def on_subscription_result(self, result: StrawberryExecutionResult) -> None:
def on_subscription_result(self, result: GraphQLExecutionResult) -> None:

Copilot uses AI. Check for mistakes.
Comment on lines +152 to +157
assert len(result.errors) == 1

# The crucial check: MaskErrors successfully intercepted and masked it!
error_message = result.errors[0].message
assert error_message == "Unexpected error."
assert "fieldThatDoesNotExist" not in error_message
Copy link

Copilot AI Apr 5, 2026

Choose a reason for hiding this comment

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

This test asserts len(result.errors) == 1, but earlier comment notes Strawberry may yield 1+ validation errors. Since MaskErrors masks messages without changing the number of errors, this assertion will be flaky depending on validation output. Consider asserting that result.errors is non-empty and that all error messages are masked (iterate over result.errors) rather than enforcing a single error.

Suggested change
assert len(result.errors) == 1
# The crucial check: MaskErrors successfully intercepted and masked it!
error_message = result.errors[0].message
assert error_message == "Unexpected error."
assert "fieldThatDoesNotExist" not in error_message
assert result.errors
# The crucial check: MaskErrors successfully intercepted and masked every error!
for error in result.errors:
error_message = error.message
assert error_message == "Unexpected error."
assert "fieldThatDoesNotExist" not in error_message

Copilot uses AI. Check for mistakes.
Comment on lines +119 to +124
query = "subscription { count }"
sub_generator = await schema.subscribe(query)

# Consume first result from the async iterator
first_result = await sub_generator.__anext__()

Copy link

Copilot AI Apr 5, 2026

Choose a reason for hiding this comment

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

The test consumes only the first item from the subscription iterator and never closes it. Other subscription tests in the repo wrap schema.subscribe(...) in contextlib.aclosing(...) to ensure the async generator is closed even when not fully exhausted. Consider using aclosing (or explicitly calling await sub_generator.aclose()) to avoid leaking an open subscription iterator.

Copilot uses AI. Check for mistakes.
Comment on lines +54 to +60
def on_subscription_result(
self, result: ExecutionResult
) -> None | AwaitableOrValue[None]:
"""Called exactly once for every event/result yielded by a GraphQL subscription.

Extensions can mutate the `result` object directly (e.g., masking errors).
"""
Copy link

Copilot AI Apr 5, 2026

Choose a reason for hiding this comment

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

The return type for on_subscription_result is currently None | AwaitableOrValue[None], but AwaitableOrValue[None] already includes None. Consider simplifying this to AwaitableOrValue[None] to avoid redundant typing and match the pattern used by other extension methods like resolve/get_results.

Copilot uses AI. Check for mistakes.
@codspeed-hq
Copy link
Copy Markdown

codspeed-hq bot commented Apr 5, 2026

Merging this PR will not alter performance

✅ 31 untouched benchmarks


Comparing Ladol:sub-extension-fix (29754da) with main (d1b75b1)

Open in CodSpeed

@Ladol
Copy link
Copy Markdown
Author

Ladol commented Apr 6, 2026

@greptileai

@Ladol
Copy link
Copy Markdown
Author

Ladol commented Apr 6, 2026

@greptileai

@Ladol
Copy link
Copy Markdown
Author

Ladol commented Apr 6, 2026

@greptileai

@Ladol
Copy link
Copy Markdown
Author

Ladol commented Apr 6, 2026

@greptileai

@Ladol Ladol requested a review from bellini666 April 6, 2026 18:47
Copy link
Copy Markdown
Member

@bellini666 bellini666 left a comment

Choose a reason for hiding this comment

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

This looks good to me. But would like @patrick91 's opinion here as well 🙏🏼

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants