Repository: strawberry-graphql/strawberry

Issue:
Query and Mutation not accepted over protocol `graphql-protocol-ws`
Attempting to issue a `query` over the websockets protocol results in the socket being closed by the server with error 4400.
However, the `websockets-transport-ws` protocol specifically allows for queries and mutations, **in addition to** subscriptions: https://github.com/enisdenjo/graphql-ws/blob/master/PROTOCOL.md#query-and-mutation-operations-without-streaming-directives

It would be useful to have these supported, so that a client need only keep a single websocket connection open and not have to have a separate HTTP pathway for queries and mutations.


Discussion:

Resolving patch:
diff --git a/strawberry/subscriptions/protocols/graphql_transport_ws/handlers.py b/strawberry/subscriptions/protocols/graphql_transport_ws/handlers.py
--- a/strawberry/subscriptions/protocols/graphql_transport_ws/handlers.py
+++ b/strawberry/subscriptions/protocols/graphql_transport_ws/handlers.py
@@ -4,7 +4,13 @@
 from datetime import timedelta
 from typing import Any, AsyncGenerator, Dict, Optional
 
-from graphql import ExecutionResult as GraphQLExecutionResult, GraphQLError
+from graphql import (
+    ExecutionResult as GraphQLExecutionResult,
+    GraphQLError,
+    GraphQLSyntaxError,
+    OperationType,
+    parse,
+)
 from graphql.error.graphql_error import format_error as format_graphql_error
 
 from strawberry.schema import BaseSchema
@@ -21,6 +27,7 @@
     SubscribeMessagePayload,
 )
 from strawberry.utils.debug import pretty_print_graphql_operation
+from strawberry.utils.operation import get_operation_type
 
 
 class BaseGraphQLTransportWSHandler(ABC):
@@ -125,14 +132,26 @@ async def handle_subscribe(self, message: SubscribeMessage) -> None:
             await self.close(code=4401, reason="Unauthorized")
             return
 
-        if message.id in self.subscriptions.keys():
-            reason = f"Subscriber for {message.id} already exists"
-            await self.close(code=4409, reason=reason)
+        try:
+            graphql_document = parse(message.payload.query)
+        except GraphQLSyntaxError as exc:
+            await self.close(code=4400, reason=exc.message)
             return
 
-        context = await self.get_context()
-        root_value = await self.get_root_value()
+        try:
+            operation_type = get_operation_type(
+                graphql_document, message.payload.operationName
+            )
+        except RuntimeError:
+            await self.close(code=4400, reason="Can't get GraphQL operation type")
+            return
+
+        if operation_type == OperationType.SUBSCRIPTION:
+            return await self.handle_streaming_operation(message)
+        else:
+            return await self.handle_single_result_operation(message)
 
+    async def handle_single_result_operation(self, message: SubscribeMessage):
         if self.debug:  # pragma: no cover
             pretty_print_graphql_operation(
                 message.payload.operationName,
@@ -140,20 +159,52 @@ async def handle_subscribe(self, message: SubscribeMessage) -> None:
                 message.payload.variables,
             )
 
-        try:
-            result_source = await self.schema.subscribe(
-                query=message.payload.query,
-                variable_values=message.payload.variables,
-                operation_name=message.payload.operationName,
-                context_value=context,
-                root_value=root_value,
-            )
-        except GraphQLError as error:
-            payload = [format_graphql_error(error)]
+        context = await self.get_context()
+        root_value = await self.get_root_value()
+
+        result = await self.schema.execute(
+            query=message.payload.query,
+            variable_values=message.payload.variables,
+            context_value=context,
+            root_value=root_value,
+            operation_name=message.payload.operationName,
+        )
+
+        if result.errors:
+            payload = [format_graphql_error(result.errors[0])]
             await self.send_message(ErrorMessage(id=message.id, payload=payload))
-            self.schema.process_errors([error])
+            self.schema.process_errors(result.errors)
             return
 
+        await self.send_message(
+            NextMessage(id=message.id, payload={"data": result.data})
+        )
+        await self.send_message(CompleteMessage(id=message.id))
+
+    async def handle_streaming_operation(self, message: SubscribeMessage) -> None:
+        if message.id in self.subscriptions.keys():
+            reason = f"Subscriber for {message.id} already exists"
+            await self.close(code=4409, reason=reason)
+            return
+
+        if self.debug:  # pragma: no cover
+            pretty_print_graphql_operation(
+                message.payload.operationName,
+                message.payload.query,
+                message.payload.variables,
+            )
+
+        context = await self.get_context()
+        root_value = await self.get_root_value()
+
+        result_source = await self.schema.subscribe(
+            query=message.payload.query,
+            variable_values=message.payload.variables,
+            operation_name=message.payload.operationName,
+            context_value=context,
+            root_value=root_value,
+        )
+
         if isinstance(result_source, GraphQLExecutionResult):
             assert result_source.errors
             payload = [format_graphql_error(result_source.errors[0])]
diff --git a/strawberry/types/execution.py b/strawberry/types/execution.py
--- a/strawberry/types/execution.py
+++ b/strawberry/types/execution.py
@@ -11,12 +11,14 @@
 from graphql.error.graphql_error import GraphQLError
 from graphql.language import DocumentNode, OperationDefinitionNode
 
+from strawberry.utils.operation import get_first_operation, get_operation_type
+
 
 if TYPE_CHECKING:
     from strawberry.schema import Schema
 
 
-GraphqlOperationTypes = Literal["QUERY", "MUTATION"]
+GraphqlOperationTypes = Literal["QUERY", "MUTATION", "SUBSCRIPTION"]
 
 
 @dataclasses.dataclass
@@ -58,40 +60,20 @@ def operation_name(self) -> Optional[str]:
 
     @property
     def operation_type(self) -> GraphqlOperationTypes:
-        definition: Optional[OperationDefinitionNode] = None
-
         graphql_document = self.graphql_document
         if not graphql_document:
             raise RuntimeError("No GraphQL document available")
 
-        # If no operation_name has been specified then use the first
-        # OperationDefinitionNode
-        if not self._provided_operation_name:
-            definition = self._get_first_operation()
-        else:
-            for d in graphql_document.definitions:
-                d = cast(OperationDefinitionNode, d)
-                if d.name and d.name.value == self._provided_operation_name:
-                    definition = d
-                    break
+        operation_type = get_operation_type(graphql_document, self.operation_name)
 
-        if not definition:
-            raise RuntimeError("Can't get GraphQL operation type")
-
-        return cast(GraphqlOperationTypes, definition.operation.name)
+        return cast(GraphqlOperationTypes, operation_type.name)
 
     def _get_first_operation(self) -> Optional[OperationDefinitionNode]:
         graphql_document = self.graphql_document
         if not graphql_document:
             return None
 
-        definition: Optional[OperationDefinitionNode] = None
-        for d in graphql_document.definitions:
-            if isinstance(d, OperationDefinitionNode):
-                definition = d
-                break
-
-        return definition
+        return get_first_operation(graphql_document)
 
 
 @dataclasses.dataclass
diff --git a/strawberry/utils/operation.py b/strawberry/utils/operation.py
new file mode 100644
--- /dev/null
+++ b/strawberry/utils/operation.py
@@ -0,0 +1,33 @@
+from typing import Optional, cast
+
+from graphql.language import DocumentNode, OperationDefinitionNode, OperationType
+
+
+def get_first_operation(
+    graphql_document: DocumentNode,
+) -> Optional[OperationDefinitionNode]:
+    for definition in graphql_document.definitions:
+        if isinstance(definition, OperationDefinitionNode):
+            return definition
+
+    return None
+
+
+def get_operation_type(
+    graphql_document: DocumentNode, operation_name: Optional[str] = None
+) -> OperationType:
+    definition: Optional[OperationDefinitionNode] = None
+
+    if operation_name:
+        for d in graphql_document.definitions:
+            d = cast(OperationDefinitionNode, d)
+            if d.name and d.name.value == operation_name:
+                definition = d
+                break
+    else:
+        definition = get_first_operation(graphql_document)
+
+    if not definition:
+        raise RuntimeError("Can't get GraphQL operation type")
+
+    return definition.operation


Test patch:
diff --git a/tests/aiohttp/schema.py b/tests/aiohttp/schema.py
--- a/tests/aiohttp/schema.py
+++ b/tests/aiohttp/schema.py
@@ -6,9 +6,17 @@
 
 import strawberry
 from strawberry.file_uploads import Upload
+from strawberry.permission import BasePermission
 from strawberry.subscriptions.protocols.graphql_transport_ws.types import PingMessage
 
 
+class AlwaysFailPermission(BasePermission):
+    message = "You are not authorized"
+
+    def has_permission(self, source, info, **kwargs) -> bool:
+        return False
+
+
 @strawberry.enum
 class Flavor(Enum):
     VANILLA = "vanilla"
@@ -29,11 +37,21 @@ class DebugInfo:
 
 @strawberry.type
 class Query:
-    hello: str = "strawberry"
+    @strawberry.field
+    def hello(self, name: typing.Optional[str] = None) -> str:
+        return f"Hello {name or 'world'}"
+
+    @strawberry.field(permission_classes=[AlwaysFailPermission])
+    def always_fail(self) -> typing.Optional[str]:
+        return "Hey"
 
 
 @strawberry.type
 class Mutation:
+    @strawberry.mutation
+    def hello(self) -> str:
+        return "strawberry"
+
     @strawberry.mutation
     def read_text(self, text_file: Upload) -> str:
         return text_file.read().decode()
diff --git a/tests/aiohttp/test_graphql_transport_ws.py b/tests/aiohttp/test_graphql_transport_ws.py
--- a/tests/aiohttp/test_graphql_transport_ws.py
+++ b/tests/aiohttp/test_graphql_transport_ws.py
@@ -337,19 +337,10 @@ async def test_subscription_syntax_error(aiohttp_client):
             ).as_dict()
         )
 
-        response = await ws.receive_json()
-        assert response["type"] == ErrorMessage.type
-        assert response["id"] == "sub1"
-        assert len(response["payload"]) == 1
-        assert response["payload"][0].get("path") is None
-        assert response["payload"][0]["locations"] == [{"line": 1, "column": 31}]
-        assert (
-            response["payload"][0]["message"]
-            == "Syntax Error: Expected Name, found <EOF>."
-        )
-
-        await ws.close()
+        data = await ws.receive(timeout=2)
         assert ws.closed
+        assert ws.close_code == 4400
+        assert data.extra == "Syntax Error: Expected Name, found <EOF>."
 
 
 async def test_subscription_field_errors(aiohttp_client):
@@ -518,3 +509,176 @@ async def test_subscription_exceptions(aiohttp_client):
 
         await ws.close()
         assert ws.closed
+
+
+async def test_single_result_query_operation(aiohttp_client):
+    app = create_app()
+    aiohttp_app_client = await aiohttp_client(app)
+
+    async with aiohttp_app_client.ws_connect(
+        "/graphql", protocols=[GRAPHQL_TRANSPORT_WS_PROTOCOL]
+    ) as ws:
+        await ws.send_json(ConnectionInitMessage().as_dict())
+
+        response = await ws.receive_json()
+        assert response == ConnectionAckMessage().as_dict()
+
+        await ws.send_json(
+            SubscribeMessage(
+                id="sub1",
+                payload=SubscribeMessagePayload(query="query { hello }"),
+            ).as_dict()
+        )
+
+        response = await ws.receive_json()
+        assert (
+            response
+            == NextMessage(
+                id="sub1", payload={"data": {"hello": "Hello world"}}
+            ).as_dict()
+        )
+
+        response = await ws.receive_json()
+        assert response == CompleteMessage(id="sub1").as_dict()
+
+        await ws.close()
+        assert ws.closed
+
+
+async def test_single_result_mutation_operation(aiohttp_client):
+    app = create_app()
+    aiohttp_app_client = await aiohttp_client(app)
+
+    async with aiohttp_app_client.ws_connect(
+        "/graphql", protocols=[GRAPHQL_TRANSPORT_WS_PROTOCOL]
+    ) as ws:
+        await ws.send_json(ConnectionInitMessage().as_dict())
+
+        response = await ws.receive_json()
+        assert response == ConnectionAckMessage().as_dict()
+
+        await ws.send_json(
+            SubscribeMessage(
+                id="sub1",
+                payload=SubscribeMessagePayload(query="mutation { hello }"),
+            ).as_dict()
+        )
+
+        response = await ws.receive_json()
+        assert (
+            response
+            == NextMessage(
+                id="sub1", payload={"data": {"hello": "strawberry"}}
+            ).as_dict()
+        )
+
+        response = await ws.receive_json()
+        assert response == CompleteMessage(id="sub1").as_dict()
+
+        await ws.close()
+        assert ws.closed
+
+
+async def test_single_result_operation_selection(aiohttp_client):
+    app = create_app()
+    aiohttp_app_client = await aiohttp_client(app)
+
+    async with aiohttp_app_client.ws_connect(
+        "/graphql", protocols=[GRAPHQL_TRANSPORT_WS_PROTOCOL]
+    ) as ws:
+        await ws.send_json(ConnectionInitMessage().as_dict())
+
+        response = await ws.receive_json()
+        assert response == ConnectionAckMessage().as_dict()
+
+        query = """
+            query Query1 {
+                hello
+            }
+            query Query2 {
+                hello(name: "Strawberry")
+            }
+        """
+
+        await ws.send_json(
+            SubscribeMessage(
+                id="sub1",
+                payload=SubscribeMessagePayload(query=query, operationName="Query2"),
+            ).as_dict()
+        )
+
+        response = await ws.receive_json()
+        assert (
+            response
+            == NextMessage(
+                id="sub1", payload={"data": {"hello": "Hello Strawberry"}}
+            ).as_dict()
+        )
+
+        response = await ws.receive_json()
+        assert response == CompleteMessage(id="sub1").as_dict()
+
+        await ws.close()
+        assert ws.closed
+
+
+async def test_single_result_invalid_operation_selection(aiohttp_client):
+    app = create_app()
+    aiohttp_app_client = await aiohttp_client(app)
+
+    async with aiohttp_app_client.ws_connect(
+        "/graphql", protocols=[GRAPHQL_TRANSPORT_WS_PROTOCOL]
+    ) as ws:
+        await ws.send_json(ConnectionInitMessage().as_dict())
+
+        response = await ws.receive_json()
+        assert response == ConnectionAckMessage().as_dict()
+
+        query = """
+            query Query1 {
+                hello
+            }
+        """
+
+        await ws.send_json(
+            SubscribeMessage(
+                id="sub1",
+                payload=SubscribeMessagePayload(query=query, operationName="Query2"),
+            ).as_dict()
+        )
+
+        data = await ws.receive(timeout=2)
+        assert ws.closed
+        assert ws.close_code == 4400
+        assert data.extra == "Can't get GraphQL operation type"
+
+
+async def test_single_result_operation_error(aiohttp_client):
+    app = create_app()
+    aiohttp_app_client = await aiohttp_client(app)
+
+    async with aiohttp_app_client.ws_connect(
+        "/graphql", protocols=[GRAPHQL_TRANSPORT_WS_PROTOCOL]
+    ) as ws:
+        await ws.send_json(ConnectionInitMessage().as_dict())
+
+        response = await ws.receive_json()
+        assert response == ConnectionAckMessage().as_dict()
+
+        await ws.send_json(
+            SubscribeMessage(
+                id="sub1",
+                payload=SubscribeMessagePayload(
+                    query="query { alwaysFail }",
+                ),
+            ).as_dict()
+        )
+
+        response = await ws.receive_json()
+        assert response["type"] == ErrorMessage.type
+        assert response["id"] == "sub1"
+        assert len(response["payload"]) == 1
+        assert response["payload"][0]["message"] == "You are not authorized"
+
+        await ws.close()
+        assert ws.closed
diff --git a/tests/aiohttp/test_http.py b/tests/aiohttp/test_http.py
--- a/tests/aiohttp/test_http.py
+++ b/tests/aiohttp/test_http.py
@@ -16,7 +16,7 @@ async def test_graphql_query(aiohttp_app_client):
     response = await aiohttp_app_client.post("/graphql", json=query)
     data = await response.json()
     assert response.status == 200
-    assert data["data"]["hello"] == "strawberry"
+    assert data["data"]["hello"] == "Hello world"
 
 
 async def test_custom_context(aiohttp_client):
diff --git a/tests/asgi/schema.py b/tests/asgi/schema.py
--- a/tests/asgi/schema.py
+++ b/tests/asgi/schema.py
@@ -54,6 +54,10 @@ def root_name(root) -> str:
 
 @strawberry.type
 class Mutation:
+    @strawberry.mutation
+    async def hello(self) -> str:
+        return "strawberry"
+
     @strawberry.mutation
     async def read_text(self, text_file: Upload) -> str:
         return (await text_file.read()).decode()
diff --git a/tests/asgi/test_graphql_transport_ws.py b/tests/asgi/test_graphql_transport_ws.py
--- a/tests/asgi/test_graphql_transport_ws.py
+++ b/tests/asgi/test_graphql_transport_ws.py
@@ -261,18 +261,9 @@ def test_subscription_syntax_error(test_client):
             ).as_dict()
         )
 
-        response = ws.receive_json()
-        assert response["type"] == ErrorMessage.type
-        assert response["id"] == "sub1"
-        assert len(response["payload"]) == 1
-        assert response["payload"][0].get("path") is None
-        assert response["payload"][0]["locations"] == [{"line": 1, "column": 31}]
-        assert (
-            response["payload"][0]["message"]
-            == "Syntax Error: Expected Name, found <EOF>."
-        )
-
-        ws.close()
+        data = ws.receive()
+        assert data["type"] == "websocket.close"
+        assert data["code"] == 4400
 
 
 def test_subscription_field_errors(test_client):
@@ -417,3 +408,148 @@ def test_subscription_exceptions(test_client):
         assert response["payload"][0]["message"] == "TEST EXC"
 
         ws.close()
+
+
+def test_single_result_query_operation(test_client):
+    with test_client.websocket_connect(
+        "/graphql", [GRAPHQL_TRANSPORT_WS_PROTOCOL]
+    ) as ws:
+        ws.send_json(ConnectionInitMessage().as_dict())
+
+        response = ws.receive_json()
+        assert response == ConnectionAckMessage().as_dict()
+
+        ws.send_json(
+            SubscribeMessage(
+                id="sub1",
+                payload=SubscribeMessagePayload(query="query { hello }"),
+            ).as_dict()
+        )
+
+        response = ws.receive_json()
+        assert (
+            response
+            == NextMessage(
+                id="sub1", payload={"data": {"hello": "Hello world"}}
+            ).as_dict()
+        )
+
+        response = ws.receive_json()
+        assert response == CompleteMessage(id="sub1").as_dict()
+
+
+def test_single_result_mutation_operation(test_client):
+    with test_client.websocket_connect(
+        "/graphql", [GRAPHQL_TRANSPORT_WS_PROTOCOL]
+    ) as ws:
+        ws.send_json(ConnectionInitMessage().as_dict())
+
+        response = ws.receive_json()
+        assert response == ConnectionAckMessage().as_dict()
+
+        ws.send_json(
+            SubscribeMessage(
+                id="sub1",
+                payload=SubscribeMessagePayload(query="mutation { hello }"),
+            ).as_dict()
+        )
+
+        response = ws.receive_json()
+        assert (
+            response
+            == NextMessage(
+                id="sub1", payload={"data": {"hello": "strawberry"}}
+            ).as_dict()
+        )
+
+        response = ws.receive_json()
+        assert response == CompleteMessage(id="sub1").as_dict()
+
+
+def test_single_result_operation_selection(test_client):
+    with test_client.websocket_connect(
+        "/graphql", [GRAPHQL_TRANSPORT_WS_PROTOCOL]
+    ) as ws:
+        ws.send_json(ConnectionInitMessage().as_dict())
+
+        response = ws.receive_json()
+        assert response == ConnectionAckMessage().as_dict()
+
+        query = """
+            query Query1 {
+                hello
+            }
+            query Query2 {
+                hello(name: "Strawberry")
+            }
+        """
+
+        ws.send_json(
+            SubscribeMessage(
+                id="sub1",
+                payload=SubscribeMessagePayload(query=query, operationName="Query2"),
+            ).as_dict()
+        )
+
+        response = ws.receive_json()
+        assert (
+            response
+            == NextMessage(
+                id="sub1", payload={"data": {"hello": "Hello Strawberry"}}
+            ).as_dict()
+        )
+
+        response = ws.receive_json()
+        assert response == CompleteMessage(id="sub1").as_dict()
+
+
+def test_single_result_invalid_operation_selection(test_client):
+    with test_client.websocket_connect(
+        "/graphql", [GRAPHQL_TRANSPORT_WS_PROTOCOL]
+    ) as ws:
+        ws.send_json(ConnectionInitMessage().as_dict())
+
+        response = ws.receive_json()
+        assert response == ConnectionAckMessage().as_dict()
+
+        query = """
+            query Query1 {
+                hello
+            }
+        """
+
+        ws.send_json(
+            SubscribeMessage(
+                id="sub1",
+                payload=SubscribeMessagePayload(query=query, operationName="Query2"),
+            ).as_dict()
+        )
+
+        data = ws.receive()
+        assert data["type"] == "websocket.close"
+        assert data["code"] == 4400
+
+
+def test_single_result_operation_error(test_client):
+    with test_client.websocket_connect(
+        "/graphql", [GRAPHQL_TRANSPORT_WS_PROTOCOL]
+    ) as ws:
+        ws.send_json(ConnectionInitMessage().as_dict())
+
+        response = ws.receive_json()
+        assert response == ConnectionAckMessage().as_dict()
+
+        ws.send_json(
+            SubscribeMessage(
+                id="sub1",
+                payload=SubscribeMessagePayload(
+                    query="query { alwaysFail }",
+                ),
+            ).as_dict()
+        )
+
+        response = ws.receive_json()
+        assert response["type"] == ErrorMessage.type
+        assert response["id"] == "sub1"
+        assert len(response["payload"]) == 1
+        assert response["payload"][0]["message"] == "You are not authorized"
diff --git a/tests/fastapi/schema.py b/tests/fastapi/schema.py
--- a/tests/fastapi/schema.py
+++ b/tests/fastapi/schema.py
@@ -54,6 +54,10 @@ def root_name(root) -> str:
 
 @strawberry.type
 class Mutation:
+    @strawberry.mutation
+    async def hello(self) -> str:
+        return "strawberry"
+
     @strawberry.mutation
     async def read_text(self, text_file: Upload) -> str:
         return (await text_file.read()).decode()
diff --git a/tests/fastapi/test_graphql_transport_ws.py b/tests/fastapi/test_graphql_transport_ws.py
--- a/tests/fastapi/test_graphql_transport_ws.py
+++ b/tests/fastapi/test_graphql_transport_ws.py
@@ -289,18 +289,9 @@ def test_subscription_syntax_error(test_client):
             ).as_dict()
         )
 
-        response = ws.receive_json()
-        assert response["type"] == ErrorMessage.type
-        assert response["id"] == "sub1"
-        assert len(response["payload"]) == 1
-        assert response["payload"][0].get("path") is None
-        assert response["payload"][0]["locations"] == [{"line": 1, "column": 31}]
-        assert (
-            response["payload"][0]["message"]
-            == "Syntax Error: Expected Name, found <EOF>."
-        )
-
-        ws.close()
+        data = ws.receive()
+        assert data["type"] == "websocket.close"
+        assert data["code"] == 4400
 
 
 def test_subscription_field_errors(test_client):
@@ -453,3 +444,148 @@ def test_subscription_exceptions(test_client):
         assert response["payload"][0]["message"] == "TEST EXC"
 
         ws.close()
+
+
+def test_single_result_query_operation(test_client):
+    with test_client.websocket_connect(
+        "/graphql", [GRAPHQL_TRANSPORT_WS_PROTOCOL]
+    ) as ws:
+        ws.send_json(ConnectionInitMessage().as_dict())
+
+        response = ws.receive_json()
+        assert response == ConnectionAckMessage().as_dict()
+
+        ws.send_json(
+            SubscribeMessage(
+                id="sub1",
+                payload=SubscribeMessagePayload(query="query { hello }"),
+            ).as_dict()
+        )
+
+        response = ws.receive_json()
+        assert (
+            response
+            == NextMessage(
+                id="sub1", payload={"data": {"hello": "Hello world"}}
+            ).as_dict()
+        )
+
+        response = ws.receive_json()
+        assert response == CompleteMessage(id="sub1").as_dict()
+
+
+def test_single_result_mutation_operation(test_client):
+    with test_client.websocket_connect(
+        "/graphql", [GRAPHQL_TRANSPORT_WS_PROTOCOL]
+    ) as ws:
+        ws.send_json(ConnectionInitMessage().as_dict())
+
+        response = ws.receive_json()
+        assert response == ConnectionAckMessage().as_dict()
+
+        ws.send_json(
+            SubscribeMessage(
+                id="sub1",
+                payload=SubscribeMessagePayload(query="mutation { hello }"),
+            ).as_dict()
+        )
+
+        response = ws.receive_json()
+        assert (
+            response
+            == NextMessage(
+                id="sub1", payload={"data": {"hello": "strawberry"}}
+            ).as_dict()
+        )
+
+        response = ws.receive_json()
+        assert response == CompleteMessage(id="sub1").as_dict()
+
+
+def test_single_result_operation_selection(test_client):
+    with test_client.websocket_connect(
+        "/graphql", [GRAPHQL_TRANSPORT_WS_PROTOCOL]
+    ) as ws:
+        ws.send_json(ConnectionInitMessage().as_dict())
+
+        response = ws.receive_json()
+        assert response == ConnectionAckMessage().as_dict()
+
+        query = """
+            query Query1 {
+                hello
+            }
+            query Query2 {
+                hello(name: "Strawberry")
+            }
+        """
+
+        ws.send_json(
+            SubscribeMessage(
+                id="sub1",
+                payload=SubscribeMessagePayload(query=query, operationName="Query2"),
+            ).as_dict()
+        )
+
+        response = ws.receive_json()
+        assert (
+            response
+            == NextMessage(
+                id="sub1", payload={"data": {"hello": "Hello Strawberry"}}
+            ).as_dict()
+        )
+
+        response = ws.receive_json()
+        assert response == CompleteMessage(id="sub1").as_dict()
+
+
+def test_single_result_invalid_operation_selection(test_client):
+    with test_client.websocket_connect(
+        "/graphql", [GRAPHQL_TRANSPORT_WS_PROTOCOL]
+    ) as ws:
+        ws.send_json(ConnectionInitMessage().as_dict())
+
+        response = ws.receive_json()
+        assert response == ConnectionAckMessage().as_dict()
+
+        query = """
+            query Query1 {
+                hello
+            }
+        """
+
+        ws.send_json(
+            SubscribeMessage(
+                id="sub1",
+                payload=SubscribeMessagePayload(query=query, operationName="Query2"),
+            ).as_dict()
+        )
+
+        data = ws.receive()
+        assert data["type"] == "websocket.close"
+        assert data["code"] == 4400
+
+
+def test_single_result_operation_error(test_client):
+    with test_client.websocket_connect(
+        "/graphql", [GRAPHQL_TRANSPORT_WS_PROTOCOL]
+    ) as ws:
+        ws.send_json(ConnectionInitMessage().as_dict())
+
+        response = ws.receive_json()
+        assert response == ConnectionAckMessage().as_dict()
+
+        ws.send_json(
+            SubscribeMessage(
+                id="sub1",
+                payload=SubscribeMessagePayload(
+                    query="query { alwaysFail }",
+                ),
+            ).as_dict()
+        )
+
+        response = ws.receive_json()
+        assert response["type"] == ErrorMessage.type
+        assert response["id"] == "sub1"
+        assert len(response["payload"]) == 1
+        assert response["payload"][0]["message"] == "You are not authorized"
diff --git a/tests/utils/test_get_first_operation.py b/tests/utils/test_get_first_operation.py
new file mode 100644
--- /dev/null
+++ b/tests/utils/test_get_first_operation.py
@@ -0,0 +1,41 @@
+from graphql import OperationType, parse
+
+from strawberry.utils.operation import get_first_operation
+
+
+def test_document_without_operation_definition_notes():
+    document = parse(
+        """
+        fragment Test on Query {
+            hello
+        }
+    """
+    )
+    assert get_first_operation(document) is None
+
+
+def test_single_operation_definition_note():
+    document = parse(
+        """
+        query Operation1 {
+            hello
+        }
+    """
+    )
+    assert get_first_operation(document) is not None
+    assert get_first_operation(document).operation == OperationType.QUERY
+
+
+def test_multiple_operation_definition_notes():
+    document = parse(
+        """
+        mutation Operation1 {
+            hello
+        }
+        query Operation2 {
+            hello
+        }
+    """
+    )
+    assert get_first_operation(document) is not None
+    assert get_first_operation(document).operation == OperationType.MUTATION