]> git.ipfire.org Git - thirdparty/fastapi/fastapi.git/commitdiff
✨ Allow setting the `response_class` to `RedirectResponse` and returning the URL...
authorSebastián Ramírez <tiangolo@gmail.com>
Sat, 3 Jul 2021 19:51:28 +0000 (21:51 +0200)
committerGitHub <noreply@github.com>
Sat, 3 Jul 2021 19:51:28 +0000 (21:51 +0200)
13 files changed:
docs/en/docs/advanced/custom-response.md
docs_src/custom_response/tutorial006.py
docs_src/custom_response/tutorial006b.py [new file with mode: 0644]
docs_src/custom_response/tutorial006c.py [new file with mode: 0644]
docs_src/custom_response/tutorial009b.py [new file with mode: 0644]
fastapi/applications.py
fastapi/openapi/utils.py
fastapi/routing.py
tests/test_tutorial/test_custom_response/test_tutorial006.py
tests/test_tutorial/test_custom_response/test_tutorial006b.py [new file with mode: 0644]
tests/test_tutorial/test_custom_response/test_tutorial006c.py [new file with mode: 0644]
tests/test_tutorial/test_custom_response/test_tutorial009.py [new file with mode: 0644]
tests/test_tutorial/test_custom_response/test_tutorial009b.py [new file with mode: 0644]

index fa253305e2e60c474a647190539954c55b7a09bc..20b694ced8733e5cffa20ad7bd3b4e72175d3da0 100644 (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.
 
+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.
index 1c55568cac67f120687fc89ba866654b64c14861..332f8f87f1e53e7c108ca783f98ff67140452e1d 100644 (file)
@@ -5,5 +5,5 @@ app = FastAPI()
 
 
 @app.get("/typer")
-async def read_typer():
+async def redirect_typer():
     return RedirectResponse("https://typer.tiangolo.com")
diff --git a/docs_src/custom_response/tutorial006b.py b/docs_src/custom_response/tutorial006b.py
new file mode 100644 (file)
index 0000000..03a7be3
--- /dev/null
@@ -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"
diff --git a/docs_src/custom_response/tutorial006c.py b/docs_src/custom_response/tutorial006c.py
new file mode 100644 (file)
index 0000000..db87a93
--- /dev/null
@@ -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/"
diff --git a/docs_src/custom_response/tutorial009b.py b/docs_src/custom_response/tutorial009b.py
new file mode 100644 (file)
index 0000000..27200ee
--- /dev/null
@@ -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
index 92d041c5cf3f654925e07d8b8cdb1e74a523a737..3f78238d60f2fee84287f978d1fad1e5c0ca16df 100644 (file)
@@ -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,
index 6f749ef9c264a7a48b03ba0d0ab084f84412cce4..8dbe3902b10130ec0737cd4c4a2f089d90de3f63 100644 (file)
@@ -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
index 3cf35f5095659aef102acaf9c2815de47d05d614..a7c62af3cbb2b8a8c1699766959b2f600a876885 100644 (file)
@@ -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,
index 33a17ea46e569c8e6fd8b05d33b0640b801653e3..72bbfd277720921255f56f27ee3c423604e4cb52 100644 (file)
@@ -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
diff --git a/tests/test_tutorial/test_custom_response/test_tutorial006b.py b/tests/test_tutorial/test_custom_response/test_tutorial006b.py
new file mode 100644 (file)
index 0000000..ac5a76d
--- /dev/null
@@ -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"
diff --git a/tests/test_tutorial/test_custom_response/test_tutorial006c.py b/tests/test_tutorial/test_custom_response/test_tutorial006c.py
new file mode 100644 (file)
index 0000000..009225e
--- /dev/null
@@ -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/"
diff --git a/tests/test_tutorial/test_custom_response/test_tutorial009.py b/tests/test_tutorial/test_custom_response/test_tutorial009.py
new file mode 100644 (file)
index 0000000..ac20f89
--- /dev/null
@@ -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
diff --git a/tests/test_tutorial/test_custom_response/test_tutorial009b.py b/tests/test_tutorial/test_custom_response/test_tutorial009b.py
new file mode 100644 (file)
index 0000000..4f56e2f
--- /dev/null
@@ -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