Browse Source

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

pull/3458/head
Sebastián Ramírez 4 years ago
committed by GitHub
parent
commit
dc5a966548
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 31
      docs/en/docs/advanced/custom-response.md
  2. 2
      docs_src/custom_response/tutorial006.py
  3. 9
      docs_src/custom_response/tutorial006b.py
  4. 9
      docs_src/custom_response/tutorial006c.py
  5. 10
      docs_src/custom_response/tutorial009b.py
  6. 20
      fastapi/applications.py
  7. 15
      fastapi/openapi/utils.py
  8. 35
      fastapi/routing.py
  9. 26
      tests/test_tutorial/test_custom_response/test_tutorial006.py
  10. 32
      tests/test_tutorial/test_custom_response/test_tutorial006b.py
  11. 32
      tests/test_tutorial/test_custom_response/test_tutorial006c.py
  12. 17
      tests/test_tutorial/test_custom_response/test_tutorial009.py
  13. 17
      tests/test_tutorial/test_custom_response/test_tutorial009b.py

31
docs/en/docs/advanced/custom-response.md

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

2
docs_src/custom_response/tutorial006.py

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

9
docs_src/custom_response/tutorial006b.py

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

9
docs_src/custom_response/tutorial006c.py

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

10
docs_src/custom_response/tutorial009b.py

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

20
fastapi/applications.py

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

15
fastapi/openapi/utils.py

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

35
fastapi/routing.py

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

26
tests/test_tutorial/test_custom_response/test_tutorial006.py

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

32
tests/test_tutorial/test_custom_response/test_tutorial006b.py

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

32
tests/test_tutorial/test_custom_response/test_tutorial006c.py

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

17
tests/test_tutorial/test_custom_response/test_tutorial009.py

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

17
tests/test_tutorial/test_custom_response/test_tutorial009b.py

@ -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…
Cancel
Save