Usage¶
A view accepts QUERY by defining a query() handler. The request body is parsed by DRF's usual parsers, so
request.data works as it does for POST:
from rest_framework.response import Response
from rest_framework.views import APIView
class SearchNotes(APIView):
def query(self, request):
notes = Note.objects.filter(title__icontains=request.data.get("title", ""))
return Response(NoteSerializer(notes, many=True).data)
Generic views and viewsets don't provide query(), so a list view that should accept QUERY delegates to list():
class NoteViewSet(ModelViewSet):
...
def query(self, request, *args, **kwargs):
return self.list(request, *args, **kwargs)
The rest of DRF needs to know about QUERY too: routers, viewsets, filters, permissions and the test client. There
are two ways to do that.
Option 1: patch DRF¶
Call patch() once at startup, for example in one of your apps' AppConfig.ready():
from django.apps import AppConfig
class NotesConfig(AppConfig):
name = "notes"
def ready(self):
import drf_query_method
drf_query_method.patch()
DRF then behaves as if encode/django-rest-framework#10091 were merged:
SimpleRouterandDefaultRouterrouteQUERYto a viewset'squery()on the list route only. An existing@actionnamedquerykeeps its own route.QUERYis inpermissions.SAFE_METHODS, and needs no permissions inDjangoModelPermissionsandDjangoObjectPermissions.request.filter_paramsmerges aQUERYrequest's body over its URL query parameters, andSearchFilterandOrderingFilterread from it.@action(...).mapping.query,APIRequestFactory.query()andAPIClient.query()are available.- DRF's deprecated OpenAPI generator leaves
QUERYout, as OpenAPI 3.0 has no query operation. - If django-filter is installed,
DjangoFilterBackendreads the request body too. - If django-rest-framework-datatables is installed, its filter backends, pagination and renderer read the request body too. See DataTables below.
patch() is safe to call more than once, and leaves DRF alone once DRF supports QUERY itself.
Option 2: opt-in classes¶
Every class has the same name as DRF's (or django-filter's), so you change the import rather than the code:
| Instead of | Import |
|---|---|
rest_framework.routers.SimpleRouter, DefaultRouter |
drf_query_method.routers |
rest_framework.viewsets.ViewSet, GenericViewSet, ReadOnlyModelViewSet, ModelViewSet |
drf_query_method.viewsets |
rest_framework.filters.SearchFilter, OrderingFilter |
drf_query_method.filters |
rest_framework.permissions.IsAuthenticatedOrReadOnly, DjangoModelPermissions, DjangoModelPermissionsOrAnonReadOnly, DjangoObjectPermissions, SAFE_METHODS |
drf_query_method.permissions |
rest_framework.test.APIRequestFactory, APIClient, APITestCase and friends |
drf_query_method.test |
django_filters.rest_framework.DjangoFilterBackend |
drf_query_method.django_filters |
django-rest-framework-datatables is supported through patch() only.
Use the router and viewset classes together: the viewsets stop a query() method answering QUERY on detail routes.
On Django versions before 6.2, also call drf_query_method.add_query_method() at startup (patch() does this for you).
Filters and the request body¶
QUERY parameters take the same form as query-string values. Multiple ordering fields are one comma-separated string,
and a JSON list becomes multiple values, like a repeated query-string parameter:
QUERY /notes/?page=2 HTTP/1.1
Content-Type: application/json
{"search": "django", "ordering": "-created,title"}
Pagination parameters stay in the URL, so the next and previous links carry the page. Clients must send them as
QUERY requests with the same body to keep the filters.
Custom filter backends should read their parameters with drf_query_method.filter_params(request) rather than
request.query_params.
Sending QUERY requests¶
Most HTTP clients accept any method name. With fetch():
const response = await fetch("/api/notes/", {
method: "QUERY",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({search: "django", ordering: "-created"}),
});
With jQuery, which sends data as a form-encoded body for any method other than GET and HEAD:
With curl:
curl -X QUERY -H "Content-Type: application/json" -d '{"search": "django"}' https://example.com/api/notes/
A few things to check before relying on QUERY in production:
-
CORS.
QUERYis not a CORS-safelisted method, so cross-origin requests trigger a preflight request, and the server must listQUERYinAccess-Control-Allow-Methods. With django-cors-headers, add it toCORS_ALLOW_METHODS: -
CSRF. None needed:
QUERYis a safe method, so it never needs a CSRF token on any supported Django version. See Django versions before 6.2 for how older versions get the same behaviour. - Proxies, CDNs and firewalls. Some reject request methods they don't recognise. Check that
QUERYreaches your application through everything in front of it. - Caching.
QUERYresponses are cacheable in principle, but caches generally don't key on request bodies, and Django'scache_pageignoresQUERYfrom 6.2. Don't expect responses to be cached.
DataTables¶
With patch() and django-rest-framework-datatables,
a server-side DataTable can send its parameters as a QUERY body instead of a long query string. Keep format=datatables
in the URL, and set the Ajax type:
new DataTable("#notes", {
serverSide: true,
ajax: {url: "/api/notes/?format=datatables", type: "QUERY"},
columns: [{data: "title"}, {data: "created"}],
});
jQuery sends DataTables' parameters as a form-encoded body for any method other than GET and HEAD, which is the
form django-rest-framework-datatables expects. The view needs a query() handler, as above. Its ?keep= parameter is
still read from the URL.
Django versions before 6.2¶
Django 6.2 supports QUERY natively. On Django 4.2 to 6.1, add_query_method() (which patch() calls) brings the same
behaviour to older versions in two ways:
- It adds
queryto Django's allowed view methods, so views can define aquery()handler. - It treats
QUERYas a safe method inCsrfViewMiddleware, as Django 6.2 does. Before 6.2 the middleware only exemptsGET,HEAD,OPTIONSandTRACE, so without this, session-authenticatedQUERYrequests would need a CSRF token. DRF'sSessionAuthenticationchecks CSRF through the same middleware, so it follows suit.
The result is that QUERY never needs a CSRF token, whichever supported Django version you're on. If you call
add_query_method() yourself rather than patch(), call it once at startup, before requests are served.
This is safe for the same reason it is in Django 6.2. CSRF protects against another site making a logged-in user's
browser send a request. HTML forms can only send GET and POST, so QUERY can only come from JavaScript, and
because QUERY is not a CORS-safelisted method, the browser asks your server's permission with a preflight request
before sending a cross-origin one. As with GET, a query() handler must never change data.
Moving to DRF's own support¶
If DRF adds QUERY support, remove the patch() call (or switch the imports back to DRF's) and uninstall this
package. Your query() handlers, request.filter_params and test code keep working.