]> git.ipfire.org Git - thirdparty/fastapi/fastapi.git/commitdiff
:sparkles: Add support for Response parameters to set headers, cookies, and status...
authorSebastián Ramírez <tiangolo@gmail.com>
Thu, 6 Jun 2019 10:29:40 +0000 (14:29 +0400)
committerGitHub <noreply@github.com>
Thu, 6 Jun 2019 10:29:40 +0000 (14:29 +0400)
* :sparkles: Add support for declaring a Response parameter to set headers and cookies

* :white_check_mark: Add source for docs and tests

* :memo: Add docs for setting headers, cookies and status code

* :memo: Add attribution to Hug for inspiring response parameters

17 files changed:
docs/alternatives.md
docs/src/response_change_status_code/tutorial001.py [new file with mode: 0644]
docs/src/response_cookies/tutorial002.py [new file with mode: 0644]
docs/src/response_headers/tutorial002.py [new file with mode: 0644]
docs/tutorial/response-change-status-code.md [new file with mode: 0644]
docs/tutorial/response-cookies.md
docs/tutorial/response-headers.md
docs/tutorial/response-status-code.md
fastapi/dependencies/models.py
fastapi/dependencies/utils.py
fastapi/routing.py
mkdocs.yml
tests/test_response_change_status_code.py [new file with mode: 0644]
tests/test_tutorial/test_response_change_status_code/__init__.py [new file with mode: 0644]
tests/test_tutorial/test_response_change_status_code/test_tutorial001.py [new file with mode: 0644]
tests/test_tutorial/test_response_cookies/test_tutorial002.py [new file with mode: 0644]
tests/test_tutorial/test_response_headers/test_tutorial002.py [new file with mode: 0644]

index b5f4abeaab61b03ebd68bd6317acdce6970ab742..1e873bb56cc43a5d5067a777760de35fe66c9761 100644 (file)
@@ -240,7 +240,7 @@ It was one of the first extremely fast Python frameworks based on `asyncio`. It
 
 Falcon is another high performance Python framework, it is designed to be minimal, and work as the foundation of other frameworks like Hug.
 
-It uses the previous standard for Python web frameworks (WSGI) which is synchronous, so it can't handle Websockets and other use cases. Nevertheless, it also has a very good performance.
+It uses the previous standard for Python web frameworks (WSGI) which is synchronous, so it can't handle WebSockets and other use cases. Nevertheless, it also has a very good performance.
 
 It is designed to have functions that receive two parameters, one "request" and one "response". Then you "read" parts from the request, and "write" parts to the response. Because of this design, it is not possible to declare request parameters and bodies with standard Python type hints as function parameters.
 
@@ -249,6 +249,10 @@ So, data validation, serialization, and documentation, have to be done in code,
 !!! check "Inspired **FastAPI** to"
     Find ways to get great performance.
 
+    Along with Hug (as Hug is based on Falcon) inspired **FastAPI** to declare a `response` parameter in functions.
+
+    Although in FastAPI it's optional, and is used mainly to set headers, cookies, and alternative status codes.
+
 ### <a href="https://moltenframework.com/" target="_blank">Molten</a>
 
 I discovered Molten in the first stages of building **FastAPI**. And it has quite similar ideas:
@@ -292,6 +296,7 @@ As it is based on the previous standard for synchronous Python web frameworks (W
 
     Hug helped inspiring **FastAPI** to use Python type hints to declare parameters, and to generate a schema defining the API automatically.
 
+    Hug inspired **FastAPI** to declare a `response` parameter in functions to set headers and cookies.
 
 ### <a href="https://github.com/encode/apistar" target="_blank">APIStar</a> (<= 0.5)
 
diff --git a/docs/src/response_change_status_code/tutorial001.py b/docs/src/response_change_status_code/tutorial001.py
new file mode 100644 (file)
index 0000000..9bdfef7
--- /dev/null
@@ -0,0 +1,15 @@
+from fastapi import FastAPI
+from starlette.responses import Response
+from starlette.status import HTTP_201_CREATED
+
+app = FastAPI()
+
+tasks = {"foo": "Listen to the Bar Fighters"}
+
+
+@app.put("/get-or-create-task/{task_id}", status_code=200)
+def get_or_create_task(task_id: str, response: Response):
+    if task_id not in tasks:
+        tasks[task_id] = "This didn't exist before"
+        response.status_code = HTTP_201_CREATED
+    return tasks[task_id]
diff --git a/docs/src/response_cookies/tutorial002.py b/docs/src/response_cookies/tutorial002.py
new file mode 100644 (file)
index 0000000..fed1fad
--- /dev/null
@@ -0,0 +1,10 @@
+from fastapi import FastAPI
+from starlette.responses import Response
+
+app = FastAPI()
+
+
+@app.post("/cookie-and-object/")
+def create_cookie(response: Response):
+    response.set_cookie(key="fakesession", value="fake-cookie-session-value")
+    return {"message": "Come to the dark side, we have cookies"}
diff --git a/docs/src/response_headers/tutorial002.py b/docs/src/response_headers/tutorial002.py
new file mode 100644 (file)
index 0000000..3fcde02
--- /dev/null
@@ -0,0 +1,10 @@
+from fastapi import FastAPI
+from starlette.responses import Response
+
+app = FastAPI()
+
+
+@app.get("/headers-and-object/")
+def get_headers(response: Response):
+    response.headers["X-Cat-Dog"] = "alone in the world"
+    return {"message": "Hello World"}
diff --git a/docs/tutorial/response-change-status-code.md b/docs/tutorial/response-change-status-code.md
new file mode 100644 (file)
index 0000000..2c3655c
--- /dev/null
@@ -0,0 +1,31 @@
+You probably read before that you can set a <a href="https://fastapi.tiangolo.com/tutorial/response-status-code/" target="_blank">default Response Status Code</a>.
+
+But in some cases you need to return a different status code than the default.
+
+## Use case
+
+For example, imagine that you want to return an HTTP status code of "OK" `200` by default.
+
+But if the data didn't exist, you want to create it, and return an HTTP status code of "CREATED" `201`.
+
+But you still want to be able to filter and convert the data you return with a `response_model`.
+
+For those cases, you can use a `Response` parameter.
+
+## Use a `Response` parameter
+
+You can declare a parameter of type `Response` in your *path operation function* (as you can do for cookies and headers).
+
+And then you can set the `status_code` in that *temporal* response object.
+
+```Python hl_lines="2 11 14"
+{!./src/response_change_status_code/tutorial001.py!}
+```
+
+And then you can return any object you need, as you normally would (a `dict`, a database model, etc).
+
+And if you declared a `response_model`, it will still be used to filter and convert the object you returned.
+
+**FastAPI** will use that *temporal* response to extract the status code (also cookies and headers), and will put them in the final response that contains the value you returned, filtered by any `response_model`.
+
+You can also declare the `Response` parameter in dependencies, and set the status code in them. But have in mind that the last one to be set will win.
index c36587c4932c4ceea22f3b727c0ec56895fcad88..62c633752c1b8c9f47da198ac70eb9eb81cfd426 100644 (file)
@@ -1,4 +1,24 @@
-You can create (set) Cookies in your response.
+## Use a `Response` parameter
+
+You can declare a parameter of type `Response` in your *path operation function*, the same way you can declare a `Request` parameter.
+
+And then you can set headers in that *temporal* response object.
+
+```Python hl_lines="2 8 9"
+{!./src/response_cookies/tutorial002.py!}
+```
+
+And then you can return any object you need, as you normally would (a `dict`, a database model, etc).
+
+And if you declared a `response_model`, it will still be used to filter and convert the object you returned.
+
+**FastAPI** will use that *temporal* response to extract the cookies (also headers and status code), and will put them in the final response that contains the value you returned, filtered by any `response_model`.
+
+You can also declare the `Response` parameter in dependencies, and set cookies (and headers) in them.
+
+## Return a `Response` directly
+
+You can also create cookies when returning a `Response` directly in your code.
 
 To do that, you can create a response as described in <a href="https://fastapi.tiangolo.com/tutorial/response-directly/" target="_blank">Return a Response directly</a>.
 
@@ -8,6 +28,13 @@ Then set Cookies in it, and then return it:
 {!./src/response_cookies/tutorial001.py!}
 ```
 
-## More info
+!!! tip
+    Have in mind that if you return a response directly instead of using the `Response` parameter, FastAPI will return it directly. 
+
+    So, you will have to make sure your data is of the correct type. E.g. it is compatible with JSON, if you are returning a `JSONResponse`.
+
+    And also that you are not sending any data that should have been filtered by a `response_model`.
+
+### More info
 
 To see all the available parameters and options, check the <a href="https://www.starlette.io/responses/#set-cookie" target="_blank">documentation in Starlette</a>.
index b7b1c5557f4c7f6822150653bfa43367af51c209..f9608956ed214a957036b598abc041d96a27c9b1 100644 (file)
@@ -1,4 +1,24 @@
-You can add headers to your response.
+## Use a `Response` parameter
+
+You can declare a parameter of type `Response` in your *path operation function* (as you can do for cookies), the same way you can declare a `Request` parameter.
+
+And then you can set headers in that *temporal* response object.
+
+```Python hl_lines="2 8 9"
+{!./src/response_headers/tutorial002.py!}
+```
+
+And then you can return any object you need, as you normally would (a `dict`, a database model, etc).
+
+And if you declared a `response_model`, it will still be used to filter and convert the object you returned.
+
+**FastAPI** will use that *temporal* response to extract the headers (also cookies and status code), and will put them in the final response that contains the value you returned, filtered by any `response_model`.
+
+You can also declare the `Response` parameter in dependencies, and set headers (and cookies) in them.
+
+## Return a `Response` directly
+
+You can also add headers when you return a `Response` directly.
 
 Create a response as described in <a href="https://fastapi.tiangolo.com/tutorial/response-directly/" target="_blank">Return a Response directly</a> and pass the headers as an additional parameter:
 
@@ -6,7 +26,8 @@ Create a response as described in <a href="https://fastapi.tiangolo.com/tutorial
 {!./src/response_headers/tutorial001.py!}
 ```
 
-!!! tip
-    Have in mind that custom proprietary headers can be added <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers" target="_blank">using the 'X-' prefix</a>.
+## Custom Headers
+
+Have in mind that custom proprietary headers can be added <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers" target="_blank">using the 'X-' prefix</a>.
 
-    But if you have custom headers that you want a client in a browser to be able to see, you need to add them to your <a href="https://fastapi.tiangolo.com/tutorial/cors/" target="_blank">CORS configurations</a>, using the parameter `expose_headers` documented in <a href="https://www.starlette.io/middleware/#corsmiddleware" target="_blank">Starlette's CORS docs</a>.
+But if you have custom headers that you want a client in a browser to be able to see, you need to add them to your <a href="https://fastapi.tiangolo.com/tutorial/cors/" target="_blank">CORS configurations</a>, using the parameter `expose_headers` documented in <a href="https://www.starlette.io/middleware/#corsmiddleware" target="_blank">Starlette's CORS docs</a>.
index 795834c4aa596a7e98fbfc8fdcd97b54df5943a2..f87035ca7468d1ccfb5b824131c651c9e37614ab 100644 (file)
@@ -47,7 +47,6 @@ In short:
 !!! tip
     To know more about each status code and which code is for what, check the <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Status" target="_blank"><abbr title="Mozilla Developer Network">MDN</abbr> documentation about HTTP status codes</a>.
 
-
 ## Shortcut to remember the names
 
 Let's see the previous example again:
@@ -69,3 +68,7 @@ You can use the convenience variables from `starlette.status`.
 They are just a convenience, they hold the same number, but that way you can use the editor's autocomplete to find them:
 
 <img src="/img/tutorial/response-status-code/image02.png">
+
+## Changing the default
+
+Later, in a more advanced part of the tutorial/user guide, you will see how to <a href="https://fastapi.tiangolo.com/tutorial/response-change-status-code/" target="_blank">return a different status code than the default</a> you are declaring here.
index 29fdd0e22af2c08d885c0aa1fc37e06bfd92356f..12f0679ed76cb954d3be93634658b646da924026 100644 (file)
@@ -27,6 +27,7 @@ class Dependant:
         call: Callable = None,
         request_param_name: str = None,
         websocket_param_name: str = None,
+        response_param_name: str = None,
         background_tasks_param_name: str = None,
         security_scopes_param_name: str = None,
         security_scopes: List[str] = None,
@@ -42,6 +43,7 @@ class Dependant:
         self.security_requirements = security_schemes or []
         self.request_param_name = request_param_name
         self.websocket_param_name = websocket_param_name
+        self.response_param_name = response_param_name
         self.background_tasks_param_name = background_tasks_param_name
         self.security_scopes = security_scopes
         self.security_scopes_param_name = security_scopes_param_name
index e79a9a6a0ef1cc1cf4ebde0d851d981847972650..a16c649040a972d51afeb66d6ad0acf8eebb52ef 100644 (file)
@@ -31,6 +31,7 @@ from starlette.background import BackgroundTasks
 from starlette.concurrency import run_in_threadpool
 from starlette.datastructures import FormData, Headers, QueryParams, UploadFile
 from starlette.requests import Request
+from starlette.responses import Response
 from starlette.websockets import WebSocket
 
 sequence_shapes = {
@@ -212,6 +213,9 @@ def add_non_field_param_to_dependency(
     elif lenient_issubclass(param.annotation, WebSocket):
         dependant.websocket_param_name = param.name
         return True
+    elif lenient_issubclass(param.annotation, Response):
+        dependant.response_param_name = param.name
+        return True
     elif lenient_issubclass(param.annotation, BackgroundTasks):
         dependant.background_tasks_param_name = param.name
         return True
@@ -295,16 +299,21 @@ async def solve_dependencies(
     dependant: Dependant,
     body: Dict[str, Any] = None,
     background_tasks: BackgroundTasks = None,
+    response: Response = None,
     dependency_overrides_provider: Any = None,
     dependency_cache: Dict[Tuple[Callable, Tuple[str]], Any] = None,
 ) -> Tuple[
     Dict[str, Any],
     List[ErrorWrapper],
     Optional[BackgroundTasks],
+    Response,
     Dict[Tuple[Callable, Tuple[str]], Any],
 ]:
     values: Dict[str, Any] = {}
     errors: List[ErrorWrapper] = []
+    response = response or Response(  # type: ignore
+        content=None, status_code=None, headers=None, media_type=None, background=None
+    )
     dependency_cache = dependency_cache or {}
     sub_dependant: Dependant
     for sub_dependant in dependant.dependencies:
@@ -330,14 +339,22 @@ async def solve_dependencies(
                 security_scopes=sub_dependant.security_scopes,
             )
 
-        sub_values, sub_errors, background_tasks, sub_dependency_cache = await solve_dependencies(
+        solved_result = await solve_dependencies(
             request=request,
             dependant=use_sub_dependant,
             body=body,
             background_tasks=background_tasks,
+            response=response,
             dependency_overrides_provider=dependency_overrides_provider,
             dependency_cache=dependency_cache,
         )
+        sub_values, sub_errors, background_tasks, sub_response, sub_dependency_cache = (
+            solved_result
+        )
+        sub_response = cast(Response, sub_response)
+        response.headers.raw.extend(sub_response.headers.raw)
+        if sub_response.status_code:
+            response.status_code = sub_response.status_code
         dependency_cache.update(sub_dependency_cache)
         if sub_errors:
             errors.extend(sub_errors)
@@ -383,11 +400,13 @@ async def solve_dependencies(
         if background_tasks is None:
             background_tasks = BackgroundTasks()
         values[dependant.background_tasks_param_name] = background_tasks
+    if dependant.response_param_name:
+        values[dependant.response_param_name] = response
     if dependant.security_scopes_param_name:
         values[dependant.security_scopes_param_name] = SecurityScopes(
             scopes=dependant.security_scopes
         )
-    return values, errors, background_tasks, dependency_cache
+    return values, errors, background_tasks, response, dependency_cache
 
 
 def request_params_to_args(
index 4ae8bb586d50dc0ca77860940561816838c9ee44..526cc485df0bccf4b47c5d5a9d99540429910d51 100644 (file)
@@ -102,12 +102,13 @@ def get_app(
             raise HTTPException(
                 status_code=400, detail="There was an error parsing the body"
             ) from e
-        values, errors, background_tasks, _ = await solve_dependencies(
+        solved_result = await solve_dependencies(
             request=request,
             dependant=dependant,
             body=body,
             dependency_overrides_provider=dependency_overrides_provider,
         )
+        values, errors, background_tasks, sub_response, _ = solved_result
         if errors:
             raise RequestValidationError(errors)
         else:
@@ -128,11 +129,15 @@ def get_app(
                 by_alias=response_model_by_alias,
                 skip_defaults=response_model_skip_defaults,
             )
-            return response_class(
+            response = response_class(
                 content=response_data,
                 status_code=status_code,
                 background=background_tasks,
             )
+            response.headers.raw.extend(sub_response.headers.raw)
+            if sub_response.status_code:
+                response.status_code = sub_response.status_code
+            return response
 
     return app
 
@@ -141,11 +146,12 @@ def get_websocket_app(
     dependant: Dependant, dependency_overrides_provider: Any = None
 ) -> Callable:
     async def app(websocket: WebSocket) -> None:
-        values, errors, _, _2 = await solve_dependencies(
+        solved_result = await solve_dependencies(
             request=websocket,
             dependant=dependant,
             dependency_overrides_provider=dependency_overrides_provider,
         )
+        values, errors, _, _2, _3 = solved_result
         if errors:
             await websocket.close(code=WS_1008_POLICY_VIOLATION)
             raise WebSocketRequestValidationError(errors)
index c75581133a31ca01128fdb0e420999e2837a27ce..c6f8fdfd8ca127e8604d91db11b0bb3387ec6699 100644 (file)
@@ -51,6 +51,7 @@ nav:
         - Additional Responses in OpenAPI: 'tutorial/additional-responses.md'
         - Response Cookies: 'tutorial/response-cookies.md'
         - Response Headers: 'tutorial/response-headers.md'
+        - Response - Change Status Code: 'tutorial/response-change-status-code.md'
         - Dependencies:
             - First Steps: 'tutorial/dependencies/first-steps.md'
             - Classes as Dependencies: 'tutorial/dependencies/classes-as-dependencies.md'
diff --git a/tests/test_response_change_status_code.py b/tests/test_response_change_status_code.py
new file mode 100644 (file)
index 0000000..0e2ba88
--- /dev/null
@@ -0,0 +1,27 @@
+from fastapi import Depends, FastAPI
+from starlette.responses import Response
+from starlette.testclient import TestClient
+
+app = FastAPI()
+
+
+async def response_status_setter(response: Response):
+    response.status_code = 201
+
+
+async def parent_dep(result=Depends(response_status_setter)):
+    return result
+
+
+@app.get("/", dependencies=[Depends(parent_dep)])
+async def get_main():
+    return {"msg": "Hello World"}
+
+
+client = TestClient(app)
+
+
+def test_dependency_set_status_code():
+    response = client.get("/")
+    assert response.status_code == 201
+    assert response.json() == {"msg": "Hello World"}
diff --git a/tests/test_tutorial/test_response_change_status_code/__init__.py b/tests/test_tutorial/test_response_change_status_code/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/tests/test_tutorial/test_response_change_status_code/test_tutorial001.py b/tests/test_tutorial/test_response_change_status_code/test_tutorial001.py
new file mode 100644 (file)
index 0000000..f52ba20
--- /dev/null
@@ -0,0 +1,15 @@
+from starlette.testclient import TestClient
+
+from response_change_status_code.tutorial001 import app
+
+client = TestClient(app)
+
+
+def test_path_operation():
+    response = client.put("/get-or-create-task/foo")
+    print(response.content)
+    assert response.status_code == 200
+    assert response.json() == "Listen to the Bar Fighters"
+    response = client.put("/get-or-create-task/bar")
+    assert response.status_code == 201
+    assert response.json() == "This didn't exist before"
diff --git a/tests/test_tutorial/test_response_cookies/test_tutorial002.py b/tests/test_tutorial/test_response_cookies/test_tutorial002.py
new file mode 100644 (file)
index 0000000..1444d63
--- /dev/null
@@ -0,0 +1,12 @@
+from starlette.testclient import TestClient
+
+from response_cookies.tutorial002 import app
+
+client = TestClient(app)
+
+
+def test_path_operation():
+    response = client.post("/cookie-and-object/")
+    assert response.status_code == 200
+    assert response.json() == {"message": "Come to the dark side, we have cookies"}
+    assert response.cookies["fakesession"] == "fake-cookie-session-value"
diff --git a/tests/test_tutorial/test_response_headers/test_tutorial002.py b/tests/test_tutorial/test_response_headers/test_tutorial002.py
new file mode 100644 (file)
index 0000000..6dc7fda
--- /dev/null
@@ -0,0 +1,12 @@
+from starlette.testclient import TestClient
+
+from response_headers.tutorial002 import app
+
+client = TestClient(app)
+
+
+def test_path_operation():
+    response = client.get("/headers-and-object/")
+    assert response.status_code == 200
+    assert response.json() == {"message": "Hello World"}
+    assert response.headers["X-Cat-Dog"] == "alone in the world"