mirror of https://github.com/tiangolo/fastapi.git
✨ Allow setting the `response_class` to `RedirectResponse` and returning the URL from the function (#3457)
This commit is contained in:
parent
ea8d7f689e
commit
dc5a966548
|
|
@ -161,10 +161,33 @@ An alternative JSON response using <a href="https://github.com/ultrajson/ultrajs
|
|||
|
||||
Returns an HTTP redirect. Uses a 307 status code (Temporary Redirect) by default.
|
||||
|
||||
You can return a `RedirectResponse` directly:
|
||||
|
||||
```Python hl_lines="2 9"
|
||||
{!../../../docs_src/custom_response/tutorial006.py!}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Or you can use it in the `response_class` parameter:
|
||||
|
||||
|
||||
```Python hl_lines="2 7 9"
|
||||
{!../../../docs_src/custom_response/tutorial006b.py!}
|
||||
```
|
||||
|
||||
If you do that, then you can return the URL directly from your *path operation* function.
|
||||
|
||||
In this case, the `status_code` used will be the default one for the `RedirectResponse`, which is `307`.
|
||||
|
||||
---
|
||||
|
||||
You can also use the `status_code` parameter combined with the `response_class` parameter:
|
||||
|
||||
```Python hl_lines="2 7 9"
|
||||
{!../../../docs_src/custom_response/tutorial006c.py!}
|
||||
```
|
||||
|
||||
### `StreamingResponse`
|
||||
|
||||
Takes an async generator or a normal generator/iterator and streams the response body.
|
||||
|
|
@ -203,6 +226,14 @@ File responses will include appropriate `Content-Length`, `Last-Modified` and `E
|
|||
{!../../../docs_src/custom_response/tutorial009.py!}
|
||||
```
|
||||
|
||||
You can also use the `response_class` parameter:
|
||||
|
||||
```Python hl_lines="2 8 10"
|
||||
{!../../../docs_src/custom_response/tutorial009b.py!}
|
||||
```
|
||||
|
||||
In this case, you can return the file path directly from your *path operation* function.
|
||||
|
||||
## Default response class
|
||||
|
||||
When creating a **FastAPI** class instance or an `APIRouter` you can specify which response class to use by default.
|
||||
|
|
|
|||
|
|
@ -5,5 +5,5 @@ app = FastAPI()
|
|||
|
||||
|
||||
@app.get("/typer")
|
||||
async def read_typer():
|
||||
async def redirect_typer():
|
||||
return RedirectResponse("https://typer.tiangolo.com")
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
from fastapi import FastAPI
|
||||
from fastapi.responses import RedirectResponse
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.get("/fastapi", response_class=RedirectResponse)
|
||||
async def redirect_fastapi():
|
||||
return "https://fastapi.tiangolo.com"
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
from fastapi import FastAPI
|
||||
from fastapi.responses import RedirectResponse
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.get("/pydantic", response_class=RedirectResponse, status_code=302)
|
||||
async def redirect_pydantic():
|
||||
return "https://pydantic-docs.helpmanual.io/"
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
from fastapi import FastAPI
|
||||
from fastapi.responses import FileResponse
|
||||
|
||||
some_file_path = "large-video-file.mp4"
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.get("/", response_class=FileResponse)
|
||||
async def main():
|
||||
return some_file_path
|
||||
|
|
@ -206,7 +206,7 @@ class FastAPI(Starlette):
|
|||
endpoint: Callable[..., Coroutine[Any, Any, Response]],
|
||||
*,
|
||||
response_model: Optional[Type[Any]] = None,
|
||||
status_code: int = 200,
|
||||
status_code: Optional[int] = None,
|
||||
tags: Optional[List[str]] = None,
|
||||
dependencies: Optional[Sequence[Depends]] = None,
|
||||
summary: Optional[str] = None,
|
||||
|
|
@ -258,7 +258,7 @@ class FastAPI(Starlette):
|
|||
path: str,
|
||||
*,
|
||||
response_model: Optional[Type[Any]] = None,
|
||||
status_code: int = 200,
|
||||
status_code: Optional[int] = None,
|
||||
tags: Optional[List[str]] = None,
|
||||
dependencies: Optional[Sequence[Depends]] = None,
|
||||
summary: Optional[str] = None,
|
||||
|
|
@ -351,7 +351,7 @@ class FastAPI(Starlette):
|
|||
path: str,
|
||||
*,
|
||||
response_model: Optional[Type[Any]] = None,
|
||||
status_code: int = 200,
|
||||
status_code: Optional[int] = None,
|
||||
tags: Optional[List[str]] = None,
|
||||
dependencies: Optional[Sequence[Depends]] = None,
|
||||
summary: Optional[str] = None,
|
||||
|
|
@ -400,7 +400,7 @@ class FastAPI(Starlette):
|
|||
path: str,
|
||||
*,
|
||||
response_model: Optional[Type[Any]] = None,
|
||||
status_code: int = 200,
|
||||
status_code: Optional[int] = None,
|
||||
tags: Optional[List[str]] = None,
|
||||
dependencies: Optional[Sequence[Depends]] = None,
|
||||
summary: Optional[str] = None,
|
||||
|
|
@ -449,7 +449,7 @@ class FastAPI(Starlette):
|
|||
path: str,
|
||||
*,
|
||||
response_model: Optional[Type[Any]] = None,
|
||||
status_code: int = 200,
|
||||
status_code: Optional[int] = None,
|
||||
tags: Optional[List[str]] = None,
|
||||
dependencies: Optional[Sequence[Depends]] = None,
|
||||
summary: Optional[str] = None,
|
||||
|
|
@ -498,7 +498,7 @@ class FastAPI(Starlette):
|
|||
path: str,
|
||||
*,
|
||||
response_model: Optional[Type[Any]] = None,
|
||||
status_code: int = 200,
|
||||
status_code: Optional[int] = None,
|
||||
tags: Optional[List[str]] = None,
|
||||
dependencies: Optional[Sequence[Depends]] = None,
|
||||
summary: Optional[str] = None,
|
||||
|
|
@ -547,7 +547,7 @@ class FastAPI(Starlette):
|
|||
path: str,
|
||||
*,
|
||||
response_model: Optional[Type[Any]] = None,
|
||||
status_code: int = 200,
|
||||
status_code: Optional[int] = None,
|
||||
tags: Optional[List[str]] = None,
|
||||
dependencies: Optional[Sequence[Depends]] = None,
|
||||
summary: Optional[str] = None,
|
||||
|
|
@ -596,7 +596,7 @@ class FastAPI(Starlette):
|
|||
path: str,
|
||||
*,
|
||||
response_model: Optional[Type[Any]] = None,
|
||||
status_code: int = 200,
|
||||
status_code: Optional[int] = None,
|
||||
tags: Optional[List[str]] = None,
|
||||
dependencies: Optional[Sequence[Depends]] = None,
|
||||
summary: Optional[str] = None,
|
||||
|
|
@ -645,7 +645,7 @@ class FastAPI(Starlette):
|
|||
path: str,
|
||||
*,
|
||||
response_model: Optional[Type[Any]] = None,
|
||||
status_code: int = 200,
|
||||
status_code: Optional[int] = None,
|
||||
tags: Optional[List[str]] = None,
|
||||
dependencies: Optional[Sequence[Depends]] = None,
|
||||
summary: Optional[str] = None,
|
||||
|
|
@ -694,7 +694,7 @@ class FastAPI(Starlette):
|
|||
path: str,
|
||||
*,
|
||||
response_model: Optional[Type[Any]] = None,
|
||||
status_code: int = 200,
|
||||
status_code: Optional[int] = None,
|
||||
tags: Optional[List[str]] = None,
|
||||
dependencies: Optional[Sequence[Depends]] = None,
|
||||
summary: Optional[str] = None,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import http.client
|
||||
import inspect
|
||||
from enum import Enum
|
||||
from typing import Any, Dict, List, Optional, Sequence, Set, Tuple, Type, Union, cast
|
||||
|
||||
|
|
@ -218,7 +219,19 @@ def get_openapi_path(
|
|||
)
|
||||
callbacks[callback.name] = {callback.path: cb_path}
|
||||
operation["callbacks"] = callbacks
|
||||
status_code = str(route.status_code)
|
||||
if route.status_code is not None:
|
||||
status_code = str(route.status_code)
|
||||
else:
|
||||
# It would probably make more sense for all response classes to have an
|
||||
# explicit default status_code, and to extract it from them, instead of
|
||||
# doing this inspection tricks, that would probably be in the future
|
||||
# TODO: probably make status_code a default class attribute for all
|
||||
# responses in Starlette
|
||||
response_signature = inspect.signature(current_response_class.__init__)
|
||||
status_code_param = response_signature.parameters.get("status_code")
|
||||
if status_code_param is not None:
|
||||
if isinstance(status_code_param.default, int):
|
||||
status_code = str(status_code_param.default)
|
||||
operation.setdefault("responses", {}).setdefault(status_code, {})[
|
||||
"description"
|
||||
] = route.response_description
|
||||
|
|
|
|||
|
|
@ -154,7 +154,7 @@ async def run_endpoint_function(
|
|||
def get_request_handler(
|
||||
dependant: Dependant,
|
||||
body_field: Optional[ModelField] = None,
|
||||
status_code: int = 200,
|
||||
status_code: Optional[int] = None,
|
||||
response_class: Union[Type[Response], DefaultPlaceholder] = Default(JSONResponse),
|
||||
response_field: Optional[ModelField] = None,
|
||||
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
|
|
@ -232,11 +232,12 @@ def get_request_handler(
|
|||
exclude_none=response_model_exclude_none,
|
||||
is_coroutine=is_coroutine,
|
||||
)
|
||||
response = actual_response_class(
|
||||
content=response_data,
|
||||
status_code=status_code,
|
||||
background=background_tasks, # type: ignore # in Starlette
|
||||
)
|
||||
response_args: Dict[str, Any] = {"background": background_tasks}
|
||||
# If status_code was set, use it, otherwise use the default from the
|
||||
# response class, in the case of redirect it's 307
|
||||
if status_code is not None:
|
||||
response_args["status_code"] = status_code
|
||||
response = actual_response_class(response_data, **response_args)
|
||||
response.headers.raw.extend(sub_response.headers.raw)
|
||||
if sub_response.status_code:
|
||||
response.status_code = sub_response.status_code
|
||||
|
|
@ -293,7 +294,7 @@ class APIRoute(routing.Route):
|
|||
endpoint: Callable[..., Any],
|
||||
*,
|
||||
response_model: Optional[Type[Any]] = None,
|
||||
status_code: int = 200,
|
||||
status_code: Optional[int] = None,
|
||||
tags: Optional[List[str]] = None,
|
||||
dependencies: Optional[Sequence[params.Depends]] = None,
|
||||
summary: Optional[str] = None,
|
||||
|
|
@ -469,7 +470,7 @@ class APIRouter(routing.Router):
|
|||
endpoint: Callable[..., Any],
|
||||
*,
|
||||
response_model: Optional[Type[Any]] = None,
|
||||
status_code: int = 200,
|
||||
status_code: Optional[int] = None,
|
||||
tags: Optional[List[str]] = None,
|
||||
dependencies: Optional[Sequence[params.Depends]] = None,
|
||||
summary: Optional[str] = None,
|
||||
|
|
@ -541,7 +542,7 @@ class APIRouter(routing.Router):
|
|||
path: str,
|
||||
*,
|
||||
response_model: Optional[Type[Any]] = None,
|
||||
status_code: int = 200,
|
||||
status_code: Optional[int] = None,
|
||||
tags: Optional[List[str]] = None,
|
||||
dependencies: Optional[Sequence[params.Depends]] = None,
|
||||
summary: Optional[str] = None,
|
||||
|
|
@ -719,7 +720,7 @@ class APIRouter(routing.Router):
|
|||
path: str,
|
||||
*,
|
||||
response_model: Optional[Type[Any]] = None,
|
||||
status_code: int = 200,
|
||||
status_code: Optional[int] = None,
|
||||
tags: Optional[List[str]] = None,
|
||||
dependencies: Optional[Sequence[params.Depends]] = None,
|
||||
summary: Optional[str] = None,
|
||||
|
|
@ -769,7 +770,7 @@ class APIRouter(routing.Router):
|
|||
path: str,
|
||||
*,
|
||||
response_model: Optional[Type[Any]] = None,
|
||||
status_code: int = 200,
|
||||
status_code: Optional[int] = None,
|
||||
tags: Optional[List[str]] = None,
|
||||
dependencies: Optional[Sequence[params.Depends]] = None,
|
||||
summary: Optional[str] = None,
|
||||
|
|
@ -819,7 +820,7 @@ class APIRouter(routing.Router):
|
|||
path: str,
|
||||
*,
|
||||
response_model: Optional[Type[Any]] = None,
|
||||
status_code: int = 200,
|
||||
status_code: Optional[int] = None,
|
||||
tags: Optional[List[str]] = None,
|
||||
dependencies: Optional[Sequence[params.Depends]] = None,
|
||||
summary: Optional[str] = None,
|
||||
|
|
@ -869,7 +870,7 @@ class APIRouter(routing.Router):
|
|||
path: str,
|
||||
*,
|
||||
response_model: Optional[Type[Any]] = None,
|
||||
status_code: int = 200,
|
||||
status_code: Optional[int] = None,
|
||||
tags: Optional[List[str]] = None,
|
||||
dependencies: Optional[Sequence[params.Depends]] = None,
|
||||
summary: Optional[str] = None,
|
||||
|
|
@ -919,7 +920,7 @@ class APIRouter(routing.Router):
|
|||
path: str,
|
||||
*,
|
||||
response_model: Optional[Type[Any]] = None,
|
||||
status_code: int = 200,
|
||||
status_code: Optional[int] = None,
|
||||
tags: Optional[List[str]] = None,
|
||||
dependencies: Optional[Sequence[params.Depends]] = None,
|
||||
summary: Optional[str] = None,
|
||||
|
|
@ -969,7 +970,7 @@ class APIRouter(routing.Router):
|
|||
path: str,
|
||||
*,
|
||||
response_model: Optional[Type[Any]] = None,
|
||||
status_code: int = 200,
|
||||
status_code: Optional[int] = None,
|
||||
tags: Optional[List[str]] = None,
|
||||
dependencies: Optional[Sequence[params.Depends]] = None,
|
||||
summary: Optional[str] = None,
|
||||
|
|
@ -1019,7 +1020,7 @@ class APIRouter(routing.Router):
|
|||
path: str,
|
||||
*,
|
||||
response_model: Optional[Type[Any]] = None,
|
||||
status_code: int = 200,
|
||||
status_code: Optional[int] = None,
|
||||
tags: Optional[List[str]] = None,
|
||||
dependencies: Optional[Sequence[params.Depends]] = None,
|
||||
summary: Optional[str] = None,
|
||||
|
|
@ -1069,7 +1070,7 @@ class APIRouter(routing.Router):
|
|||
path: str,
|
||||
*,
|
||||
response_model: Optional[Type[Any]] = None,
|
||||
status_code: int = 200,
|
||||
status_code: Optional[int] = None,
|
||||
tags: Optional[List[str]] = None,
|
||||
dependencies: Optional[Sequence[params.Depends]] = None,
|
||||
summary: Optional[str] = None,
|
||||
|
|
|
|||
|
|
@ -5,6 +5,32 @@ from docs_src.custom_response.tutorial006 import app
|
|||
client = TestClient(app)
|
||||
|
||||
|
||||
openapi_schema = {
|
||||
"openapi": "3.0.2",
|
||||
"info": {"title": "FastAPI", "version": "0.1.0"},
|
||||
"paths": {
|
||||
"/typer": {
|
||||
"get": {
|
||||
"summary": "Redirect Typer",
|
||||
"operationId": "redirect_typer_typer_get",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == openapi_schema
|
||||
|
||||
|
||||
def test_get():
|
||||
response = client.get("/typer", allow_redirects=False)
|
||||
assert response.status_code == 307, response.text
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
from fastapi.testclient import TestClient
|
||||
|
||||
from docs_src.custom_response.tutorial006b import app
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
openapi_schema = {
|
||||
"openapi": "3.0.2",
|
||||
"info": {"title": "FastAPI", "version": "0.1.0"},
|
||||
"paths": {
|
||||
"/fastapi": {
|
||||
"get": {
|
||||
"summary": "Redirect Fastapi",
|
||||
"operationId": "redirect_fastapi_fastapi_get",
|
||||
"responses": {"307": {"description": "Successful Response"}},
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == openapi_schema
|
||||
|
||||
|
||||
def test_redirect_response_class():
|
||||
response = client.get("/fastapi", allow_redirects=False)
|
||||
assert response.status_code == 307
|
||||
assert response.headers["location"] == "https://fastapi.tiangolo.com"
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
from fastapi.testclient import TestClient
|
||||
|
||||
from docs_src.custom_response.tutorial006c import app
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
openapi_schema = {
|
||||
"openapi": "3.0.2",
|
||||
"info": {"title": "FastAPI", "version": "0.1.0"},
|
||||
"paths": {
|
||||
"/pydantic": {
|
||||
"get": {
|
||||
"summary": "Redirect Pydantic",
|
||||
"operationId": "redirect_pydantic_pydantic_get",
|
||||
"responses": {"302": {"description": "Successful Response"}},
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == openapi_schema
|
||||
|
||||
|
||||
def test_redirect_status_code():
|
||||
response = client.get("/pydantic", allow_redirects=False)
|
||||
assert response.status_code == 302
|
||||
assert response.headers["location"] == "https://pydantic-docs.helpmanual.io/"
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
from pathlib import Path
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from docs_src.custom_response import tutorial009
|
||||
from docs_src.custom_response.tutorial009 import app
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
def test_get(tmp_path: Path):
|
||||
file_path: Path = tmp_path / "large-video-file.mp4"
|
||||
tutorial009.some_file_path = str(file_path)
|
||||
test_content = b"Fake video bytes"
|
||||
file_path.write_bytes(test_content)
|
||||
response = client.get("/")
|
||||
assert response.content == test_content
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
from pathlib import Path
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from docs_src.custom_response import tutorial009b
|
||||
from docs_src.custom_response.tutorial009b import app
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
def test_get(tmp_path: Path):
|
||||
file_path: Path = tmp_path / "large-video-file.mp4"
|
||||
tutorial009b.some_file_path = str(file_path)
|
||||
test_content = b"Fake video bytes"
|
||||
file_path.write_bytes(test_content)
|
||||
response = client.get("/")
|
||||
assert response.content == test_content
|
||||
Loading…
Reference in New Issue