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 884c8ed04..5c283fe9d 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/docs/es/docs/how-to/custom-request-and-route.md b/docs/es/docs/how-to/custom-request-and-route.md index a6ea657d1..896a913a5 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. + +/// diff --git a/fastapi/applications.py b/fastapi/applications.py index 02193312b..49d393cef 100644 --- a/fastapi/applications.py +++ b/fastapi/applications.py @@ -4541,6 +4541,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 9be2b44bc..59065a112 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -4538,6 +4538,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"]