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

Subscriptions: SSE distinct connection support #1195

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

Conversation

danplischke
Copy link

This PR introduces a first implementation of subscriptions over Server-Sent Events (SSE) based on the definition in graphql-sse. This provides an alternative to the existing WebSocket support, which is particularly useful in scenarios where WebSocket connections may be restricted (e.g., corporate proxies).

Key Features / Considerations

  • SSE Support: Added to the GraphQLHTTPHandler ASGI class, specifically for subscriptions.
  • Routing Requirement: The request must include Accept: text/event-stream to be routed to the SSE handler.
  • Distinct Connection Support: Currently supports only one subscription per HTTP connection ('distinct connection mode' in the protocol definition). The single connection mode would require state management, which has not been implemented due to Ariadne's stateless design.
  • Custom Starlette Response Class: A custom response class for SSE support has been added, based on the implementation in sse-starlette to avoid introducing an additional dependency.

It would be great to get some feedback from you on:

  • Does adding SSE support align with Ariadne's goals, given that the protocol is not part of the official GraphQL Over HTTP specification?
  • Should SSE be implemented as a separate handler, similar to WebSockets, or can it be integrated into the existing GraphQLHTTPHandler?
  • Should there be a way of disabling SSE support?
  • How can I best pass configuration options (i.e. send_timeout, ping_interval, headers, encoding) for the 'ServerSentEventResponse' from the 'GraphQLHTTPHandler'? Are parameters / member variables in the init of the 'GraphQLHTTPHandler' acceptable or do you see a cleaner alternative?
  • Since the 'subscribe' function returns formatted GraphQL errors as dicts, do you see a more elegant solution of passing them in an 'ExecutionResult' to the 'GrapQLServerSentEvent'? Modifying the 'GrapQLServerSentEvent' to accept dicts, seemed a bit hacky.

Looking forward to your feedback!
Thanks in advance!

@danplischke
Copy link
Author

Hey, I wanted to see if someone has any feedback for me on this.
Tagging @rafalp, since you have the most contributions in this project.

Thanks!

@rafalp
Copy link
Collaborator

rafalp commented Oct 10, 2024

Apologies @danplischke but I am no longer with Mirumee.

Poking @DamianCzajkowski

@mociepka
Copy link
Member

Hi @danplischke, thank you for the contribution. We are currently paced with commercial projects, but I am searching for a solution for you and the rest of the new PRs.

Are you on the time pressure?

@danplischke
Copy link
Author

Hi @mociepka
thanks for reaching back. No time pressure from my side. Currently, I just use my forked repository (https://github.com/danplischke/ariadne/tree/main) in a couple of projects.
But I think it would be nice to get this integrated eventually.

Let me know if I can somehow make this easier on you. Happy to help.

@rafalp rafalp added this to the Next release milestone Nov 19, 2024
@rafalp
Copy link
Collaborator

rafalp commented Dec 5, 2024

If this is to be merged into Ariadne, it needs to be done as a separate handler. It will also need to have a documentation explaining what it is and how to use it (linking to graphql-sse repo with comment "go and read it" won't cut it as a doc).

However, a question needs to be answered first (by @mociepka) if there will be a commitment to maintaining this after it gets merged. Because when something gets into Ariadne it means we are taking 100% responsibility for it and we will fix it when bugs happen or spec changes and client libs are suddenly broke. Is the GraphQL SSE even versioned or is it a living standard where whatever is there in latest release is the spec?

If answer to those two questions is True, we could put SSE support handler in ariadne.contrib (to distinct it from core handlers) and also note in docs that Ariadne supports GraphQL SSE version X.Y.Z and this is achieved as community contributed module importable from ariadne.contrib.

Also, making this part of Ariadne will mean eventual fixes or updates in SSE support will be tied to Ariadne releases.

I'll drop suggestions to the actual code in this PR later, but at quick glance I wonder how does the protocol that uses line terminator as separator handle multiline characters in result payloads?

@rafalp rafalp assigned rafalp and danplischke and unassigned rafalp Dec 5, 2024
@rafalp rafalp self-requested a review December 5, 2024 20:40
@mociepka
Copy link
Member

Thank you, @rafalp, for outlining the risks and opportunities. SSE as a transport layer looks like a valuable addition, and I am happy to welcome it. I understand that SSE is not yet standardized, but it originates from @enisdenjo, and (correct me if I am wrong, @rafalp) we use his spec for graphql-ws.

Addressing @danplischke's questions:

  • SSE aligns with Ariadne's goals, even given its unofficial status.
  • As @rafalp mentioned, to be in line of the current architecture defining SSE as a separate handler in the ariadne.contrib module is the preferable approach. @danplischke, could you let us know if you have the capacity to implement these changes?
  • If the logic is moved to a separate handler and the contrib module, there’s no need to implement a disabling feature.
  • @rafalp, could you advise on the best option for passing configuration?
  • @rafalp, could you also suggest a more elegant solution for passing GraphQL errors from the subscribe function?

Looking forward to your thoughts!

Copy link
Collaborator

@rafalp rafalp left a comment

Choose a reason for hiding this comment

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

Please cleanup imports in modules, make magic methods of your classes on top of those classes bodies (currently they are mixed with rest of logic) and then group both class definitions and their methods in order of their usage in business logic.

Also move your changes to a dedicated handler importable from ariadne.contrib.sse

List,
Union,
)
from typing import Type
Copy link
Collaborator

Choose a reason for hiding this comment

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

Imports will need cleaning up

create_task_group,
)
from graphql import DocumentNode
from graphql import MiddlewareManager
Copy link
Collaborator

Choose a reason for hiding this comment

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

Ditto

@rafalp
Copy link
Collaborator

rafalp commented Dec 10, 2024

could you advise on the best option for passing configuration?

This work needs to be split into a separate handler, then it should accept configuration options as __init__ kwargs, like how other handlers do it.

could you also suggest a more elegant solution for passing GraphQL errors from the subscribe function?

I am afraid I can't. This is also finnicky in standard impl we are using for regular subscriptions.

@danplischke
Copy link
Author

danplischke commented Dec 12, 2024

Thanks @rafalp and @mociepka for your feedback!

I have

  1. moved the implementation into contrib/sse.py
  2. added config options through init
  3. fixed the imports and the layout of the functions in the classes

Just fyi: I had to update the versions in the requirements.txt in tests_integrations/fastapi (using pip compile) due to an error in the TestClient in the old starlette version which was used in the workflows. Afterwards, I had to manually set the anyio version lower for the Python 3.8 workflow to run through. Feel free to change this.

@rafalp concerning your question about the line terminator as separator:
My understanding is that json encoding does not actually use the control characters \n (ascii 10) \r (ascii 13) in strings but the representation for \ + n and \ + r respectively. If it was only text that would be send, that would be an issue. But since all messages are json encoded, everything is going to stay on one line.

import json
v = json.dumps("valu\n\n\r\ne")
print([ord(b) for b in v])
print([ord(b) for b in "valu\n\n\r\ne"])

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants