Skip to content

Commit ce784da

Browse files
magicmarkclaude
andcommitted
Add HTTP multipart transport for GraphQL subscriptions
Implements the multipart subscription protocol for receiving streaming subscription updates over HTTP as an alternative to WebSocket transports. This protocol is implemented by Apollo GraphOS Router and other compatible servers, and is particularly useful when WebSocket connections are not available or blocked by infrastructure. The transport handles multipart/mixed responses with heartbeat support and proper error handling for both GraphQL and transport-level errors. It requires servers to support the multipart subscription protocol - requests that don't receive a multipart response will fail with a clear error message. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent a3a4597 commit ce784da

File tree

8 files changed

+1050
-1
lines changed

8 files changed

+1050
-1
lines changed
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import asyncio
2+
import logging
3+
4+
from gql import Client, gql
5+
from gql.transport.http_multipart_transport import HTTPMultipartTransport
6+
7+
logging.basicConfig(level=logging.INFO)
8+
9+
10+
async def main():
11+
12+
transport = HTTPMultipartTransport(
13+
url="http://localhost:8000/graphql"
14+
)
15+
16+
# Using `async with` on the client will start a connection on the transport
17+
# and provide a `session` variable to execute queries on this connection
18+
async with Client(
19+
transport=transport,
20+
) as session:
21+
22+
# Request subscription
23+
subscription = gql(
24+
"""
25+
subscription {
26+
book {
27+
title
28+
author
29+
}
30+
}
31+
"""
32+
)
33+
34+
# Subscribe and receive streaming updates
35+
async for result in session.subscribe(subscription):
36+
print(f"Received: {result}")
37+
38+
39+
asyncio.run(main())

docs/modules/gql.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ Sub-Packages
2929
transport_common_adapters_aiohttp
3030
transport_common_adapters_websockets
3131
transport_exceptions
32+
transport_http_multipart
3233
transport_phoenix_channel_websockets
3334
transport_requests
3435
transport_httpx
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
gql.transport.http\_multipart\_transport module
2+
===============================================
3+
4+
.. automodule:: gql.transport.http_multipart_transport
5+
:members:
6+
:undoc-members:
7+
:show-inheritance:

docs/transports/async_transports.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Async transports are transports which are using an underlying async library. The
1111

1212
aiohttp
1313
httpx_async
14+
http_multipart
1415
websockets
1516
aiohttp_websockets
1617
phoenix

docs/transports/http_multipart.rst

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
.. _http_multipart_transport:
2+
3+
HTTPMultipartTransport
4+
======================
5+
6+
This transport implements GraphQL subscriptions over HTTP using the `multipart subscription protocol`_
7+
as implemented by Apollo GraphOS Router and other compatible servers.
8+
9+
This provides an HTTP-based alternative to WebSocket transports for receiving streaming
10+
subscription updates. It's particularly useful when:
11+
12+
- WebSocket connections are not available or blocked by infrastructure
13+
- You want to use standard HTTP with existing load balancers and proxies
14+
- The backend implements the multipart subscription protocol
15+
16+
Reference: :class:`gql.transport.http_multipart_transport.HTTPMultipartTransport`
17+
18+
.. note::
19+
20+
This transport is specifically designed for GraphQL subscriptions. While it can handle
21+
queries and mutations via the ``execute()`` method, standard HTTP transports like
22+
:ref:`AIOHTTPTransport <aiohttp_transport>` are more efficient for those operations.
23+
24+
.. literalinclude:: ../code_examples/http_multipart_async.py
25+
26+
How It Works
27+
------------
28+
29+
The transport sends a standard HTTP POST request with an ``Accept`` header indicating
30+
support for multipart responses:
31+
32+
.. code-block:: text
33+
34+
Accept: multipart/mixed;subscriptionSpec="1.0", application/json
35+
36+
The server responds with a ``multipart/mixed`` content type and streams subscription
37+
updates as separate parts in the response body. Each part contains a JSON payload
38+
with GraphQL execution results.
39+
40+
Protocol Details
41+
----------------
42+
43+
**Message Format**
44+
45+
Each message part follows this structure:
46+
47+
.. code-block:: text
48+
49+
--graphql
50+
Content-Type: application/json
51+
52+
{"payload": {"data": {...}, "errors": [...]}}
53+
54+
**Heartbeats**
55+
56+
Servers may send empty JSON objects (``{}``) as heartbeat messages to keep the
57+
connection alive. These are automatically filtered out by the transport.
58+
59+
**Error Handling**
60+
61+
The protocol distinguishes between two types of errors:
62+
63+
- **GraphQL errors**: Returned within the ``payload`` property alongside data
64+
- **Transport errors**: Returned with a top-level ``errors`` field and ``null`` payload
65+
66+
**End of Stream**
67+
68+
The subscription ends when the server sends the final boundary marker:
69+
70+
.. code-block:: text
71+
72+
--graphql--
73+
74+
Authentication
75+
--------------
76+
77+
Authentication works the same as with :ref:`AIOHTTPTransport <aiohttp_transport>`.
78+
79+
Using HTTP Headers
80+
^^^^^^^^^^^^^^^^^^
81+
82+
.. code-block:: python
83+
84+
transport = HTTPMultipartTransport(
85+
url='https://SERVER_URL:SERVER_PORT/graphql',
86+
headers={'Authorization': 'Bearer YOUR_TOKEN'}
87+
)
88+
89+
Using HTTP Cookies
90+
^^^^^^^^^^^^^^^^^^
91+
92+
.. code-block:: python
93+
94+
transport = HTTPMultipartTransport(
95+
url=url,
96+
cookies={"session_id": "your_session_cookie"}
97+
)
98+
99+
Or use a cookie jar to save and reuse cookies:
100+
101+
.. code-block:: python
102+
103+
import aiohttp
104+
105+
jar = aiohttp.CookieJar()
106+
transport = HTTPMultipartTransport(
107+
url=url,
108+
client_session_args={'cookie_jar': jar}
109+
)
110+
111+
Configuration
112+
-------------
113+
114+
Timeout Settings
115+
^^^^^^^^^^^^^^^^
116+
117+
Set a timeout for the HTTP request:
118+
119+
.. code-block:: python
120+
121+
transport = HTTPMultipartTransport(
122+
url='https://SERVER_URL/graphql',
123+
timeout=30 # 30 second timeout
124+
)
125+
126+
SSL Configuration
127+
^^^^^^^^^^^^^^^^^
128+
129+
Control SSL certificate verification:
130+
131+
.. code-block:: python
132+
133+
transport = HTTPMultipartTransport(
134+
url='https://SERVER_URL/graphql',
135+
ssl=False # Disable SSL verification (not recommended for production)
136+
)
137+
138+
Or provide a custom SSL context:
139+
140+
.. code-block:: python
141+
142+
import ssl
143+
144+
ssl_context = ssl.create_default_context()
145+
ssl_context.load_cert_chain('client.crt', 'client.key')
146+
147+
transport = HTTPMultipartTransport(
148+
url='https://SERVER_URL/graphql',
149+
ssl=ssl_context
150+
)
151+
152+
Limitations
153+
-----------
154+
155+
- This transport requires the server to implement the multipart subscription protocol
156+
- Long-lived connections may be terminated by intermediate proxies or load balancers
157+
- Some server configurations may not support HTTP/1.1 chunked transfer encoding required for streaming
158+
159+
.. _multipart subscription protocol: https://www.apollographql.com/docs/graphos/routing/operations/subscriptions/multipart-protocol

gql/transport/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from .async_transport import AsyncTransport
2+
from .http_multipart_transport import HTTPMultipartTransport
23
from .transport import Transport
34

4-
__all__ = ["AsyncTransport", "Transport"]
5+
__all__ = ["AsyncTransport", "HTTPMultipartTransport", "Transport"]

0 commit comments

Comments
 (0)