Python
Python handles QUERY out of the box — http.server dispatches any method via do_<METHOD>, httpx and requests accept arbitrary verbs, and web frameworks (FastAPI, Flask) route by string. No monkey-patching, no hacks.
Server: pure http.server (zero dependencies)
Section titled “Server: pure http.server (zero dependencies)”http.server calls do_QUERY() if the method exists on the handler. Works on any Python 3.8+, nothing to install.
"""HTTP QUERY server — stdlib only."""import jsonfrom http.server import HTTPServer, BaseHTTPRequestHandler
CONTACTS = [ {"name": "Ana", "city": "São Paulo", "role": "backend"}, {"name": "Bruno", "city": "Curitiba", "role": "frontend"}, {"name": "Carla", "city": "São Paulo", "role": "devops"}, {"name": "Daniel", "city": "Recife", "role": "backend"},]
class QueryHandler(BaseHTTPRequestHandler): def do_QUERY(self): # Validate Content-Type (RFC 10008 §2.1) content_type = self.headers.get("Content-Type", "") if "application/json" not in content_type: self._respond(415, {"error": "Content-Type must be application/json"}) return
# Read body length = int(self.headers.get("Content-Length", 0)) body = json.loads(self.rfile.read(length)) if length else {}
# Filter data filters = body.get("filter", {}) results = [ c for c in CONTACTS if all(c.get(k) == v for k, v in filters.items()) ]
# Apply limit limit = body.get("limit", len(results)) results = results[:limit]
self._respond(200, results)
def _respond(self, status, data): body = json.dumps(data, ensure_ascii=False).encode() self.send_response(status) self.send_header("Content-Type", "application/json") self.send_header("Content-Length", str(len(body))) # RFC 10008 §3 — advertise accepted query formats self.send_header("Accept-Query", "application/json") self.end_headers() self.wfile.write(body)
if __name__ == "__main__": server = HTTPServer(("localhost", 8000), QueryHandler) print("QUERY server running at http://localhost:8000") server.serve_forever()Why it works
Section titled “Why it works”BaseHTTPRequestHandler uses dynamic dispatch: any do_XYZ method responds to the XYZ HTTP verb. No registration needed — Python resolves it via introspection. This has actually worked since Python 2.
Tip: Add a
do_OPTIONSreturningAllow: GET, QUERY, OPTIONSfor full spec compliance.
Client: httpx
Section titled “Client: httpx”httpx accepts any method via client.request(). It’s Python’s modern HTTP client — async, HTTP/2, typed.
"""QUERY client with httpx."""import httpx
query = { "filter": {"city": "São Paulo"}, "limit": 10,}
with httpx.Client() as client: resp = client.request( "QUERY", "http://localhost:8000/contacts", json=query, ) print(resp.status_code) # 200 print(resp.json()) # [{"name": "Ana", ...}, {"name": "Carla", ...}]Async version
Section titled “Async version”import httpximport asyncio
async def search_contacts(): async with httpx.AsyncClient() as client: resp = await client.request( "QUERY", "http://localhost:8000/contacts", json={"filter": {"role": "backend"}, "limit": 5}, ) return resp.json()
results = asyncio.run(search_contacts())Why request() and not a dedicated method
Section titled “Why request() and not a dedicated method”httpx has shortcuts for GET, POST, PUT, DELETE — but not QUERY (yet). The request() method is the generic entry point: first argument is the verb string. No restrictions.
FastAPI
Section titled “FastAPI”FastAPI doesn’t have a @app.query() decorator, but add_api_route() accepts any method in the methods list. Straightforward:
"""FastAPI with QUERY route."""from fastapi import FastAPI, Requestfrom fastapi.responses import JSONResponse
app = FastAPI()
PRODUCTS = [ {"id": 1, "name": "Keyboard", "category": "peripherals", "price": 89}, {"id": 2, "name": "Monitor", "category": "displays", "price": 599}, {"id": 3, "name": "Mouse", "category": "peripherals", "price": 49}, {"id": 4, "name": "SSD", "category": "storage", "price": 129},]
async def search_products(request: Request): body = await request.json() filters = body.get("filter", {}) limit = body.get("limit", 50)
results = [ p for p in PRODUCTS if all(p.get(k) == v for k, v in filters.items()) ] return JSONResponse( content=results[:limit], headers={"Accept-Query": "application/json"}, )
app.add_api_route( "/products", search_products, methods=["QUERY"],)Running
Section titled “Running”uvicorn server:app --reloadWhy add_api_route and not a decorator
Section titled “Why add_api_route and not a decorator”FastAPI only generates decorators for standard HTTP methods (@app.get, @app.post, etc.). For custom methods, add_api_route() is the official path — it accepts any string in methods. Starlette underneath doesn’t validate the verb.
Note: The generated OpenAPI schema may not include the QUERY route in Swagger UI until the OpenAPI spec formally recognizes the method.
Flask accepts any string in the methods list of @app.route(). That simple:
"""Flask with QUERY route."""from flask import Flask, request, jsonify
app = Flask(__name__)
USERS = [ {"id": 1, "name": "Ana", "role": "admin", "active": True}, {"id": 2, "name": "Bruno", "role": "editor", "active": True}, {"id": 3, "name": "Carla", "role": "admin", "active": False}, {"id": 4, "name": "Daniel","role": "viewer", "active": True},]
@app.route("/users", methods=["QUERY"])def search_users(): body = request.get_json() filters = body.get("filter", {}) limit = body.get("limit", 100)
results = [ u for u in USERS if all(u.get(k) == v for k, v in filters.items()) ]
resp = jsonify(results[:limit]) resp.headers["Accept-Query"] = "application/json" return respRunning
Section titled “Running”flask --app server runWhy it works
Section titled “Why it works”Werkzeug (Flask’s WSGI layer) doesn’t restrict HTTP methods — any string is routed normally. Flask just needs it declared in the methods list. No extensions, no hacks.
requests
Section titled “requests”The requests library accepts arbitrary methods via requests.request():
"""QUERY client with requests."""import requests
resp = requests.request( "QUERY", "http://localhost:8000/contacts", json={"filter": {"city": "Recife"}, "limit": 5},)
print(resp.status_code) # 200print(resp.json()) # [{"name": "Daniel", ...}]With session (connection reuse)
Section titled “With session (connection reuse)”import requests
session = requests.Session()session.headers.update({ "Accept": "application/json",})
resp1 = session.request("QUERY", "http://localhost:8000/contacts", json={"filter": {"role": "backend"}})resp2 = session.request("QUERY", "http://localhost:8000/contacts", json={"filter": {"city": "Curitiba"}})Why not requests.query()
Section titled “Why not requests.query()”requests doesn’t have a QUERY helper — and probably won’t anytime soon. requests.request() is the generic API that takes any verb. Works the same as httpx.Client().request().
Testing with curl
Section titled “Testing with curl”curl accepts any method via -X. Works with all the servers above:
curl -X QUERY http://localhost:8000/contacts \ -H "Content-Type: application/json" \ -d '{"filter": {"city": "São Paulo"}, "limit": 10}'curl -s -X QUERY http://localhost:8000/contacts \ -H "Content-Type: application/json" \ -d '{"filter": {"role": "backend"}}' | jq .curl -i -X QUERY http://localhost:8000/contacts \ -H "Content-Type: application/json" \ -d '{"filter": {}}'curl -i -X QUERY http://localhost:8000/contacts \ -H "Content-Type: text/plain" \ -d 'city=São Paulo'Useful flags
Section titled “Useful flags”| Flag | What it does |
|---|---|
-X QUERY |
Sets the HTTP method |
-H "Content-Type: ..." |
Required header (RFC 10008 §2.1) |
-d '{...}' |
Request body |
-i |
Shows response headers |
-s |
Silences progress bar (useful with jq) |
-v |
Verbose mode — shows the full request on the wire |
Quick reference
Section titled “Quick reference”| Tool | How to use QUERY |
|---|---|
| http.server | do_QUERY(self) on the handler |
| httpx | client.request("QUERY", url, json=...) |
| FastAPI | app.add_api_route(path, fn, methods=["QUERY"]) |
| Flask | @app.route(path, methods=["QUERY"]) |
| requests | requests.request("QUERY", url, json=...) |
| curl | curl -X QUERY url -H "Content-Type: ..." -d '...' |
Every Python library already supports QUERY — because none of them restrict which HTTP verb you can use. RFC 10008 just standardized what was technically possible all along.