Add support for the HTTP QUERY method (RFC 10008) - #2
Open
BenA-SA wants to merge 1 commit into
Open
Conversation
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).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds support for the
QUERYmethod defined by RFC 10008: safe and idempotent likeGET, but carrying a request body likePOST.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.
compat.pyView.http_method_names, behind the sameif not inguard already used forPATCHpermissions.pyQUERYjoinsSAFE_METHODS;'QUERY': []in bothperms_mapdictsrouters.py'query': 'query'onSimpleRouter's list routedecorators.pyMethodMapper.query, so@action.mapping.queryworkstest.py.query()onAPIRequestFactoryandAPIClientschemas/openapi.pyQUERY;operationId; array response for collection queriesschemas/utils.pyis_list_viewrecognisesQUERYon a collectionschemas/generators.pyQUERYsorts immediately afterGETNothing in the request path needed touching. Dispatch is already generic over
http_method_names, andrequest.py_parse()keys offcontent_type, never the method - sorequest.dataparses aQUERYbody with no change at all. A view opts in by defining aquery()handler:Backwards compatibility
Three separate mechanisms mean existing code is untouched:
compat.py's guard.if 'query' not in View.http_method_names- once Django shipsQUERYnatively the patch does nothing. Pinned bytest_compat_patch_is_idempotent.get_method_map'shasattrcheck. A viewset without aquery()method gets noQUERYroute. 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.View._allowed_methods(). Filters onhasattr, so no view advertisesQUERYinAllowunless it handles it.perms_mapoverrides written beforeQUERYexisted have no'QUERY'key, so aQUERYrequest against them raisesMethodNotAllowed- it fails closed rather than being permitted by default.test_overridden_perms_map_without_query_is_method_not_allowedpins that, and the docs now name it.The contentious line
This changes
IsAuthenticatedOrReadOnly,DjangoModelPermissionsOrAnonReadOnly, and the 403-vs-404 branch inDjangoObjectPermissions. It is correct per the RFC, but it makes a view's security posture depend on the handler author honouring "aQUERYhandler must never write".test_cannot_query_permissions_hides_the_objectis written so it genuinely discriminates: withQUERYinSAFE_METHODSa deniedQUERY404s immediately; revert that one line and it 403s instead, and the test fails with403 != 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.
test_views.pyquery(), function-based views, 405 without a handler,Allowheader, compat-patch idempotencetest_request.pyrequest.datafor form and JSONQUERYbodies,UnsupportedMediaTypenegotiationtest_permissions.pyperms_map, anonymous read-only access, the object-level 403-vs-404 branch, legacyperms_mapoverridestest_routers.pyquery()exists, URL names unchanged,@actioncollisiontest_viewsets.pyas_view({'query': 'query'})binds,self.action == 'query'test_testing.py.query()on factory and client across formatsschemas/test_openapi.pyoperationIdnot colliding withlist, method orderingOne existing test changed:
test_method_mapping_http_methodmaps every name inAPIView.http_method_namesontohttp.HTTPMethod, which has noQUERYmember. CPython gh-153309 / PR #155786 are open to add it, so the guard is temporary in the same way thecompat.pypatch is.test_method_mapping_http_methodsneeded no change - it iterateshttp_method_namesand asserts every name is mappable, so it fails on its own ifMethodMapper.queryis 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, thebase,distanddocstox 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,distanddocs.