Allow setting the `response_class` to `RedirectResponse` and returning the URL from the function (#3457)

This commit is contained in:
Sebastián Ramírez 2021-07-03 21:51:28 +02:00 committed by GitHub
parent ea8d7f689e
commit dc5a966548
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 226 additions and 29 deletions

View File

@ -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. Returns an HTTP redirect. Uses a 307 status code (Temporary Redirect) by default.
You can return a `RedirectResponse` directly:
```Python hl_lines="2 9" ```Python hl_lines="2 9"
{!../../../docs_src/custom_response/tutorial006.py!} {!../../../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` ### `StreamingResponse`
Takes an async generator or a normal generator/iterator and streams the response body. 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!} {!../../../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 ## Default response class
When creating a **FastAPI** class instance or an `APIRouter` you can specify which response class to use by default. When creating a **FastAPI** class instance or an `APIRouter` you can specify which response class to use by default.

View File

@ -5,5 +5,5 @@ app = FastAPI()
@app.get("/typer") @app.get("/typer")
async def read_typer(): async def redirect_typer():
return RedirectResponse("https://typer.tiangolo.com") return RedirectResponse("https://typer.tiangolo.com")

View File

@ -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"

View File

@ -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/"

View File

@ -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

View File

@ -206,7 +206,7 @@ class FastAPI(Starlette):
endpoint: Callable[..., Coroutine[Any, Any, Response]], endpoint: Callable[..., Coroutine[Any, Any, Response]],
*, *,
response_model: Optional[Type[Any]] = None, response_model: Optional[Type[Any]] = None,
status_code: int = 200, status_code: Optional[int] = None,
tags: Optional[List[str]] = None, tags: Optional[List[str]] = None,
dependencies: Optional[Sequence[Depends]] = None, dependencies: Optional[Sequence[Depends]] = None,
summary: Optional[str] = None, summary: Optional[str] = None,
@ -258,7 +258,7 @@ class FastAPI(Starlette):
path: str, path: str,
*, *,
response_model: Optional[Type[Any]] = None, response_model: Optional[Type[Any]] = None,
status_code: int = 200, status_code: Optional[int] = None,
tags: Optional[List[str]] = None, tags: Optional[List[str]] = None,
dependencies: Optional[Sequence[Depends]] = None, dependencies: Optional[Sequence[Depends]] = None,
summary: Optional[str] = None, summary: Optional[str] = None,
@ -351,7 +351,7 @@ class FastAPI(Starlette):
path: str, path: str,
*, *,
response_model: Optional[Type[Any]] = None, response_model: Optional[Type[Any]] = None,
status_code: int = 200, status_code: Optional[int] = None,
tags: Optional[List[str]] = None, tags: Optional[List[str]] = None,
dependencies: Optional[Sequence[Depends]] = None, dependencies: Optional[Sequence[Depends]] = None,
summary: Optional[str] = None, summary: Optional[str] = None,
@ -400,7 +400,7 @@ class FastAPI(Starlette):
path: str, path: str,
*, *,
response_model: Optional[Type[Any]] = None, response_model: Optional[Type[Any]] = None,
status_code: int = 200, status_code: Optional[int] = None,
tags: Optional[List[str]] = None, tags: Optional[List[str]] = None,
dependencies: Optional[Sequence[Depends]] = None, dependencies: Optional[Sequence[Depends]] = None,
summary: Optional[str] = None, summary: Optional[str] = None,
@ -449,7 +449,7 @@ class FastAPI(Starlette):
path: str, path: str,
*, *,
response_model: Optional[Type[Any]] = None, response_model: Optional[Type[Any]] = None,
status_code: int = 200, status_code: Optional[int] = None,
tags: Optional[List[str]] = None, tags: Optional[List[str]] = None,
dependencies: Optional[Sequence[Depends]] = None, dependencies: Optional[Sequence[Depends]] = None,
summary: Optional[str] = None, summary: Optional[str] = None,
@ -498,7 +498,7 @@ class FastAPI(Starlette):
path: str, path: str,
*, *,
response_model: Optional[Type[Any]] = None, response_model: Optional[Type[Any]] = None,
status_code: int = 200, status_code: Optional[int] = None,
tags: Optional[List[str]] = None, tags: Optional[List[str]] = None,
dependencies: Optional[Sequence[Depends]] = None, dependencies: Optional[Sequence[Depends]] = None,
summary: Optional[str] = None, summary: Optional[str] = None,
@ -547,7 +547,7 @@ class FastAPI(Starlette):
path: str, path: str,
*, *,
response_model: Optional[Type[Any]] = None, response_model: Optional[Type[Any]] = None,
status_code: int = 200, status_code: Optional[int] = None,
tags: Optional[List[str]] = None, tags: Optional[List[str]] = None,
dependencies: Optional[Sequence[Depends]] = None, dependencies: Optional[Sequence[Depends]] = None,
summary: Optional[str] = None, summary: Optional[str] = None,
@ -596,7 +596,7 @@ class FastAPI(Starlette):
path: str, path: str,
*, *,
response_model: Optional[Type[Any]] = None, response_model: Optional[Type[Any]] = None,
status_code: int = 200, status_code: Optional[int] = None,
tags: Optional[List[str]] = None, tags: Optional[List[str]] = None,
dependencies: Optional[Sequence[Depends]] = None, dependencies: Optional[Sequence[Depends]] = None,
summary: Optional[str] = None, summary: Optional[str] = None,
@ -645,7 +645,7 @@ class FastAPI(Starlette):
path: str, path: str,
*, *,
response_model: Optional[Type[Any]] = None, response_model: Optional[Type[Any]] = None,
status_code: int = 200, status_code: Optional[int] = None,
tags: Optional[List[str]] = None, tags: Optional[List[str]] = None,
dependencies: Optional[Sequence[Depends]] = None, dependencies: Optional[Sequence[Depends]] = None,
summary: Optional[str] = None, summary: Optional[str] = None,
@ -694,7 +694,7 @@ class FastAPI(Starlette):
path: str, path: str,
*, *,
response_model: Optional[Type[Any]] = None, response_model: Optional[Type[Any]] = None,
status_code: int = 200, status_code: Optional[int] = None,
tags: Optional[List[str]] = None, tags: Optional[List[str]] = None,
dependencies: Optional[Sequence[Depends]] = None, dependencies: Optional[Sequence[Depends]] = None,
summary: Optional[str] = None, summary: Optional[str] = None,

View File

@ -1,4 +1,5 @@
import http.client import http.client
import inspect
from enum import Enum from enum import Enum
from typing import Any, Dict, List, Optional, Sequence, Set, Tuple, Type, Union, cast 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} callbacks[callback.name] = {callback.path: cb_path}
operation["callbacks"] = callbacks 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, {})[ operation.setdefault("responses", {}).setdefault(status_code, {})[
"description" "description"
] = route.response_description ] = route.response_description

View File

@ -154,7 +154,7 @@ async def run_endpoint_function(
def get_request_handler( def get_request_handler(
dependant: Dependant, dependant: Dependant,
body_field: Optional[ModelField] = None, body_field: Optional[ModelField] = None,
status_code: int = 200, status_code: Optional[int] = None,
response_class: Union[Type[Response], DefaultPlaceholder] = Default(JSONResponse), response_class: Union[Type[Response], DefaultPlaceholder] = Default(JSONResponse),
response_field: Optional[ModelField] = None, response_field: Optional[ModelField] = None,
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None, response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
@ -232,11 +232,12 @@ def get_request_handler(
exclude_none=response_model_exclude_none, exclude_none=response_model_exclude_none,
is_coroutine=is_coroutine, is_coroutine=is_coroutine,
) )
response = actual_response_class( response_args: Dict[str, Any] = {"background": background_tasks}
content=response_data, # If status_code was set, use it, otherwise use the default from the
status_code=status_code, # response class, in the case of redirect it's 307
background=background_tasks, # type: ignore # in Starlette 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) response.headers.raw.extend(sub_response.headers.raw)
if sub_response.status_code: if sub_response.status_code:
response.status_code = sub_response.status_code response.status_code = sub_response.status_code
@ -293,7 +294,7 @@ class APIRoute(routing.Route):
endpoint: Callable[..., Any], endpoint: Callable[..., Any],
*, *,
response_model: Optional[Type[Any]] = None, response_model: Optional[Type[Any]] = None,
status_code: int = 200, status_code: Optional[int] = None,
tags: Optional[List[str]] = None, tags: Optional[List[str]] = None,
dependencies: Optional[Sequence[params.Depends]] = None, dependencies: Optional[Sequence[params.Depends]] = None,
summary: Optional[str] = None, summary: Optional[str] = None,
@ -469,7 +470,7 @@ class APIRouter(routing.Router):
endpoint: Callable[..., Any], endpoint: Callable[..., Any],
*, *,
response_model: Optional[Type[Any]] = None, response_model: Optional[Type[Any]] = None,
status_code: int = 200, status_code: Optional[int] = None,
tags: Optional[List[str]] = None, tags: Optional[List[str]] = None,
dependencies: Optional[Sequence[params.Depends]] = None, dependencies: Optional[Sequence[params.Depends]] = None,
summary: Optional[str] = None, summary: Optional[str] = None,
@ -541,7 +542,7 @@ class APIRouter(routing.Router):
path: str, path: str,
*, *,
response_model: Optional[Type[Any]] = None, response_model: Optional[Type[Any]] = None,
status_code: int = 200, status_code: Optional[int] = None,
tags: Optional[List[str]] = None, tags: Optional[List[str]] = None,
dependencies: Optional[Sequence[params.Depends]] = None, dependencies: Optional[Sequence[params.Depends]] = None,
summary: Optional[str] = None, summary: Optional[str] = None,
@ -719,7 +720,7 @@ class APIRouter(routing.Router):
path: str, path: str,
*, *,
response_model: Optional[Type[Any]] = None, response_model: Optional[Type[Any]] = None,
status_code: int = 200, status_code: Optional[int] = None,
tags: Optional[List[str]] = None, tags: Optional[List[str]] = None,
dependencies: Optional[Sequence[params.Depends]] = None, dependencies: Optional[Sequence[params.Depends]] = None,
summary: Optional[str] = None, summary: Optional[str] = None,
@ -769,7 +770,7 @@ class APIRouter(routing.Router):
path: str, path: str,
*, *,
response_model: Optional[Type[Any]] = None, response_model: Optional[Type[Any]] = None,
status_code: int = 200, status_code: Optional[int] = None,
tags: Optional[List[str]] = None, tags: Optional[List[str]] = None,
dependencies: Optional[Sequence[params.Depends]] = None, dependencies: Optional[Sequence[params.Depends]] = None,
summary: Optional[str] = None, summary: Optional[str] = None,
@ -819,7 +820,7 @@ class APIRouter(routing.Router):
path: str, path: str,
*, *,
response_model: Optional[Type[Any]] = None, response_model: Optional[Type[Any]] = None,
status_code: int = 200, status_code: Optional[int] = None,
tags: Optional[List[str]] = None, tags: Optional[List[str]] = None,
dependencies: Optional[Sequence[params.Depends]] = None, dependencies: Optional[Sequence[params.Depends]] = None,
summary: Optional[str] = None, summary: Optional[str] = None,
@ -869,7 +870,7 @@ class APIRouter(routing.Router):
path: str, path: str,
*, *,
response_model: Optional[Type[Any]] = None, response_model: Optional[Type[Any]] = None,
status_code: int = 200, status_code: Optional[int] = None,
tags: Optional[List[str]] = None, tags: Optional[List[str]] = None,
dependencies: Optional[Sequence[params.Depends]] = None, dependencies: Optional[Sequence[params.Depends]] = None,
summary: Optional[str] = None, summary: Optional[str] = None,
@ -919,7 +920,7 @@ class APIRouter(routing.Router):
path: str, path: str,
*, *,
response_model: Optional[Type[Any]] = None, response_model: Optional[Type[Any]] = None,
status_code: int = 200, status_code: Optional[int] = None,
tags: Optional[List[str]] = None, tags: Optional[List[str]] = None,
dependencies: Optional[Sequence[params.Depends]] = None, dependencies: Optional[Sequence[params.Depends]] = None,
summary: Optional[str] = None, summary: Optional[str] = None,
@ -969,7 +970,7 @@ class APIRouter(routing.Router):
path: str, path: str,
*, *,
response_model: Optional[Type[Any]] = None, response_model: Optional[Type[Any]] = None,
status_code: int = 200, status_code: Optional[int] = None,
tags: Optional[List[str]] = None, tags: Optional[List[str]] = None,
dependencies: Optional[Sequence[params.Depends]] = None, dependencies: Optional[Sequence[params.Depends]] = None,
summary: Optional[str] = None, summary: Optional[str] = None,
@ -1019,7 +1020,7 @@ class APIRouter(routing.Router):
path: str, path: str,
*, *,
response_model: Optional[Type[Any]] = None, response_model: Optional[Type[Any]] = None,
status_code: int = 200, status_code: Optional[int] = None,
tags: Optional[List[str]] = None, tags: Optional[List[str]] = None,
dependencies: Optional[Sequence[params.Depends]] = None, dependencies: Optional[Sequence[params.Depends]] = None,
summary: Optional[str] = None, summary: Optional[str] = None,
@ -1069,7 +1070,7 @@ class APIRouter(routing.Router):
path: str, path: str,
*, *,
response_model: Optional[Type[Any]] = None, response_model: Optional[Type[Any]] = None,
status_code: int = 200, status_code: Optional[int] = None,
tags: Optional[List[str]] = None, tags: Optional[List[str]] = None,
dependencies: Optional[Sequence[params.Depends]] = None, dependencies: Optional[Sequence[params.Depends]] = None,
summary: Optional[str] = None, summary: Optional[str] = None,

View File

@ -5,6 +5,32 @@ from docs_src.custom_response.tutorial006 import app
client = TestClient(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(): def test_get():
response = client.get("/typer", allow_redirects=False) response = client.get("/typer", allow_redirects=False)
assert response.status_code == 307, response.text assert response.status_code == 307, response.text

View File

@ -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"

View File

@ -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/"

View File

@ -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

View File

@ -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