graphql-python / graphql-python/graphene-django
Request body not always ignored for queries requested via HTTP GET
- Dominant language
- Python
- Stars
- 4.4k
- Forks
- 760
- PR merge metrics
- No merged PRs in 30d
Description
### What is the current behavior?
`graphene-django` will always attempt to JSON parse request body if the `Content-Type` header is set in the request, and rejects if it is invalid (such as when there's no payload at all), even if the request is an HTTP GET request.
### What is the expected behavior?
`graphene-django` should not read `request.body` at all when the request method is GET, regardless of the body's presence or any HTTP headers.
### Reason
# Per [RFC 7231 4.3.1](https://datatracker.ietf.org/doc/html/rfc7231#section-4.3.1)
> A payload within a GET request message has no defined semantics;
... which means a provider shouldn't try to understand the meaning of a request through the body. For GraphQL, when a query is made via HTTP GET, `graphene-django` should only attempt to parse URL parameters, [which is where the document is supposed to be specified](https://graphql.org/learn/serving-over-http/#get-request).
`graphene-django` is also more wrong here, since the client _didn't actually enclose a request body_ — `graphene-django` assumes that a body exists based on the `Content-Type` header, then tries to decode an empty string, which then throws `JSONDecodeError` and causes it to reject the request entirely.
Notwithstanding that it's also not a good idea to send `Content-Type` when there's no request payload (the Apollo client does this which is how I found out this issue), `graphene-django` should try to be more robust and not flat-out reject a query because of stray HTTP headers.
### To patch
A patch that fixes this specific issue (unclear if there are other side effects):
In `views.py`, in `dispatch`, replace:
https://github.com/graphql-python/graphene-django/blob/e7f7d8da07ba1020f9916153f17e97b0ec037712/graphene_django/views.py#L145
with
```py
if request.method == 'POST':
data = self.parse_body(request)
else:
data = {}
```
**More over, the view also rejects POST requests if there is a `query=` URL parameter that contains an invalid GraphQL document (see below)**, thus:
In `views.py`, in `get_graphql_params`, replace:
https://github.com/graphql-python/graphene-django/blob/e7f7d8da07ba1020f9916153f17e97b0ec037712/graphene_django/views.py#L371-L372
with
```py
if request.method == 'GET':
query = request.GET.get("query")
variables = request.GET.get("variables")
else:
query = data.get("query")
variables = data.get("variables")
```
### To reproduce
Environment: Python 3.9.6, `graphene-django==3.0.0b7`, `django==3.2.5`
Create a new django project, then create some minimal schema and URL config following documentation.
```python
class Query(ObjectType):
truth = Field(Boolean)
@classmethod
def resolve_truth(cls, root, info):
return True
schema = Schema(query=Query)
```
Run the server, then use cURL (or any other HTTP library), observe the following:
No issue for GET requests without a `Content-Type` header:
```shell
> curl -L 'http://127.0.0.1:8000/gql/?query=query%7Btruth%7D'
{"data":{"truth":true}}
```
**HTTP 400 when the request contains `Content-Type: application/json`:**
```shell
curl -i -H 'Content-Type: application/json' 'http://127.0.0.1:8000/gql/?query=query%7Btruth%7D'
HTTP/1.1 400 Bad Request
...
{"errors":[{"message":"POST body sent invalid JSON."}]}
```
**HTTP 400 when it is a POST request with a valid body but also a `query` in the URL with invalid data:**
```shell
> curl -i -X POST -H 'Content-Type: application/graphql' --data 'query{truth}' 'http://127.0.0.1:8000/gql/?query=query%7Btruth' # <-- Invalid parameter
HTTP/1.1 400 Bad Request
...
{"errors":[{"message":"Syntax Error: Expected Name, found .","locations":[{"line":1,"column":12}],"path":null}]}
```
Contributor guide
Assessment
This issue has not been assessed yet.