<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Ben Atkinson</title>
    <description>The latest articles on DEV Community by Ben Atkinson (@ben_atkinson_sa).</description>
    <link>https://dev.to/ben_atkinson_sa</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4217961%2Fa3e570c8-9e3e-4eda-b269-257c573b5b6f.jpg</url>
      <title>DEV Community: Ben Atkinson</title>
      <link>https://dev.to/ben_atkinson_sa</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ben_atkinson_sa"/>
    <language>en</language>
    <item>
      <title>The New HTTP QUERY Method, and Using It in Django</title>
      <dc:creator>Ben Atkinson</dc:creator>
      <pubDate>Fri, 30 Oct 2026 21:52:21 +0000</pubDate>
      <link>https://dev.to/ben_atkinson_sa/my-datatables-urls-got-too-long-so-i-went-looking-at-the-http-query-method-21ea</link>
      <guid>https://dev.to/ben_atkinson_sa/my-datatables-urls-got-too-long-so-i-went-looking-at-the-http-query-method-21ea</guid>
      <description>&lt;p&gt;This is my first post on here, so bear with me. It's about the new HTTP &lt;code&gt;QUERY&lt;/code&gt; method, and a small package I've put together so anyone can use it with &lt;a href="https://www.django-rest-framework.org/" rel="noopener noreferrer"&gt;Django REST framework&lt;/a&gt; now, rather than waiting for more widespread adoption. But mostly it's about the problem that made me care about it in the first place.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem: GET URLs that kept growing
&lt;/h2&gt;

&lt;p&gt;I look after a &lt;a href="https://www.djangoproject.com/" rel="noopener noreferrer"&gt;Django&lt;/a&gt; project for a client where most of the app is big filterable, sortable tables. They're server-side &lt;a href="https://datatables.net/" rel="noopener noreferrer"&gt;DataTables&lt;/a&gt;, backed by &lt;a href="https://github.com/izimobil/django-rest-framework-datatables" rel="noopener noreferrer"&gt;django-rest-framework-datatables&lt;/a&gt;, with &lt;a href="https://github.com/carltongibson/django-filter" rel="noopener noreferrer"&gt;django-filter&lt;/a&gt; doing a lot of the filtering on the backend. It's a setup I really like - it's simple to set up and modify over time, and all the searching, sorting and paging happens in the database.&lt;/p&gt;

&lt;p&gt;The issue is how DataTables asks. In server-side mode, by default, it sends everything as a GET query string. Not just the search box and the page, but six parameters for every single column (&lt;code&gt;columns[3][data]&lt;/code&gt;, &lt;code&gt;columns[3][searchable]&lt;/code&gt;, &lt;code&gt;columns[3][search][value]&lt;/code&gt; and so on), plus the ordering, plus whatever extra filters you bolt on.&lt;/p&gt;

&lt;p&gt;Our main table has 21 columns, and on top of that the filter panel adds around 40 more parameters - booleans, ranges, multi-selects, tags, and so on. When I rebuilt a request roughly from today's table, a relatively simple URL came out at about 4.8KB. Pick a handful of options in one of the multi-selects and it's well past 6KB.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://gunicorn.org/" rel="noopener noreferrer"&gt;Gunicorn&lt;/a&gt;'s default limit on the request line is 4094 bytes. So, unsurprisingly, years ago, it fell over, and left me scrambling to try and work out why, and how to fix it. My fix back in November 2022 was a commit called, in full, "Attempt to fix gunicorn error.", which added &lt;code&gt;--limit-request-line 0&lt;/code&gt; to the gunicorn start script. Zero means no limit at all.&lt;/p&gt;

&lt;p&gt;That worked, but it never sat right with me. I have always felt there must be a better approach.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why not just use POST?
&lt;/h3&gt;

&lt;p&gt;The obvious answer is to switch the tables to POST and put everything in the body.&lt;/p&gt;

&lt;p&gt;But it's a read pretending to be a write. POST says "this changes something", so it isn't cacheable, it isn't safe to retry, it needs a CSRF token for what is just a search, and any permission class that checks for safe methods treats it as a write. With DRF in particular, &lt;code&gt;IsAuthenticatedOrReadOnly&lt;/code&gt; and friends would see a list request as an attempt to create something. You end up working around the framework instead of with it.&lt;/p&gt;

&lt;p&gt;So it was either a URL nobody should have to look at, or a request that lies about what it does. Selfishly, I just wanted my tables to work properly, but I think a lot of people are in the same spot.&lt;/p&gt;

&lt;h2&gt;
  
  
  What QUERY is
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc10008.html" rel="noopener noreferrer"&gt;RFC 10008&lt;/a&gt; defines a new HTTP method, &lt;code&gt;QUERY&lt;/code&gt;. It's the first new method since &lt;code&gt;PATCH&lt;/code&gt;. The short version is that it's safe and idempotent like &lt;code&gt;GET&lt;/code&gt;, but it carries a request body like &lt;code&gt;POST&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It fills the gap between GET and POST exactly, which makes it a perfect fit for dynamic filtering - search screens, filter panels and big tables like mine, where users can combine any number of filters. Filters go in the body, so the size limit is one you control rather than the tightest thing on the path, and the request is still honestly a read, so retries, permissions and (in principle) caching all behave the way they should.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where things are at
&lt;/h2&gt;

&lt;p&gt;Django has merged &lt;code&gt;QUERY&lt;/code&gt; support for 6.2 (&lt;a href="https://github.com/django/django/pull/21855" rel="noopener noreferrer"&gt;django/django#21855&lt;/a&gt;, by Michiel Beijen). &lt;code&gt;"query"&lt;/code&gt; is now in &lt;code&gt;View.http_method_names&lt;/code&gt;, so a class-based view just defines a &lt;code&gt;query()&lt;/code&gt; method. It's treated as safe throughout, so it's exempt from CSRF, &lt;code&gt;cache_page&lt;/code&gt; and the cache middleware don't cache it, and the test client gets &lt;code&gt;RequestFactory.query()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That's the foundation, but I don't write plain Django views for my APIs. I use packages like DRF, and DRF (along with the packages around it) doesn't know about &lt;code&gt;QUERY&lt;/code&gt; yet. When I talked through how I could help make this new method easily adoptable with my mentor, &lt;a href="https://www.better-simple.com/" rel="noopener noreferrer"&gt;Tim Schilling&lt;/a&gt; (fantastic, by the way, if anyone is looking for coding-based mentoring, whether that's technical support or career support), his suggestion was to give people something they can use now, as well as contributing to the wider community directly. So here we are.&lt;/p&gt;

&lt;h2&gt;
  
  
  drf-query-method
&lt;/h2&gt;

&lt;p&gt;Until the rest of the ecosystem catches up, I've put together &lt;a href="https://pypi.org/project/drf-query-method/" rel="noopener noreferrer"&gt;drf-query-method&lt;/a&gt;, a small package that adds &lt;code&gt;QUERY&lt;/code&gt; support to DRF today.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;drf-query-method
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Call &lt;code&gt;patch()&lt;/code&gt; once at startup, for example in an &lt;code&gt;AppConfig.ready()&lt;/code&gt;, and give your viewset a &lt;code&gt;query()&lt;/code&gt; handler:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;drf_query_method&lt;/span&gt;

&lt;span class="n"&gt;drf_query_method&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;patch&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;NoteViewSet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ModelViewSet&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;queryset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Note&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;serializer_class&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;NoteSerializer&lt;/span&gt;
    &lt;span class="n"&gt;filter_backends&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;SearchFilter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;OrderingFilter&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;search_fields&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;ordering_fields&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;created&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now &lt;code&gt;QUERY /notes/&lt;/code&gt; goes to &lt;code&gt;query()&lt;/code&gt;, and the filters read their parameters from the body:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="nf"&gt;QUERY&lt;/span&gt; &lt;span class="nn"&gt;/notes/&lt;/span&gt; &lt;span class="k"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt;
&lt;span class="na"&gt;Content-Type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;application/json&lt;/span&gt;

&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"search"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"django"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"ordering"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"-created,title"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you'd rather not patch DRF, there are opt-in classes with the same names as DRF's (&lt;code&gt;drf_query_method.routers&lt;/code&gt;, &lt;code&gt;.viewsets&lt;/code&gt;, &lt;code&gt;.filters&lt;/code&gt;, &lt;code&gt;.permissions&lt;/code&gt;, &lt;code&gt;.test&lt;/code&gt;), so it's an import change rather than a code change.&lt;/p&gt;

&lt;p&gt;It also works on older Django. Django 6.2 adds &lt;code&gt;QUERY&lt;/code&gt; itself; on 4.2 to 6.1 the package adds it for you (&lt;code&gt;add_query_method()&lt;/code&gt;, which &lt;code&gt;patch()&lt;/code&gt; calls).&lt;/p&gt;

&lt;p&gt;A quick word on CSRF, because it's one of the nice things about &lt;code&gt;QUERY&lt;/code&gt;: you don't need a token. It's a safe method, so Django 6.2 skips the CSRF check for it, just as it does for GET. Older versions of Django's CSRF middleware only know about GET, HEAD, OPTIONS and TRACE, so on 4.2 to 6.1 the package teaches it that &lt;code&gt;QUERY&lt;/code&gt; is safe too. Either way, you don't send a CSRF token, even with session authentication. It's still protected against cross-site requests by CORS: browsers can only send &lt;code&gt;QUERY&lt;/code&gt; from JavaScript, and a cross-origin &lt;code&gt;QUERY&lt;/code&gt; needs your server's permission first. The rule is the same as for GET - a &lt;code&gt;query()&lt;/code&gt; handler should never change anything.&lt;/p&gt;

&lt;h3&gt;
  
  
  Closing the loop on DataTables
&lt;/h3&gt;

&lt;p&gt;This was the bit I actually wanted. If django-filter is installed, &lt;code&gt;patch()&lt;/code&gt; makes &lt;code&gt;DjangoFilterBackend&lt;/code&gt; read the body too, and as of 0.2.0 it does the same for django-rest-framework-datatables - its filter backends, pagination and renderer.&lt;/p&gt;

&lt;p&gt;So the table setup becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DataTable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#notes&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;serverSide&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;ajax&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/notes/?format=datatables&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;QUERY&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;columns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;title&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;created&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://jquery.com/" rel="noopener noreferrer"&gt;jQuery&lt;/a&gt; sends &lt;code&gt;data&lt;/code&gt; as a form-encoded body for any method other than GET and HEAD, which is the shape django-rest-framework-datatables already expects. &lt;code&gt;format=datatables&lt;/code&gt; stays in the URL, and everything else moves into the body. And there's no CSRF token to send, because &lt;code&gt;QUERY&lt;/code&gt; is a safe method.&lt;/p&gt;

&lt;h3&gt;
  
  
  htmx supports it too
&lt;/h3&gt;

&lt;p&gt;For &lt;a href="https://htmx.org/" rel="noopener noreferrer"&gt;htmx&lt;/a&gt; lovers like myself, who are probably wondering whether this works with htmx - it does, and htmx got there first. htmx 4 (released on 28 August 2026) adds an &lt;a href="https://four.htmx.org/reference/attributes/hx-query" rel="noopener noreferrer"&gt;&lt;code&gt;hx-query&lt;/code&gt;&lt;/a&gt; attribute. It sends a &lt;code&gt;QUERY&lt;/code&gt; request with the form values in the body, the same way &lt;code&gt;hx-post&lt;/code&gt; does, rather than squeezing them into the URL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt; &lt;span class="na"&gt;hx-query=&lt;/span&gt;&lt;span class="s"&gt;"/notes/search/"&lt;/span&gt; &lt;span class="na"&gt;hx-target=&lt;/span&gt;&lt;span class="s"&gt;"#results"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"search"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;select&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"ordering"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;...&lt;span class="nt"&gt;&amp;lt;/select&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;htmx form-encodes the values, and &lt;code&gt;filter_params&lt;/code&gt; merges a form-encoded body over the URL parameters, so &lt;code&gt;SearchFilter&lt;/code&gt;, &lt;code&gt;OrderingFilter&lt;/code&gt; and (with &lt;code&gt;patch()&lt;/code&gt;) django-filter all pick them up with nothing extra. And as with DataTables, there's no CSRF header to add.&lt;/p&gt;

&lt;p&gt;The one thing to know is that htmx 4 sits under the &lt;code&gt;next&lt;/code&gt; tag on npm, with 2.x still on &lt;code&gt;latest&lt;/code&gt;, so you have to ask for version 4 explicitly. htmx 2 doesn't have &lt;code&gt;hx-query&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It's on PyPI (0.3.0 at the time of writing), and the &lt;a href="https://bena-sa.github.io/drf-query-method/" rel="noopener noreferrer"&gt;docs are here&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The plan: make this package redundant
&lt;/h2&gt;

&lt;p&gt;I'd honestly rather this package didn't need to exist for long. The aim is for the main packages to support &lt;code&gt;QUERY&lt;/code&gt; themselves, at which point you can drop drf-query-method from your dependencies and rely on them instead. To help with that, it deliberately uses the same names as the upstream work - &lt;code&gt;query()&lt;/code&gt; handlers and &lt;code&gt;request.filter_params&lt;/code&gt; - so removing it should mean deleting a line or two rather than rewriting your views.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DRF.&lt;/strong&gt; Back in August I opened &lt;a href="https://github.com/encode/django-rest-framework/discussions/10024" rel="noopener noreferrer"&gt;a discussion on DRF&lt;/a&gt; about supporting &lt;code&gt;QUERY&lt;/code&gt;. DRF describes itself as essentially feature-complete, and the reply at the time was, reasonably, to land it in Django first. As that's now happened, I've opened a PR to update DRF accordingly &lt;a href="https://github.com/encode/django-rest-framework/pull/10091" rel="noopener noreferrer"&gt;encode/django-rest-framework#10091&lt;/a&gt;. It adds &lt;code&gt;QUERY&lt;/code&gt; to &lt;code&gt;SAFE_METHODS&lt;/code&gt; and the permission maps, routes it to a viewset's &lt;code&gt;query()&lt;/code&gt; on the list route, adds &lt;code&gt;request.filter_params&lt;/code&gt; for the filters to read, and adds &lt;code&gt;.query()&lt;/code&gt; to the test client.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;django-filter.&lt;/strong&gt; If and when the DRF change is merged, I'd like to contribute the matching change to django-filter, so &lt;code&gt;DjangoFilterBackend&lt;/code&gt; reads &lt;code&gt;QUERY&lt;/code&gt; bodies through the same &lt;code&gt;filter_params&lt;/code&gt; idea. I've already worked out what needs to change.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/tfranzel/drf-spectacular" rel="noopener noreferrer"&gt;drf-spectacular&lt;/a&gt;.&lt;/strong&gt; Schema generation currently crashes with &lt;code&gt;KeyError: 'query'&lt;/code&gt; on any view with a &lt;code&gt;query()&lt;/code&gt; handler, so I've opened a one-line fix: &lt;a href="https://github.com/tfranzel/drf-spectacular/pull/1553" rel="noopener noreferrer"&gt;tfranzel/drf-spectacular#1553&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Once those land, the main ecosystem supports &lt;code&gt;QUERY&lt;/code&gt; and drf-query-method has done its job. If it doesn't work out - some big packages are understandably conservative about adopting new things too quickly - the package stays as the fix, and I'll keep it maintained.&lt;/p&gt;

&lt;h2&gt;
  
  
  Things to watch out for
&lt;/h2&gt;

&lt;p&gt;It's a new method, so not everything knows about it yet.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CORS.&lt;/strong&gt; &lt;code&gt;QUERY&lt;/code&gt; isn't a CORS-safelisted method, so cross-origin requests get a preflight and the server has to allow it. With &lt;a href="https://github.com/adamchainz/django-cors-headers" rel="noopener noreferrer"&gt;django-cors-headers&lt;/a&gt;, add it to &lt;code&gt;CORS_ALLOW_METHODS&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Proxies, CDNs and firewalls.&lt;/strong&gt; Some reject methods they don't recognise. Check it actually reaches Django through everything in front of it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Caching.&lt;/strong&gt; Responses are cacheable in principle, but caches generally don't key on request bodies, and Django's &lt;code&gt;cache_page&lt;/code&gt; won't cache &lt;code&gt;QUERY&lt;/code&gt;. Don't expect it to be cached.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The browsable API&lt;/strong&gt; can't send &lt;code&gt;QUERY&lt;/code&gt;, so you'll be testing with curl, &lt;code&gt;fetch()&lt;/code&gt; or the test client.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Feedback welcome
&lt;/h2&gt;

&lt;p&gt;This is my first real go at contributing to DRF, so I'd really appreciate people kicking the tyres. If you've got a filter-heavy API or a big DataTable, try it and tell me what breaks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Package: &lt;a href="https://pypi.org/project/drf-query-method/" rel="noopener noreferrer"&gt;pypi.org/project/drf-query-method&lt;/a&gt; (&lt;a href="https://github.com/BenA-SA/drf-query-method" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;, &lt;a href="https://bena-sa.github.io/drf-query-method/" rel="noopener noreferrer"&gt;docs&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;DRF pull request: &lt;a href="https://github.com/encode/django-rest-framework/pull/10091" rel="noopener noreferrer"&gt;encode/django-rest-framework#10091&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;drf-spectacular fix: &lt;a href="https://github.com/tfranzel/drf-spectacular/pull/1553" rel="noopener noreferrer"&gt;tfranzel/drf-spectacular#1553&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And if you think &lt;code&gt;QUERY&lt;/code&gt; belongs in DRF itself (or don't), a comment on the pull request would help the maintainers see whether people actually want it. Thanks for reading!&lt;/p&gt;

</description>
      <category>django</category>
      <category>python</category>
      <category>api</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
