Skip to content

Commit dc50af3

Browse files
docs: add decorator workaround note for subscriptions in middleware and extensions doc
1 parent 0f0b033 commit dc50af3

3 files changed

Lines changed: 140 additions & 3 deletions

File tree

docs/06-Extensions/01-extensions.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ title: Extension system
55

66
Ariadne implements simple extension system that allows developers to inject custom python logic into the query execution process. This system was designed with performance measurement extensions in mind but may potentially support other use cases.
77

8-
> At the moment adding extensions to subscriptions is not supported.
8+
> At the moment adding extensions to subscriptions is not supported. Use Python decorators applied directly to your subscription source and resolver functions instead. See the [`subscription_auth_source_vs_resolver`](https://github.com/mirumee/ariadne/tree/main/examples/subscription_auth_source_vs_resolver.py) example for a worked example with auth and logging decorators.
99
1010

1111
## Enabling extensions

docs/06-Extensions/02-middleware.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ def lowercase_middleware(resolver, obj, info, **args)
1717
1818
> **Note**
1919
>
20-
> Middleware is not supported by subscriptions.
20+
> Middleware is not supported by subscriptions. Use Python decorators applied directly to your subscription source and resolver functions instead. See the [`subscription_auth_source_vs_resolver`](https://github.com/mirumee/ariadne/tree/main/examples/subscription_auth_source_vs_resolver.py) example for a worked example with auth and logging decorators.
2121
2222

2323
## Custom middleware example
@@ -194,4 +194,4 @@ async def lowercase_middleware(resolver, obj, info, **args):
194194
return value
195195
```
196196

197-
However, asynchronous middleware require for their result being `awaited` during query execution. Asynchronous functions in Python are considered fast, but the overhead of being sent to the event loop and having their result retrieved makes them **much** slower than plain synchronous function call for scenarios where no IO is involved. Because default implementation of middleware manager calls middlewares for every field in result set, this effectively turns all resolver calls in query executor into asynchronous calls. This makes query execution noticeably slower even for small GraphQL queries, and quick benchmarks have found that it can slow queries by x1.5 to x2.5.
197+
However, asynchronous middleware require for their result being `awaited` during query execution. Asynchronous functions in Python are considered fast, but the overhead of being sent to the event loop and having their result retrieved makes them **much** slower than plain synchronous function call for scenarios where no IO is involved. Because default implementation of middleware manager calls middlewares for every field in result set, this effectively turns all resolver calls in query executor into asynchronous calls. This makes query execution noticeably slower even for small GraphQL queries, and quick benchmarks have found that it can slow queries by x1.5 to x2.5.
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
"""
2+
Example: Bearer auth in a source, logging middleware in resolver, delivered over SSE.
3+
4+
This example demonstrates how to:
5+
- Guard a subscription source with Bearer token authentication
6+
- Apply resolver-level middleware (logging) to subscription resolvers
7+
8+
Run with:
9+
uvicorn examples.subscription_auth_source_vs_resolver:app --reload
10+
or:
11+
uv run --with "uvicorn[standard]" --with ariadne \
12+
uvicorn examples.subscription_auth_source_vs_resolver:app --reload
13+
14+
Then test with curl:
15+
16+
# With Bearer token — streams counter values via SSE
17+
curl -N -X POST http://localhost:8000/graphql/ \
18+
-H "Content-Type: application/json" \
19+
-H "Accept: text/event-stream" \
20+
-H "Authorization: Bearer mytoken" \
21+
-d '{"query": "subscription { counter }"}'
22+
23+
# Without Bearer token — source raises PermissionError immediately
24+
curl -N -X POST http://localhost:8000/graphql/ \
25+
-H "Content-Type: application/json" \
26+
-H "Accept: text/event-stream" \
27+
-d '{"query": "subscription { counter }"}'
28+
"""
29+
30+
from __future__ import annotations
31+
32+
import asyncio
33+
import logging
34+
from functools import wraps
35+
from typing import Any
36+
37+
from graphql import GraphQLResolveInfo
38+
from starlette.applications import Starlette
39+
40+
from ariadne import QueryType, SubscriptionType, make_executable_schema
41+
from ariadne.asgi import GraphQL
42+
from ariadne.asgi.handlers import GraphQLHTTPHandler
43+
from ariadne.contrib.sse import SSESubscriptionHandler
44+
45+
logging.basicConfig(
46+
level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s"
47+
)
48+
log = logging.getLogger(__name__)
49+
50+
# ---------------------------------------------------------------------------
51+
# Bearer auth helpers
52+
# ---------------------------------------------------------------------------
53+
54+
55+
def get_bearer_token(info: GraphQLResolveInfo) -> str | None:
56+
ctx = getattr(info, "context", None) or {}
57+
req = ctx.get("request") if isinstance(ctx, dict) else None
58+
if not req or not hasattr(req, "headers"):
59+
return None
60+
a = req.headers.get("Authorization", "")
61+
return a[7:].strip() if a.startswith("Bearer ") else None
62+
63+
64+
def require_bearer_token_source(fn):
65+
"""Decorator for subscription sources that require a Bearer token."""
66+
67+
@wraps(fn)
68+
async def wrapper(obj: Any, info: GraphQLResolveInfo, **kw: Any):
69+
if not get_bearer_token(info):
70+
raise PermissionError("Missing Authorization: Bearer <token>")
71+
async for v in fn(obj, info, **kw):
72+
yield v
73+
74+
return wrapper
75+
76+
77+
def log_resolver(fn):
78+
@wraps(fn)
79+
def wrapper(obj: Any, info: GraphQLResolveInfo, **kw: Any) -> Any:
80+
result = fn(obj, info, **kw)
81+
log.info("resolver %s obj=%r", info.field_name, obj)
82+
return result
83+
84+
return wrapper
85+
86+
87+
type_defs = """
88+
type Query {
89+
ping: String
90+
}
91+
92+
type Subscription {
93+
counter: Int
94+
}
95+
"""
96+
97+
query = QueryType()
98+
subscription = SubscriptionType()
99+
100+
101+
@query.field("ping")
102+
def resolve_ping(*_: Any) -> str:
103+
return "pong"
104+
105+
106+
@subscription.source("counter")
107+
@require_bearer_token_source
108+
async def counter_source(*_: Any):
109+
for i in range(5):
110+
await asyncio.sleep(0.5)
111+
yield i
112+
113+
114+
@subscription.field("counter")
115+
@log_resolver
116+
def resolve_counter(obj: Any, info: GraphQLResolveInfo) -> Any:
117+
return obj
118+
119+
120+
schema = make_executable_schema(type_defs, query, subscription)
121+
122+
graphql_app = GraphQL(
123+
schema,
124+
debug=True,
125+
http_handler=GraphQLHTTPHandler(
126+
subscription_handlers=[SSESubscriptionHandler()],
127+
),
128+
)
129+
130+
app = Starlette()
131+
app.mount("/graphql", graphql_app)
132+
133+
134+
if __name__ == "__main__":
135+
import uvicorn
136+
137+
uvicorn.run(app, host="0.0.0.0", port=8000)

0 commit comments

Comments
 (0)