strawberry-graphql 0.270.0__py3-none-any.whl → 0.270.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -372,26 +372,71 @@ class AsyncBaseHTTPView(
372
372
  )
373
373
 
374
374
  def _stream_with_heartbeat(
375
- self, stream: Callable[[], AsyncGenerator[str, None]]
375
+ self, stream: Callable[[], AsyncGenerator[str, None]], separator: str
376
376
  ) -> Callable[[], AsyncGenerator[str, None]]:
377
- """Adds a heartbeat to the stream, to prevent the connection from closing when there are no messages being sent."""
378
- queue: asyncio.Queue[tuple[bool, Any]] = asyncio.Queue(1)
379
-
377
+ """Add heartbeat messages to a GraphQL stream to prevent connection timeouts.
378
+
379
+ This method wraps an async stream generator with heartbeat functionality by:
380
+ 1. Creating a queue to coordinate between data and heartbeat messages
381
+ 2. Running two concurrent tasks: one for original stream data, one for heartbeats
382
+ 3. Merging both message types into a single output stream
383
+
384
+ Messages in the queue are tuples of (raised, done, data) where:
385
+ - raised (bool): True if this contains an exception to be re-raised
386
+ - done (bool): True if this is the final signal indicating stream completion
387
+ - data: The actual message content to yield, or exception if raised=True
388
+ Note: data is always None when done=True and can be ignored
389
+
390
+ Note: This implementation addresses two critical concerns:
391
+
392
+ 1. Race condition: There's a potential race between checking task.done() and
393
+ processing the final message. We solve this by having the drain task send
394
+ an explicit (False, True, None) completion signal as its final action.
395
+ Without this signal, we might exit before processing the final boundary.
396
+
397
+ Since the queue size is 1 and the drain task will only complete after
398
+ successfully queueing the done signal, task.done() guarantees the done
399
+ signal is either in the queue or has already been processed. This ensures
400
+ we never miss the final boundary.
401
+
402
+ 2. Flow control: The queue has maxsize=1, which is essential because:
403
+ - It provides natural backpressure between producers and consumer
404
+ - Prevents heartbeat messages from accumulating when drain is active
405
+ - Ensures proper task coordination without complex synchronization
406
+ - Guarantees the done signal is queued before drain task completes
407
+
408
+ Heartbeats are sent every 5 seconds when the drain task isn't sending data.
409
+
410
+ Note: Due to the asynchronous nature of the heartbeat task, an extra heartbeat
411
+ message may be sent after the final stream boundary message. This is safe because
412
+ both the MIME specification (RFC 2046) and Apollo's GraphQL Multipart HTTP protocol
413
+ require clients to ignore any content after the final boundary marker. Additionally,
414
+ Apollo's protocol defines heartbeats as empty JSON objects that clients must
415
+ silently ignore.
416
+ """
417
+ queue: asyncio.Queue[tuple[bool, bool, Any]] = asyncio.Queue(
418
+ maxsize=1, # Critical: maxsize=1 for flow control.
419
+ )
380
420
  cancelling = False
381
421
 
382
422
  async def drain() -> None:
383
423
  try:
384
424
  async for item in stream():
385
- await queue.put((False, item))
425
+ await queue.put((False, False, item))
386
426
  except Exception as e:
387
427
  if not cancelling:
388
- await queue.put((True, e))
428
+ await queue.put((True, False, e))
389
429
  else:
390
430
  raise
431
+ # Send completion signal to prevent race conditions. The queue.put()
432
+ # blocks until space is available (due to maxsize=1), guaranteeing that
433
+ # when task.done() is True, the final stream message has been dequeued.
434
+ await queue.put((False, True, None)) # Always use None with done=True
391
435
 
392
436
  async def heartbeat() -> None:
393
437
  while True:
394
- await queue.put((False, self.encode_multipart_data({}, "graphql")))
438
+ item = self.encode_multipart_data({}, separator)
439
+ await queue.put((False, False, item))
395
440
 
396
441
  await asyncio.sleep(5)
397
442
 
@@ -413,8 +458,19 @@ class AsyncBaseHTTPView(
413
458
  await heartbeat_task
414
459
 
415
460
  try:
461
+ # When task.done() is True, the final stream message has been
462
+ # dequeued due to queue size 1 and the blocking nature of queue.put().
416
463
  while not task.done():
417
- raised, data = await queue.get()
464
+ raised, done, data = await queue.get()
465
+
466
+ if done:
467
+ # Received done signal (data is None), stream is complete.
468
+ # Note that we may not get here because of the race between
469
+ # task.done() and queue.get(), but that's OK because if
470
+ # task.done() is True, the actual final message (including any
471
+ # exception) has been consumed. The only intent here is to
472
+ # ensure that data=None is not yielded.
473
+ break
418
474
 
419
475
  if raised:
420
476
  await cancel_tasks()
@@ -439,7 +495,7 @@ class AsyncBaseHTTPView(
439
495
 
440
496
  yield f"\r\n--{separator}--\r\n"
441
497
 
442
- return self._stream_with_heartbeat(stream)
498
+ return self._stream_with_heartbeat(stream, separator)
443
499
 
444
500
  async def parse_multipart_subscriptions(
445
501
  self, request: AsyncHTTPRequestAdapter
@@ -1,6 +1,10 @@
1
1
  from strawberry.types.graphql import OperationType
2
2
 
3
3
 
4
+ class CannotGetOperationTypeError(Exception):
5
+ """Internal error raised when we cannot get the operation type from a GraphQL document."""
6
+
7
+
4
8
  class InvalidOperationTypeError(Exception):
5
9
  def __init__(self, operation_type: OperationType) -> None:
6
10
  self.operation_type = operation_type
@@ -15,4 +19,7 @@ class InvalidOperationTypeError(Exception):
15
19
  return f"{operation_type} are not allowed when using {method}"
16
20
 
17
21
 
18
- __all__ = ["InvalidOperationTypeError"]
22
+ __all__ = [
23
+ "CannotGetOperationTypeError",
24
+ "InvalidOperationTypeError",
25
+ ]
@@ -71,7 +71,7 @@ from strawberry.utils.await_maybe import await_maybe
71
71
  from . import compat
72
72
  from .base import BaseSchema
73
73
  from .config import StrawberryConfig
74
- from .exceptions import InvalidOperationTypeError
74
+ from .exceptions import CannotGetOperationTypeError, InvalidOperationTypeError
75
75
 
76
76
  if TYPE_CHECKING:
77
77
  from collections.abc import Iterable, Mapping
@@ -495,8 +495,13 @@ class Schema(BaseSchema):
495
495
  context.errors = [error]
496
496
  return PreExecutionError(data=None, errors=[error])
497
497
 
498
- if context.operation_type not in context.allowed_operations:
499
- raise InvalidOperationTypeError(context.operation_type)
498
+ try:
499
+ operation_type = context.operation_type
500
+ except RuntimeError as error:
501
+ raise CannotGetOperationTypeError from error
502
+
503
+ if operation_type not in context.allowed_operations:
504
+ raise InvalidOperationTypeError(operation_type)
500
505
 
501
506
  async with extensions_runner.validation():
502
507
  _run_validation(context)
@@ -14,6 +14,7 @@ from typing import (
14
14
  from strawberry.exceptions import ConnectionRejectionError
15
15
  from strawberry.http.exceptions import NonTextMessageReceived, WebSocketDisconnected
16
16
  from strawberry.http.typevars import Context, RootValue
17
+ from strawberry.schema.exceptions import CannotGetOperationTypeError
17
18
  from strawberry.subscriptions.protocols.graphql_ws.types import (
18
19
  CompleteMessage,
19
20
  ConnectionInitMessage,
@@ -193,6 +194,14 @@ class BaseGraphQLWSHandler(Generic[Context, RootValue]):
193
194
 
194
195
  await self.send_message(CompleteMessage(type="complete", id=operation_id))
195
196
 
197
+ except CannotGetOperationTypeError:
198
+ await self.send_message(
199
+ ErrorMessage(
200
+ type="error",
201
+ id=operation_id,
202
+ payload={"message": f'Unknown operation named "{operation_name}".'},
203
+ )
204
+ )
196
205
  except asyncio.CancelledError:
197
206
  await self.send_message(CompleteMessage(type="complete", id=operation_id))
198
207
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: strawberry-graphql
3
- Version: 0.270.0
3
+ Version: 0.270.2
4
4
  Summary: A library for creating GraphQL APIs
5
5
  License: MIT
6
6
  Keywords: graphql,api,rest,starlette,async
@@ -131,7 +131,7 @@ strawberry/file_uploads/utils.py,sha256=-c6TbqUI-Dkb96hWCrZabh6TL2OabBuQNkCarOqg
131
131
  strawberry/flask/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
132
132
  strawberry/flask/views.py,sha256=MCvAsNgTZLU8RvTYKWfnLU2w7Wv1ZZpxW9W3TyTZuPY,6355
133
133
  strawberry/http/__init__.py,sha256=ytAirKk7K7D5knY21tpCGeZ-sCPgwMsijL5AxmOy-94,1163
134
- strawberry/http/async_base_view.py,sha256=NnFYHy_3b6WtxRZoLCWDeOXDqEz2VxzG6jlKcIwrM4E,16519
134
+ strawberry/http/async_base_view.py,sha256=RhCR58aHGpESgl_lSFSQgkrEFYFKCIE2SW5Lln5rxCo,20049
135
135
  strawberry/http/base.py,sha256=MiX0-RqOkhRvlfpmuvgTHp4tygbUmG8fnLc0uCrOllU,2550
136
136
  strawberry/http/exceptions.py,sha256=9E2dreS1crRoJVUEPuHyx23NcDELDHNzkAOa-rGv-8I,348
137
137
  strawberry/http/ides.py,sha256=WjU0nsMDgr3Bd1ebWkUEkO2d1hk0dI16mLqXyCHqklA,613
@@ -165,9 +165,9 @@ strawberry/schema/__init__.py,sha256=u1QCyDVQExUVDA20kyosKPz3TS5HMCN2NrXclhiFAL4
165
165
  strawberry/schema/base.py,sha256=wqvEOQ_aVkfebk9SlG9zg1YXl3MlwxGZhxFRoIkAxu0,4053
166
166
  strawberry/schema/compat.py,sha256=xNpOEDfi-MODpplMGaKuKeQIVcr-tcAaKaU3TlBc1Zs,1873
167
167
  strawberry/schema/config.py,sha256=KeZ1Pc1gvYK0fOx9Aghx7m0Av8sWexycl3HJGFgHPvg,969
168
- strawberry/schema/exceptions.py,sha256=rqVNb_oYrKM0dHPgvAemqCG6Um282LPPu4zwQ5cZqs4,584
168
+ strawberry/schema/exceptions.py,sha256=xXq-2wXfeGualEJObXja6yVIKzFTh_iDXCIWttpIOSE,769
169
169
  strawberry/schema/name_converter.py,sha256=xFOXEgqldFkxXRkIQvsJN1dPkWbEUaIrTYNOMYSEVwQ,6945
170
- strawberry/schema/schema.py,sha256=zRIv4mpVEFjFWv-MmfjO9v7OsuSFZ2xghr_ekIAuZI4,37113
170
+ strawberry/schema/schema.py,sha256=pTs_dfhqsftecPblY_jCPU7my8RKB1QnleicCJL35WE,37287
171
171
  strawberry/schema/schema_converter.py,sha256=_lKctaIfNcncVCan8AElYngGxMS8vf4Wy27tXfkr0Mk,39011
172
172
  strawberry/schema/types/__init__.py,sha256=oHO3COWhL3L1KLYCJNY1XFf5xt2GGtHiMC-UaYbFfnA,68
173
173
  strawberry/schema/types/base_scalars.py,sha256=JRUq0WjEkR9dFewstZnqnZKp0uOEipo4UXNF5dzRf4M,1971
@@ -187,7 +187,7 @@ strawberry/subscriptions/protocols/graphql_transport_ws/__init__.py,sha256=wN6dk
187
187
  strawberry/subscriptions/protocols/graphql_transport_ws/handlers.py,sha256=PFJDEYOXqqROkczVKZ8k4yWJbib1cE6ySx9GIR5hAVI,15079
188
188
  strawberry/subscriptions/protocols/graphql_transport_ws/types.py,sha256=N9r2mXg5jmmjYoZV5rWf3lAzgylCOUrbKGmClXCoOso,2169
189
189
  strawberry/subscriptions/protocols/graphql_ws/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
190
- strawberry/subscriptions/protocols/graphql_ws/handlers.py,sha256=h0E09Ktn1Ag9RWh4oALSfcIGYv8PdvPqaKAeriveYAY,8303
190
+ strawberry/subscriptions/protocols/graphql_ws/handlers.py,sha256=MWSipEW-mnklAvdqfmTdQZ4w3Dtk7lMLpIIuI4wyNz8,8675
191
191
  strawberry/subscriptions/protocols/graphql_ws/types.py,sha256=Uumiz-1O5qQnx-ERBaQtaf7db5yx-V9LMypOn9oGKwM,2003
192
192
  strawberry/test/__init__.py,sha256=lKVbKJDBnrYSPYHIKrg54UpaZcSoL93Z01zOpA1IzZM,115
193
193
  strawberry/test/client.py,sha256=ILAttb6A3jplH5wJNMeyyT1u_Q8KnollfpYLP_BVZR4,6438
@@ -229,8 +229,8 @@ strawberry/utils/logging.py,sha256=U1cseHGquN09YFhFmRkiphfASKCyK0HUZREImPgVb0c,7
229
229
  strawberry/utils/operation.py,sha256=s7ajvLg_q6v2mg47kEMQPjO_J-XluMKTCwo4d47mGvE,1195
230
230
  strawberry/utils/str_converters.py,sha256=-eH1Cl16IO_wrBlsGM-km4IY0IKsjhjnSNGRGOwQjVM,897
231
231
  strawberry/utils/typing.py,sha256=SDvX-Du-9HAV3-XXjqi7Q5f5qPDDFd_gASIITiwBQT4,14073
232
- strawberry_graphql-0.270.0.dist-info/LICENSE,sha256=m-XnIVUKqlG_AWnfi9NReh9JfKhYOB-gJfKE45WM1W8,1072
233
- strawberry_graphql-0.270.0.dist-info/METADATA,sha256=YwlJxcyaBIuozms-Yes8UmLXO5GFIlFUD2UjHatXNmQ,7679
234
- strawberry_graphql-0.270.0.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
235
- strawberry_graphql-0.270.0.dist-info/entry_points.txt,sha256=Nk7-aT3_uEwCgyqtHESV9H6Mc31cK-VAvhnQNTzTb4k,49
236
- strawberry_graphql-0.270.0.dist-info/RECORD,,
232
+ strawberry_graphql-0.270.2.dist-info/LICENSE,sha256=m-XnIVUKqlG_AWnfi9NReh9JfKhYOB-gJfKE45WM1W8,1072
233
+ strawberry_graphql-0.270.2.dist-info/METADATA,sha256=8c4qZ3Qf2KQQLpuYzyxXP-1FHW16cGk2yLwCZ2Mv5Mc,7679
234
+ strawberry_graphql-0.270.2.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
235
+ strawberry_graphql-0.270.2.dist-info/entry_points.txt,sha256=Nk7-aT3_uEwCgyqtHESV9H6Mc31cK-VAvhnQNTzTb4k,49
236
+ strawberry_graphql-0.270.2.dist-info/RECORD,,