Skip to content

Commit

Permalink
feat(api): standardize error responses
Browse files Browse the repository at this point in the history
This is companion to #13541 to make API error response more consistent.
We now rely on drf-standardized-erros to do the processing. It also has
drf-spectacular integration, so the error states are now documented in
OpenAPI.
  • Loading branch information
nijel committed Jan 16, 2025
1 parent d24f47c commit d3095e7
Show file tree
Hide file tree
Showing 7 changed files with 62 additions and 19 deletions.
2 changes: 2 additions & 0 deletions docs/changes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ Not yet released.

Please follow :ref:`generic-upgrade-instructions` in order to perform update.

* There are several changes in :file:`settings_example.py`, most notable is changes in ``REST_FRAMEWORK`` and ``DRF_STANDARDIZED_ERRORS``, please adjust your settings accordingly.

**Contributors**

.. include:: changes/contributors/5.10.rst
Expand Down
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,8 @@ dependencies = [
"unidecode>=1.3.8,<1.4",
"user-agents>=2.0,<2.3",
"weblate-language-data>=2024.14",
"weblate-schemas==2024.2"
"weblate-schemas==2024.2",
"drf-standardized-errors[openapi]>=0.14.1,<0.15"
]
description = "A web-based continuous localization system with tight version control integration"
keywords = [
Expand Down
21 changes: 21 additions & 0 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 12 additions & 1 deletion weblate/api/spectacular.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,20 @@ def get_spectacular_settings(
# Flatten enum definitions
"ENUM_NAME_OVERRIDES": {
"ColorEnum": "weblate.utils.colors.ColorChoices.choices",
"ValidationErrorEnum": "drf_standardized_errors.openapi_serializers.ValidationErrorEnum.choices",
"ClientErrorEnum": "drf_standardized_errors.openapi_serializers.ClientErrorEnum.choices",
"ServerErrorEnum": "drf_standardized_errors.openapi_serializers.ServerErrorEnum.choices",
"ErrorCode401Enum": "drf_standardized_errors.openapi_serializers.ErrorCode401Enum.choices",
"ErrorCode403Enum": "drf_standardized_errors.openapi_serializers.ErrorCode403Enum.choices",
"ErrorCode404Enum": "drf_standardized_errors.openapi_serializers.ErrorCode404Enum.choices",
"ErrorCode405Enum": "drf_standardized_errors.openapi_serializers.ErrorCode405Enum.choices",
"ErrorCode406Enum": "drf_standardized_errors.openapi_serializers.ErrorCode406Enum.choices",
"ErrorCode415Enum": "drf_standardized_errors.openapi_serializers.ErrorCode415Enum.choices",
"ErrorCode429Enum": "drf_standardized_errors.openapi_serializers.ErrorCode429Enum.choices",
"ErrorCode500Enum": "drf_standardized_errors.openapi_serializers.ErrorCode500Enum.choices",
},
"POSTPROCESSING_HOOKS": [
"drf_spectacular.hooks.postprocess_schema_enums",
"drf_standardized_errors.openapi_hooks.postprocess_schema_enums",
"weblate.api.docs.add_middleware_headers",
],
"EXTERNAL_DOCS": {
Expand Down
28 changes: 15 additions & 13 deletions weblate/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,13 @@
from django.shortcuts import get_object_or_404
from django.utils.datastructures import MultiValueDictKeyError
from django.utils.html import format_html
from django.utils.translation import gettext
from django.utils.translation import gettext, gettext_lazy
from django_filters import rest_framework as filters
from drf_spectacular.utils import OpenApiParameter, extend_schema, extend_schema_view
from drf_standardized_errors.handler import ExceptionHandler
from rest_framework import parsers, viewsets
from rest_framework.decorators import action
from rest_framework.exceptions import ValidationError
from rest_framework.exceptions import APIException, ValidationError
from rest_framework.mixins import CreateModelMixin, DestroyModelMixin, UpdateModelMixin
from rest_framework.permissions import IsAuthenticated
from rest_framework.renderers import BrowsableAPIRenderer, JSONRenderer
Expand All @@ -42,7 +43,7 @@
HTTP_500_INTERNAL_SERVER_ERROR,
)
from rest_framework.utils import formatting
from rest_framework.views import APIView, exception_handler
from rest_framework.views import APIView
from rest_framework.viewsets import ViewSet

from weblate.accounts.models import Subscription
Expand Down Expand Up @@ -146,18 +147,19 @@
"""


def weblate_exception_handler(exc, context):
# Call REST framework's default exception handler first,
# to get the standard error response.
response = exception_handler(exc, context)
class LockedError(APIException):
status_code = HTTP_423_LOCKED
default_detail = gettext_lazy(
"Could not obtain repository lock to perform the operation."
)
default_code = "repository-locked"

if response is None and isinstance(exc, WeblateLockTimeoutError):
return Response(
data={"error": "Could not obtain repository lock to delete the string."},
status=HTTP_423_LOCKED,
)

return response
class WeblateExceptionHandler(ExceptionHandler):
def convert_known_exceptions(self, exc: Exception) -> Exception:
if isinstance(exc, WeblateLockTimeoutError):
return LockedError()
return super().convert_known_exceptions(exc)


def get_view_description(view, html=False):
Expand Down
7 changes: 5 additions & 2 deletions weblate/settings_docker.py
Original file line number Diff line number Diff line change
Expand Up @@ -1297,9 +1297,12 @@
"DEFAULT_PAGINATION_CLASS": "weblate.api.pagination.StandardPagination",
"PAGE_SIZE": 50,
"VIEW_DESCRIPTION_FUNCTION": "weblate.api.views.get_view_description",
"EXCEPTION_HANDLER": "weblate.api.views.weblate_exception_handler",
"EXCEPTION_HANDLER": "drf_standardized_errors.handler.exception_handler",
"UNAUTHENTICATED_USER": "weblate.auth.models.get_anonymous",
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
"DEFAULT_SCHEMA_CLASS": "drf_standardized_errors.openapi.AutoSchema",
}
DRF_STANDARDIZED_ERRORS = {
"EXCEPTION_HANDLER_CLASS": "weblate.api.views.WeblateExceptionHandler"
}
SPECTACULAR_SETTINGS = get_spectacular_settings(INSTALLED_APPS, SITE_URL, SITE_TITLE)

Expand Down
7 changes: 5 additions & 2 deletions weblate/settings_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -870,9 +870,12 @@
"DEFAULT_PAGINATION_CLASS": "weblate.api.pagination.StandardPagination",
"PAGE_SIZE": 50,
"VIEW_DESCRIPTION_FUNCTION": "weblate.api.views.get_view_description",
"EXCEPTION_HANDLER": "weblate.api.views.weblate_exception_handler",
"EXCEPTION_HANDLER": "drf_standardized_errors.handler.exception_handler",
"UNAUTHENTICATED_USER": "weblate.auth.models.get_anonymous",
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
"DEFAULT_SCHEMA_CLASS": "drf_standardized_errors.openapi.AutoSchema",
}
DRF_STANDARDIZED_ERRORS = {
"EXCEPTION_HANDLER_CLASS": "weblate.api.views.WeblateExceptionHandler"
}
SPECTACULAR_SETTINGS = get_spectacular_settings(INSTALLED_APPS, SITE_URL, SITE_TITLE)

Expand Down

0 comments on commit d3095e7

Please sign in to comment.