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 graphql ws did not ignore parsing errors #3670

Open
wants to merge 3 commits into
base: main
Choose a base branch
from

Conversation

DoctorJohn
Copy link
Member

@DoctorJohn DoctorJohn commented Oct 13, 2024

Description

The legacy WS protocol dictates that servers should ignore text messages sent by clients that are not valid JSON. Here's the relevant section from the legacy protocol specification:

GQL_CONNECTION_ERROR

[...]

It server also respond with this message in case of a parsing errors of the message (which does not disconnect the client, just ignore the message).

I accidentally changed this behavior in a recent refactor to match the newer protocol, which requires servers to close the WebSocket in this case.

This PR restores the intended behavior and updates the relevant tests.

Types of Changes

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

Summary by Sourcery

Fix the handling of non-JSON messages in the legacy GraphQL WebSocket protocol to ignore parsing errors instead of closing the connection, and update tests to reflect this behavior.

Bug Fixes:

  • Restore the behavior of ignoring non-JSON messages in the legacy GraphQL WebSocket protocol, aligning with the legacy protocol specification.

Documentation:

  • Add a RELEASE.md file documenting the fix for the regression in the legacy GraphQL WebSocket protocol.

Tests:

  • Update tests to ensure non-JSON WebSocket messages are ignored rather than causing a connection closure in the legacy protocol.

Copy link
Contributor

sourcery-ai bot commented Oct 13, 2024

Reviewer's Guide by Sourcery

This pull request fixes a regression in the legacy GraphQL over WebSocket protocol implementation. The main change is to restore the behavior of ignoring client message parsing errors in the legacy protocol, which was accidentally changed in a recent refactor. The PR updates the WebSocket adapters, handlers, and related tests to properly handle and ignore parsing errors when required.

Sequence diagram for WebSocket message handling with parsing errors

sequenceDiagram
    participant Client
    participant Server
    Client->>Server: Send text message
    alt Valid JSON
        Server->>Server: Process message
    else Invalid JSON
        Server->>Server: Ignore message
    end
    Client->>Server: Send non-text message
    Server->>Client: Close connection with error "WebSocket message type must be text"
Loading

Updated class diagram for WebSocket adapters

classDiagram
    class AsyncWebSocketAdapter {
        +iter_json(ignore_parsing_errors: bool) AsyncGenerator
        +send_json(message: Mapping)
    }
    class LitestarWebSocketAdapter {
        +iter_json(ignore_parsing_errors: bool) AsyncGenerator
    }
    class ASGIWebSocketAdapter {
        +iter_json(ignore_parsing_errors: bool) AsyncGenerator
    }
    class ChannelsWebSocketAdapter {
        +iter_json(ignore_parsing_errors: bool) AsyncGenerator
    }
    class AiohttpWebSocketAdapter {
        +iter_json(ignore_parsing_errors: bool) AsyncGenerator
    }
    AsyncWebSocketAdapter <|-- LitestarWebSocketAdapter
    AsyncWebSocketAdapter <|-- ASGIWebSocketAdapter
    AsyncWebSocketAdapter <|-- ChannelsWebSocketAdapter
    AsyncWebSocketAdapter <|-- AiohttpWebSocketAdapter
    note for AsyncWebSocketAdapter "Added ignore_parsing_errors parameter to iter_json method"
Loading

Updated class diagram for WebSocket exceptions

classDiagram
    class NonTextMessageReceived {
    }
    class NonJsonMessageReceived {
    }
    class HTTPException {
        -status_code: int
        -reason: str
    }
    HTTPException <|-- NonTextMessageReceived
    HTTPException <|-- NonJsonMessageReceived
    note for NonTextMessageReceived "New exception for non-text messages"
Loading

File-Level Changes

Change Details Files
Modify WebSocket adapters to handle parsing errors
  • Add 'ignore_parsing_errors' parameter to iter_json method
  • Implement logic to ignore JSON parsing errors when specified
  • Update error handling to distinguish between non-text and non-JSON messages
strawberry/litestar/controller.py
strawberry/asgi/__init__.py
strawberry/aiohttp/views.py
strawberry/channels/handlers/ws_handler.py
strawberry/http/async_base_view.py
Update GraphQL WebSocket protocol handlers
  • Modify legacy protocol handler to ignore parsing errors
  • Update new protocol handler to close connection on parsing errors
  • Improve error handling and messaging for different types of invalid messages
strawberry/subscriptions/protocols/graphql_ws/handlers.py
strawberry/subscriptions/protocols/graphql_transport_ws/handlers.py
Refactor and update WebSocket-related tests
  • Rename test cases to reflect new behavior
  • Add test for ignoring non-JSON messages in legacy protocol
  • Update tests to check for correct error handling in new protocol
tests/websockets/test_graphql_ws.py
tests/websockets/test_graphql_transport_ws.py
Add new exception for non-text messages
  • Create NonTextMessageReceived exception class
strawberry/http/exceptions.py
Add release notes
  • Create RELEASE.md file with information about the bug fix
RELEASE.md

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.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a 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. You can also use
    this command to specify where the summary should be inserted.

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

@DoctorJohn DoctorJohn force-pushed the fix-graphql-ws-did-not-ignore-parsing-errors branch from 8d712d8 to 0ad7d1d Compare October 13, 2024 17:42
Copy link
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 @DoctorJohn - I've reviewed your changes and they look great!

Here's what I looked at during the review
  • 🟢 General issues: all looks good
  • 🟢 Security: all looks good
  • 🟢 Testing: all looks good
  • 🟢 Complexity: all looks good
  • 🟢 Documentation: all looks good

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.

@botberry
Copy link
Member

botberry commented Oct 13, 2024

Thanks for adding the RELEASE.md file!

Here's a preview of the changelog:


This release fixes a regression in the legacy GraphQL over WebSocket protocol.
Legacy protocol implementations should ignore client message parsing errors.
During a recent refactor, Strawberry changed this behavior to match the new protocol, where parsing errors must close the WebSocket connection.
The expected behavior is restored and adequately tested in this release.

Here's the tweet text:

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

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

Copy link

codspeed-hq bot commented Oct 13, 2024

CodSpeed Performance Report

Merging #3670 will not alter performance

Comparing DoctorJohn:fix-graphql-ws-did-not-ignore-parsing-errors (ab7cf21) with main (56172dc)

Summary

✅ 15 untouched benchmarks

Copy link

codecov bot commented Oct 13, 2024

Codecov Report

Attention: Patch coverage is 98.43750% with 1 line in your changes missing coverage. Please review.

Project coverage is 96.70%. Comparing base (d504428) to head (ab7cf21).
Report is 3 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3670      +/-   ##
==========================================
+ Coverage   96.67%   96.70%   +0.03%     
==========================================
  Files         503      503              
  Lines       33457    33486      +29     
  Branches     5602     5624      +22     
==========================================
+ Hits        32344    32384      +40     
  Misses        880      880              
+ Partials      233      222      -11     

Copy link
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.

Left some nits, but LGTM :)

@@ -86,16 +90,19 @@ def __init__(self, request: web.Request, ws: web.WebSocketResponse) -> None:
self.request = request
self.ws = ws

async def iter_json(self) -> AsyncGenerator[Dict[str, object], None]:
async def iter_json(
self, ignore_parsing_errors: bool
Copy link
Member

Choose a reason for hiding this comment

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

nit: ensure this needs to be passed as a keyword to avoid someone calling as iter_json(True) and the True there is hard to understand the meaning

Suggested change
self, ignore_parsing_errors: bool
self,
*,
ignore_parsing_errors: bool,

Copy link
Member

Choose a reason for hiding this comment

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

Also, should it have a default value? (not sure, just asking =P)

@@ -31,20 +31,23 @@ class ChannelsWebSocketAdapter(AsyncWebSocketAdapter):
def __init__(self, request: GraphQLWSConsumer, response: GraphQLWSConsumer) -> None:
self.ws_consumer = response

async def iter_json(self) -> AsyncGenerator[Dict[str, object], None]:
async def iter_json(
self, ignore_parsing_errors: bool
Copy link
Member

Choose a reason for hiding this comment

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

Ditto

@@ -81,7 +81,9 @@ async def get_form_data(self) -> FormData: ...

class AsyncWebSocketAdapter(abc.ABC):
@abc.abstractmethod
def iter_json(self) -> AsyncGenerator[Dict[str, object], None]: ...
def iter_json(
self, ignore_parsing_errors: bool
Copy link
Member

Choose a reason for hiding this comment

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

Ditto

@@ -192,13 +196,22 @@ class LitestarWebSocketAdapter(AsyncWebSocketAdapter):
def __init__(self, request: WebSocket, response: WebSocket) -> None:
self.ws = response

async def iter_json(self) -> AsyncGenerator[Dict[str, object], None]:
async def iter_json(
self, ignore_parsing_errors: bool
Copy link
Member

Choose a reason for hiding this comment

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

Ditto

raise NonTextMessageReceived()

try:
yield json.loads(text)
Copy link
Member

Choose a reason for hiding this comment

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

Not related to this PR (so nothing to change here), but this got me thinking if we should allow for different json libs to be used in places like this, like orjson.

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.

3 participants