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

Add Query Batching Support #3755

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
68 changes: 68 additions & 0 deletions RELEASE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
Release type: minor

## Add GraphQL Query batching support

GraphQL query batching is now supported across all frameworks (sync and async)
To enable query batching, set `batching_config.enabled` to True in the schema configuration.

This makes your GraphQL API compatible with batching features supported by various
client side libraries, such as [Apollo GraphQL](https://www.apollographql.com/docs/react/api/link/apollo-link-batch-http) and [Relay](https://github.com/relay-tools/react-relay-network-modern?tab=readme-ov-file#batching-several-requests-into-one).

Example (FastAPI):

```py
import strawberry

from fastapi import FastAPI
from strawberry.fastapi import GraphQLRouter
from strawberry.schema.config import StrawberryConfig


@strawberry.type
class Query:
@strawberry.field
def hello(self) -> str:
return "Hello World"


schema = strawberry.Schema(
Query, config=StrawberryConfig(batching_config={"enabled": True})
)

graphql_app = GraphQLRouter(schema)

app = FastAPI()
app.include_router(graphql_app, prefix="/graphql")
```

Example (Flask):
```py
import strawberry

from flask import Flask
from strawberry.flask.views import GraphQLView

app = Flask(__name__)


@strawberry.type
class Query:
@strawberry.field
def hello(self) -> str:
return "Hello World"


schema = strawberry.Schema(
Query, config=StrawberryConfig(batching_config={"enabled": True})
)

app.add_url_rule(
"/graphql/batch",
view_func=GraphQLView.as_view("graphql_view", schema=schema),
)

if __name__ == "__main__":
app.run()
```

Note: Query Batching is not supported for multipart subscriptions
144 changes: 144 additions & 0 deletions docs/guides/query-batching.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
---
title: Query Batching
---

# Query Batching

Query batching is a feature in Strawberry GraphQL that allows clients to send
multiple queries, mutations, or a combination of both in a single HTTP request.
This can help optimize network usage and improve performance for applications
that make frequent GraphQL requests.

This document explains how to enable query batching, its configuration options,
and how to integrate it into your application with an example using FastAPI.

---

## Enabling Query Batching

To enable query batching in Strawberry, you need to configure the
`StrawberryConfig` when defining your GraphQL schema. The batching configuration
is provided as a dictionary with the key `enabled`.

You can also specify the maximum number of operations allowed in a batch request
using the `max_operations` key.

### Basic Configuration

```python
from strawberry.schema.config import StrawberryConfig

config = StrawberryConfig(batching_config={"enabled": True})
```

### Configuring Maximum Operations

To set a limit on the number of operations in a batch request, use the
`max_operations` key:

```python
from strawberry.schema.config import StrawberryConfig

config = StrawberryConfig(batching_config={"enabled": True, "max_operations": 5})
```

When batching is enabled, the server can handle a list of operations
(queries/mutations) in a single request and return a list of responses.

## Example Integration with FastAPI

Query Batching is supported on all Strawberry GraphQL framework integrations.
Below is an example of how to enable query batching in a FastAPI application:

```python
import strawberry
from fastapi import FastAPI
from strawberry.fastapi import GraphQLRouter
from strawberry.schema.config import StrawberryConfig


@strawberry.type
class Query:
@strawberry.field
def hello(self) -> str:
return "Hello World"


schema = strawberry.Schema(
Query,
config=StrawberryConfig(batching_config={"enabled": True, "max_operations": 5}),
)

graphql_app = GraphQLRouter(schema)

app = FastAPI()
app.include_router(graphql_app, prefix="/graphql")
```

### Running the Application

1. Save the code in a file (e.g., `app.py`).
2. Start the FastAPI server:

```bash
uvicorn app:app --reload
```

3. The GraphQL endpoint will be available at `http://127.0.0.1:8000/graphql`.

### Testing Query Batching

You can test query batching by sending a single HTTP request with multiple
GraphQL operations. For example:

#### Request

```bash
curl -X POST -H "Content-Type: application/json" \
-d '[{"query": "{ hello }"}, {"query": "{ hello }"}]' \
http://127.0.0.1:8000/graphql
```

#### Response

```json
[{ "data": { "hello": "Hello World" } }, { "data": { "hello": "Hello World" } }]
```

### Error Handling

#### Batching Disabled

If batching is not enabled in the server configuration and a batch request is
sent, the server will respond with a 400 status code and an error message:

```json
{
"error": "Batching is not enabled"
}
```

#### Too Many Operations

If the number of operations in a batch exceeds the `max_operations` limit, the
server will return a 400 status code and an error message:

```json
{
"error": "Too many operations"
}
```

### Limitations

#### Multipart Subscriptions

Query batching does not support multipart subscriptions. Attempting to batch
such operations will result in a 400 error with a relevant message.

### Additional Notes

Query batching is particularly useful for clients that need to perform multiple
operations simultaneously, reducing the overhead of multiple HTTP requests.
Ensure your client library supports query batching before enabling it on the
server.
4 changes: 3 additions & 1 deletion strawberry/aiohttp/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,9 @@ async def get_context(
return {"request": request, "response": response} # type: ignore

def create_response(
self, response_data: GraphQLHTTPResponse, sub_response: web.Response
self,
response_data: Union[GraphQLHTTPResponse, list[GraphQLHTTPResponse]],
sub_response: web.Response,
) -> web.Response:
sub_response.text = self.encode_json(response_data)
sub_response.content_type = "application/json"
Expand Down
4 changes: 3 additions & 1 deletion strawberry/asgi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,9 @@ async def render_graphql_ide(self, request: Request) -> Response:
return HTMLResponse(self.graphql_ide_html)

def create_response(
self, response_data: GraphQLHTTPResponse, sub_response: Response
self,
response_data: Union[GraphQLHTTPResponse, list[GraphQLHTTPResponse]],
sub_response: Response,
) -> Response:
response = Response(
self.encode_json(response_data),
Expand Down
4 changes: 3 additions & 1 deletion strawberry/chalice/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,9 @@ def get_context(self, request: Request, response: TemporalResponse) -> Context:
return {"request": request, "response": response} # type: ignore

def create_response(
self, response_data: GraphQLHTTPResponse, sub_response: TemporalResponse
self,
response_data: Union[GraphQLHTTPResponse, list[GraphQLHTTPResponse]],
sub_response: TemporalResponse,
) -> Response:
status_code = 200

Expand Down
12 changes: 4 additions & 8 deletions strawberry/channels/handlers/http_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,7 @@
import warnings
from functools import cached_property
from io import BytesIO
from typing import (
TYPE_CHECKING,
Any,
Callable,
Optional,
Union,
)
from typing import TYPE_CHECKING, Any, Callable, Optional, Union
from typing_extensions import TypeGuard, assert_never
from urllib.parse import parse_qs

Expand Down Expand Up @@ -186,7 +180,9 @@ def __init__(
super().__init__(**kwargs)

def create_response(
self, response_data: GraphQLHTTPResponse, sub_response: TemporalResponse
self,
response_data: Union[GraphQLHTTPResponse, list[GraphQLHTTPResponse]],
sub_response: TemporalResponse,
) -> ChannelsResponse:
return ChannelsResponse(
content=json.dumps(response_data).encode(),
Expand Down
2 changes: 1 addition & 1 deletion strawberry/django/apps.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from django.apps import AppConfig # pragma: no cover


class StrawberryConfig(AppConfig): # pragma: no cover
class StrawberryAppConfig(AppConfig): # pragma: no cover
name = "strawberry"
4 changes: 3 additions & 1 deletion strawberry/django/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,9 @@ def __init__(
super().__init__(**kwargs)

def create_response(
self, response_data: GraphQLHTTPResponse, sub_response: HttpResponse
self,
response_data: Union[GraphQLHTTPResponse, list[GraphQLHTTPResponse]],
sub_response: HttpResponse,
) -> HttpResponseBase:
data = self.encode_json(response_data)

Expand Down
4 changes: 3 additions & 1 deletion strawberry/fastapi/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,9 @@ async def get_sub_response(self, request: Request) -> Response:
return self.temporal_response

def create_response(
self, response_data: GraphQLHTTPResponse, sub_response: Response
self,
response_data: Union[GraphQLHTTPResponse, list[GraphQLHTTPResponse]],
sub_response: Response,
) -> Response:
response = Response(
self.encode_json(response_data),
Expand Down
4 changes: 3 additions & 1 deletion strawberry/flask/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,9 @@ def __init__(
self.graphql_ide = graphql_ide

def create_response(
self, response_data: GraphQLHTTPResponse, sub_response: Response
self,
response_data: Union[GraphQLHTTPResponse, list[GraphQLHTTPResponse]],
sub_response: Response,
) -> Response:
sub_response.set_data(self.encode_json(response_data)) # type: ignore

Expand Down
Loading
Loading