From b12fe6b509797ca2ca958638eeb1fd66a3dbcc78 Mon Sep 17 00:00:00 2001 From: Gero Zayas Date: Thu, 9 Oct 2025 13:41:13 +0200 Subject: [PATCH 1/2] feat: add experimental HTTP QUERY method support - Add APIRouter.query() decorator for QUERY method (IETF draft) - Add FastAPI.query() passthrough method - Exclude QUERY routes from OpenAPI by default (spec doesn't support it) - Add test coverage for runtime execution and OpenAPI behavior - Add documentation with caveats and limitations --- .../docs/how-to/custom-request-and-route.md | 38 ++++++++++++ fastapi/applications.py | 11 ++++ fastapi/routing.py | 60 +++++++++++++++++++ tests/test_query_method.py | 27 +++++++++ 4 files changed, 136 insertions(+) create mode 100644 tests/test_query_method.py diff --git a/docs/en/docs/how-to/custom-request-and-route.md b/docs/en/docs/how-to/custom-request-and-route.md index 6df24a080..8ba216de5 100644 --- a/docs/en/docs/how-to/custom-request-and-route.md +++ b/docs/en/docs/how-to/custom-request-and-route.md @@ -107,3 +107,41 @@ You can also set the `route_class` parameter of an `APIRouter`: In this example, the *path operations* under the `router` will use the custom `TimedRoute` class, and will have an extra `X-Response-Time` header in the response with the time it took to generate the response: {* ../../docs_src/custom_request_and_route/tutorial003.py hl[13:20] *} + +## Experimental: HTTP QUERY method { #experimental-http-query-method } + +/// warning + +This is an experimental feature for the non-standard HTTP QUERY method. Use with caution. + +/// + +FastAPI and `APIRouter` expose a `.query()` decorator for the experimental HTTP QUERY method, as defined in the IETF draft for "safe method with body". + +The QUERY method works at runtime and can be used like any other HTTP method decorator: + +```python +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + +class SearchQuery(BaseModel): + text: str + limit: int = 10 + +@app.query("/search") +def search_items(query: SearchQuery): + return {"results": f"Searching for: {query.text}"} +``` + +### Limitations { #query-method-limitations } + +* **Not shown in interactive docs**: The QUERY method will not appear in the OpenAPI schema or interactive documentation (Swagger UI, ReDoc) because the OpenAPI specification does not define "query" operations. +* **Limited client support**: Not all HTTP clients and proxies support the QUERY method. + +/// tip + +For maximum interoperability, prefer using **POST** for operations that require a request body. The QUERY method is only useful in specialized scenarios where you need to follow the IETF draft specification. + +/// diff --git a/fastapi/applications.py b/fastapi/applications.py index 915f5f70a..e3739d6c5 100644 --- a/fastapi/applications.py +++ b/fastapi/applications.py @@ -4535,6 +4535,17 @@ class FastAPI(Starlette): generate_unique_id_function=generate_unique_id_function, ) + def query( + self, + path: str, + *args: Any, + **kwargs: Any, + ) -> Callable[[DecoratedCallable], DecoratedCallable]: + """ + Experimental: HTTP QUERY method. See APIRouter.query for caveats. + """ + return self.router.query(path, *args, **kwargs) + def websocket_route( self, path: str, name: Union[str, None] = None ) -> Callable[[DecoratedCallable], DecoratedCallable]: diff --git a/fastapi/routing.py b/fastapi/routing.py index 65f739d95..e08b5a682 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -4480,6 +4480,66 @@ class APIRouter(routing.Router): generate_unique_id_function=generate_unique_id_function, ) + def query( + self, + path: str, + *, + response_model: Any = Default(None), + status_code: Optional[int] = None, + tags: Optional[List[Union[str, Enum]]] = None, + dependencies: Optional[Sequence[params.Depends]] = None, + summary: Optional[str] = None, + description: Optional[str] = None, + response_description: str = "Successful Response", + responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, + deprecated: Optional[bool] = None, + operation_id: Optional[str] = None, + response_model_include: Optional[IncEx] = None, + response_model_exclude: Optional[IncEx] = None, + response_model_by_alias: bool = True, + response_model_exclude_unset: bool = False, + response_model_exclude_defaults: bool = False, + response_model_exclude_none: bool = False, + include_in_schema: bool = False, + response_class: Type[Response] = Default(JSONResponse), + name: Optional[str] = None, + callbacks: Optional[List[BaseRoute]] = None, + openapi_extra: Optional[Dict[str, Any]] = None, + generate_unique_id_function: Callable[[APIRoute], str] = Default( + generate_unique_id + ), + ) -> Callable[[DecoratedCallable], DecoratedCallable]: + """ + Experimental: register a handler for the non-standard HTTP QUERY method. + Works at runtime, but not represented in OpenAPI (spec doesn't support it). + """ + return self.api_route( + path=path, + response_model=response_model, + status_code=status_code, + tags=tags, + dependencies=dependencies, + summary=summary, + description=description, + response_description=response_description, + responses=responses, + deprecated=deprecated, + methods=["QUERY"], + operation_id=operation_id, + response_model_include=response_model_include, + response_model_exclude=response_model_exclude, + response_model_by_alias=response_model_by_alias, + response_model_exclude_unset=response_model_exclude_unset, + response_model_exclude_defaults=response_model_exclude_defaults, + response_model_exclude_none=response_model_exclude_none, + include_in_schema=include_in_schema, + response_class=response_class, + name=name, + callbacks=callbacks, + openapi_extra=openapi_extra, + generate_unique_id_function=generate_unique_id_function, + ) + @deprecated( """ on_event is deprecated, use lifespan event handlers instead. diff --git a/tests/test_query_method.py b/tests/test_query_method.py new file mode 100644 index 000000000..e83f7f3aa --- /dev/null +++ b/tests/test_query_method.py @@ -0,0 +1,27 @@ +from fastapi import FastAPI +from fastapi.testclient import TestClient +from pydantic import BaseModel + + +class Payload(BaseModel): + x: int + + +def test_query_route_executes_and_openapi_survives(): + app = FastAPI() + + @app.query("/items") + def query_items(payload: Payload): + return {"ok": payload.x} + + client = TestClient(app) + + # Runtime: the route is callable with QUERY + r = client.request("QUERY", "/items", json={"x": 42}) + assert r.status_code == 200 + assert r.json() == {"ok": 42} + + # OpenAPI: does not include the query route (excluded by default), and must not error + schema = app.openapi() + # The path is excluded from OpenAPI because include_in_schema=False by default + assert "/items" not in schema["paths"] From 1d8e5e022726bd9d98a804357453b8980d1cfe50 Mon Sep 17 00:00:00 2001 From: Gero Zayas Date: Thu, 9 Oct 2025 13:50:34 +0200 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=93=9D=20Add=20Spanish=20translation?= =?UTF-8?q?=20for=20HTTP=20QUERY=20method=20section?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Translate experimental HTTP QUERY method documentation - Add warning about non-standard HTTP method - Include code example and limitations section - Add tip about preferring POST for interoperability Brings Spanish docs up to date with English version in docs/en/docs/how-to/custom-request-and-route.md --- .../docs/how-to/custom-request-and-route.md | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/docs/es/docs/how-to/custom-request-and-route.md b/docs/es/docs/how-to/custom-request-and-route.md index 0b479bf00..bbf8b46c9 100644 --- a/docs/es/docs/how-to/custom-request-and-route.md +++ b/docs/es/docs/how-to/custom-request-and-route.md @@ -107,3 +107,41 @@ También puedes establecer el parámetro `route_class` de un `APIRouter`: En este ejemplo, las *path operations* bajo el `router` usarán la clase personalizada `TimedRoute`, y tendrán un header `X-Response-Time` extra en el response con el tiempo que tomó generar el response: {* ../../docs_src/custom_request_and_route/tutorial003.py hl[13:20] *} + +## Experimental: Método HTTP QUERY { #experimental-http-query-method } + +/// warning | Advertencia + +Esta es una funcionalidad experimental para el método HTTP QUERY no estándar. Úsalo con precaución. + +/// + +FastAPI y `APIRouter` exponen un decorador `.query()` para el método HTTP QUERY experimental, tal como se define en el borrador del IETF para "método seguro con cuerpo". + +El método QUERY funciona en tiempo de ejecución y puede usarse como cualquier otro decorador de método HTTP: + +```python +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + +class SearchQuery(BaseModel): + text: str + limit: int = 10 + +@app.query("/search") +def search_items(query: SearchQuery): + return {"results": f"Searching for: {query.text}"} +``` + +### Limitaciones { #query-method-limitations } + +* **No se muestra en la documentación interactiva**: El método QUERY no aparecerá en el esquema OpenAPI ni en la documentación interactiva (Swagger UI, ReDoc) porque la especificación OpenAPI no define operaciones "query". +* **Soporte limitado de clientes**: No todos los clientes HTTP y proxies soportan el método QUERY. + +/// tip | Consejo + +Para máxima interoperabilidad, prefiere usar **POST** para operaciones que requieren un cuerpo de request. El método QUERY solo es útil en escenarios especializados donde necesitas seguir la especificación del borrador del IETF. + +///