Skip to content

Add support for the HTTP QUERY method (RFC 10008) - #2

Open
BenA-SA wants to merge 1 commit into
mainfrom
query-method-support
Open

Add support for the HTTP QUERY method (RFC 10008)#2
BenA-SA wants to merge 1 commit into
mainfrom
query-method-support

Conversation

@BenA-SA

@BenA-SA BenA-SA commented Aug 22, 2026

Copy link
Copy Markdown
Owner

Reference implementation for the upstream discussion encode/django-rest-framework#10024. Opened against BenA-SA:main so the diff is reviewable; not submitted to encode/django-rest-framework, because DRF asks that pull requests are opened only after a maintainer recommends it. See #1 for the draft write-up.

Adds support for the QUERY method defined by RFC 10008: safe and idempotent like GET, but carrying a request body like POST.

Django does not implement it yet - Trac #37232 is accepted and assigned but has no patch - so this change is written to be a no-op the moment Django ships it.

The change

Source: 40 insertions across 8 files.

File Change
compat.py Patch View.http_method_names, behind the same if not in guard already used for PATCH
permissions.py QUERY joins SAFE_METHODS; 'QUERY': [] in both perms_map dicts
routers.py 'query': 'query' on SimpleRouter's list route
decorators.py MethodMapper.query, so @action.mapping.query works
test.py .query() on APIRequestFactory and APIClient
schemas/openapi.py Request body for QUERY; operationId; array response for collection queries
schemas/utils.py is_list_view recognises QUERY on a collection
schemas/generators.py QUERY sorts immediately after GET

Nothing in the request path needed touching. Dispatch is already generic over http_method_names, and request.py _parse() keys off content_type, never the method - so request.data parses a QUERY body with no change at all. A view opts in by defining a query() handler:

class SearchNotes(APIView):
    def query(self, request):
        notes = Note.objects.filter(**request.data['filters'])
        return Response(NoteSerializer(notes, many=True).data)

Backwards compatibility

Three separate mechanisms mean existing code is untouched:

  1. compat.py's guard. if 'query' not in View.http_method_names - once Django ships QUERY natively the patch does nothing. Pinned by test_compat_patch_is_idempotent.
  2. get_method_map's hasattr check. A viewset without a query() method gets no QUERY route. This is the single most important guarantee in the PR, so it has its own test (test_query_is_not_bound_when_the_viewset_omits_it) alongside the positive case.
  3. View._allowed_methods(). Filters on hasattr, so no view advertises QUERY in Allow unless it handles it.

perms_map overrides written before QUERY existed have no 'QUERY' key, so a QUERY request against them raises MethodNotAllowed - it fails closed rather than being permitted by default. test_overridden_perms_map_without_query_is_method_not_allowed pins that, and the docs now name it.

The contentious line

-SAFE_METHODS = ('GET', 'HEAD', 'OPTIONS')
+SAFE_METHODS = ('GET', 'HEAD', 'OPTIONS', 'QUERY')

This changes IsAuthenticatedOrReadOnly, DjangoModelPermissionsOrAnonReadOnly, and the 403-vs-404 branch in DjangoObjectPermissions. It is correct per the RFC, but it makes a view's security posture depend on the handler author honouring "a QUERY handler must never write".

test_cannot_query_permissions_hides_the_object is written so it genuinely discriminates: with QUERY in SAFE_METHODS a denied QUERY 404s immediately; revert that one line and it 403s instead, and the test fails with 403 != 404. I checked that by actually reverting it, rather than assuming.

Django's own ticket lists "exclude from CSRF checks" as a requirement, so Django appears to be heading for the same treatment - which is the strongest argument for this line, and a reason to let Django land first.

Tests

407 lines, 30 new tests.

File Covers
test_views.py Dispatch to query(), function-based views, 405 without a handler, Allow header, compat-patch idempotence
test_request.py request.data for form and JSON QUERY bodies, UnsupportedMediaType negotiation
test_permissions.py perms_map, anonymous read-only access, the object-level 403-vs-404 branch, legacy perms_map overrides
test_routers.py Route bound only when query() exists, URL names unchanged, @action collision
test_viewsets.py as_view({'query': 'query'}) binds, self.action == 'query'
test_testing.py .query() on factory and client across formats
schemas/test_openapi.py Request body, array response schema, operationId not colliding with list, method ordering

One existing test changed: test_method_mapping_http_method maps every name in APIView.http_method_names onto http.HTTPMethod, which has no QUERY member. CPython gh-153309 / PR #155786 are open to add it, so the guard is temporary in the same way the compat.py patch is.

test_method_mapping_http_methods needed no change - it iterates http_method_names and asserts every name is mappable, so it fails on its own if MethodMapper.query is missing. The suite enforces that consistency without help.

CI

The full CI suite is green on this run: pre-commit, Python 3.10 through 3.14 against Django 5.2, 6.0, 6.1 and main, the base, dist and docs tox targets, and the documentation link check. 1630 passed, 1 skipped.

Also run locally across Python 3.12, 3.13 and 3.14 against the same four Django versions: 12 of 12 tox environments pass, plus base, dist and docs.

QUERY is safe and idempotent like GET, but carries a request body like
POST. Django does not implement it yet (Trac #37232), so `compat` patches
`View.http_method_names` behind the same `if not in` guard already used
for PATCH, which makes the patch a no-op once Django ships native support.

Dispatch is generic over `http_method_names`, so a view opts in by
defining a `query()` handler. `SimpleRouter` binds `query` on the list
route, and `get_method_map`'s `hasattr` check means viewsets without a
`query()` method get no route and never advertise QUERY in `Allow`.

QUERY joins `SAFE_METHODS` and maps to no required permissions in the
`perms_map` of `DjangoModelPermissions` and `DjangoObjectPermissions`.

Also adds `MethodMapper.query`, `APIRequestFactory.query`,
`APIClient.query`, and OpenAPI support (request body, array response
schema for collection queries, operationId, method ordering).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant