Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: make 302 the default status_code for redirect responses #2189

Merged
merged 15 commits into from
Sep 7, 2023
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion docs/usage/openapi.rst
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,10 @@ You can also modify the generated schema for the route handler using the followi
A list of exception classes extending from ``litestar.HttpException``. This list should describe all
exceptions raised within the route handler's function/method. The Litestar ``ValidationException`` will be added
automatically for the schema if any validation is involved (e.g. there are parameters specified in the
method/function).
method/function). For custom exceptions, a `detail` class property should be defined, which will be integrated
into the OpenAPI schema. If `detail` isn't specified and the exception's status code matches one
from `stdlib status code <https://docs.python.org/3/library/http.html#http-status-codes>`_, a generic message
will be applied.

``responses``
A dictionary of additional status codes and a description of their expected content.
Expand Down
2 changes: 1 addition & 1 deletion docs/usage/responses.rst
Original file line number Diff line number Diff line change
Expand Up @@ -589,7 +589,7 @@ In Litestar, a redirect response looks like this:

To return a redirect response you should do the following:

- set an appropriate status code for the route handler (301, 302, 303, 307, 308)
- set an appropriate status code for the route handler (301, 302, 303, 307, 308). The default status code is 302
cofin marked this conversation as resolved.
Show resolved Hide resolved
- annotate the return value of the route handler as returning :class:`Redirect <.response.Redirect>`
- return an instance of the :class:`Redirect <.response.Redirect>` class with the desired redirect path

Expand Down
50 changes: 34 additions & 16 deletions litestar/_openapi/responses.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import contextlib
import re
from copy import copy
from dataclasses import asdict
Expand Down Expand Up @@ -188,28 +189,45 @@ def create_error_responses(exceptions: list[type[HTTPException]]) -> Iterator[tu
grouped_exceptions[exc.status_code] = []
grouped_exceptions[exc.status_code].append(exc)
for status_code, exception_group in grouped_exceptions.items():
exceptions_schemas = [
Schema(
type=OpenAPIType.OBJECT,
required=["detail", "status_code"],
properties={
"status_code": Schema(type=OpenAPIType.INTEGER),
"detail": Schema(type=OpenAPIType.STRING),
"extra": Schema(
type=[OpenAPIType.NULL, OpenAPIType.OBJECT, OpenAPIType.ARRAY], additional_properties=Schema()
),
},
description=pascal_case_to_text(get_name(exc)),
examples=[{"status_code": status_code, "detail": HTTPStatus(status_code).phrase, "extra": {}}],
exceptions_schemas = []
group_description: str = ""
for exc in exception_group:
example_detail = ""
if hasattr(exc, "detail") and exc.detail:
group_description = exc.detail
example_detail = exc.detail

if not example_detail:
with contextlib.suppress(Exception):
example_detail = HTTPStatus(status_code).phrase

exceptions_schemas.append(
Schema(
type=OpenAPIType.OBJECT,
required=["detail", "status_code"],
properties={
"status_code": Schema(type=OpenAPIType.INTEGER),
"detail": Schema(type=OpenAPIType.STRING),
"extra": Schema(
type=[OpenAPIType.NULL, OpenAPIType.OBJECT, OpenAPIType.ARRAY],
additional_properties=Schema(),
),
},
description=pascal_case_to_text(get_name(exc)),
examples=[{"status_code": status_code, "detail": example_detail, "extra": {}}],
)
)
for exc in exception_group
]
if len(exceptions_schemas) > 1: # noqa: SIM108
schema = Schema(one_of=exceptions_schemas)
else:
schema = exceptions_schemas[0]

if not group_description:
with contextlib.suppress(Exception):
group_description = HTTPStatus(status_code).description

yield str(status_code), OpenAPIResponse(
description=HTTPStatus(status_code).description,
description=group_description,
content={MediaType.JSON: OpenAPIMediaType(schema=schema)},
)

Expand Down
20 changes: 13 additions & 7 deletions litestar/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from collections import defaultdict
from copy import deepcopy
from functools import partial
from operator import attrgetter
from typing import TYPE_CHECKING, Any, Mapping, cast

from litestar._layers.utils import narrow_response_cookies, narrow_response_headers
Expand Down Expand Up @@ -186,13 +187,18 @@ def get_route_handlers(self) -> list[BaseRouteHandler]:
"""

route_handlers: list[BaseRouteHandler] = []

for field_name in set(dir(self)) - set(dir(Controller)):
if (attr := getattr(self, field_name, None)) and isinstance(attr, BaseRouteHandler):
route_handler = deepcopy(attr)
route_handler.fn.value = partial(route_handler.fn.value, self)
route_handler.owner = self
route_handlers.append(route_handler)
controller_names = set(dir(Controller))
self_handlers = [
getattr(self, name)
for name in dir(self)
if name not in controller_names and isinstance(getattr(self, name), BaseRouteHandler)
]
self_handlers.sort(key=attrgetter("handler_id"))
for self_handler in self_handlers:
route_handler = deepcopy(self_handler)
route_handler.fn.value = partial(route_handler.fn.value, self)
route_handler.owner = self
route_handlers.append(route_handler)

self.validate_route_handlers(route_handlers=route_handlers)

Expand Down
4 changes: 3 additions & 1 deletion litestar/response/redirect.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from litestar.enums import MediaType
from litestar.exceptions import ImproperlyConfiguredException
from litestar.response.base import ASGIResponse, Response
from litestar.status_codes import HTTP_307_TEMPORARY_REDIRECT
from litestar.status_codes import HTTP_302_FOUND, HTTP_307_TEMPORARY_REDIRECT
from litestar.utils import url_quote
from litestar.utils.helpers import filter_cookies, get_enum_string_value

Expand Down Expand Up @@ -95,6 +95,8 @@ def __init__(
supported.
"""
self.url = path
if status_code is None:
status_code = HTTP_302_FOUND
super().__init__(
background=background,
content=b"",
Expand Down
2 changes: 1 addition & 1 deletion litestar/routes/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ def _validate_path_parameter(param: str, path: str) -> None:
raise ImproperlyConfiguredException("Path parameter names should be of length greater than zero")
if param_type not in param_type_map:
raise ImproperlyConfiguredException(
f"Path parameters should be declared with an allowed type, i.e. one of {','.join(param_type_map.keys())}"
f"Path parameters should be declared with an allowed type, i.e. one of {', '.join(param_type_map.keys())} in path: '{path}'"
)

@classmethod
Expand Down
14 changes: 14 additions & 0 deletions tests/unit/test_openapi/test_responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,20 @@ class AlternativePetException(HTTPException):
assert schema.type


def test_create_error_responses_with_non_http_status_code() -> None:
class HouseNotFoundError(HTTPException):
status_code: int = 420
detail: str = "House not found."

house_not_found_exc_response, _ = tuple(
create_error_responses(exceptions=[HouseNotFoundError, ValidationException])
)

assert house_not_found_exc_response
assert house_not_found_exc_response[0] == str(HouseNotFoundError.status_code)
assert house_not_found_exc_response[1].description == HouseNotFoundError.detail


def test_create_success_response_with_headers() -> None:
@get(
path="/test",
Expand Down
4 changes: 2 additions & 2 deletions tests/unit/test_response/test_redirect_response.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,8 @@ def handler() -> Redirect:
def test_redirect(handler_status_code: Optional[int]) -> None:
@get("/", status_code=handler_status_code)
def handler() -> Redirect:
return Redirect(path="/something-else", status_code=301)
return Redirect(path="/something-else", status_code=handler_status_code) # type: ignore[arg-type]

with create_test_client([handler]) as client:
res = client.get("/", follow_redirects=False)
assert res.status_code == 301
assert res.status_code == 302 if handler_status_code is None else handler_status_code