İçeriğe geç

Django Rest Framework Custom Exception Handling

Giriş.

Exception handling, geliştirdiğiniz uygulamanın yürütme esnasında karşılaşabileceği beklenmedik ya da iş akışı sırasında bilinçli oluşturulan hataların, kontrollü olarak ele alınarak uygulamanın bu durumlara yanıt dönebilme yeteneğini belirten bir kavramdır.

İyi kurgulanmış bir exception handling yapısı, son kullanıcılara, uygulamanızın geliştirmesinde rol alan yazılımcılara fazlasıyla yardımcı olur. Ters giden konular hakkında gerekli bilgilendirmeyi yapar. Farklı log seviyeleri ve grupları kullandığınız bir uygulama yapınız varsa sistematik bir bilgi birikimi de sağlanabilir.

Django Rest Framework, yerleşik olarak bir exception handling mekanizmasına sahiptir. Ters giden bir durumda, belirli exception tiplerini yakalayarak uygun mesajlarla geriye yanıt döndürebilir. Bu tipler;

  • APIException’ı miras alan ve DRF içerisinde raise edilen exceptionlar.
  • Django’nın yerlesik Http404 ve PermissionDenied exception sınıfları.

Örnek olarak;

DELETE http://api.example.com/foo/bar HTTP/1.1
Accept: application/json
HTTP/1.1 405 Method Not Allowed
Content-Type: application/json
Content-Length: 42
{"detail": "Method 'DELETE' not allowed."}

Bu yazıda, yerleşik handling mekanizmasını özelleştirerek daha detaylı bilgi içeren yanıtlar dönmeye, ve uygulamanızın raise ettiği business logic ile alakalı exceptionları handle etmeye göz atacağız.

Öncelikle hata mesajları için bir dayanak olması açısından, yerleşik User modeline kayıt atan bir viewset ve serializer oluşturup kullanıma hazır hale getirelim.

# users/serializers.py
from django.contrib.auth import get_user_model
from rest_framework import serializers

User = get_user_model()


class UserSerializer(serializers.ModelSerializer):
    password_2 = serializers.CharField(write_only=True)

    class Meta:
        model = User
        fields = [
            "email",
            "username",
            "first_name",
            "last_name",
            "password",
            "password_2",
        ]
        extra_kwargs = {
            "password": {
                "write_only": True,
            },
        }

    def create(self, validated_data):
        validated_data.pop('password_2')
        return User.objects.create_user(**validated_data)

    def validate(self, attrs):
        password, password_2 = attrs.get("password"), attrs.get("password_2")
        if password != password_2:
            raise serializers.ValidationError("Both password fields must match.")
        return attrs

 

from django.contrib.auth import get_user_model
from rest_framework import authentication, permissions, viewsets

from users.serializers import UserSerializer

User = get_user_model()


class UserViewset(viewsets.ModelViewSet):
    queryset = User.objects.all()
    serializer_class = UserSerializer
    authentication_classes = (authentication.TokenAuthentication,)
    permission_classes = (permissions.IsAuthenticated,)

Bu endpointten dönebilecek birkaç hata mesajı;

ValidationError(APIException);

HTTP 400 Bad Request
Allow: GET, POST, HEAD, OPTIONS
Content-Type: application/json
Vary: Accept
{
    "non_field_errors": [
        "Both password fields must match."
    ]
}

NotAuthenticated(APIException);

HTTP 401 Unauthorized
Allow: GET, POST, HEAD, OPTIONS
Content-Type: application/json
Vary: Accept
WWW-Authenticate: Token
{
    "detail": "Authentication credentials were not provided."
}

Peki daha detaylı bilgi vermek için dönen yanıtın HTTP yanıt kodunu ve human readable açıklamasını eklemek istersek?


Handler.

Yerleşik DRF handleri settingste aşağıdaki şekilde tanımlı;

# api_settings.py

REST_FRAMEWORK = {
    "EXCEPTION_HANDLER": "rest_framework.views.exception_handler",
}

Oluşturacağımız handler 2 zorunlu argümana sahip ve hataları Response nesnelerine dönüştürmekle görevli.

Bu argümanlar oluşan exception nesnesi ve birtakım meta bilgi içeren context dict.

context içerisinde; request, view, view’ın argümanları gibi bilgiler bulunuyor.

import http.client
from typing import Any

from rest_framework.response import Response
from rest_framework.views import exception_handler as drf_exception_handler


def custom_exception_handler(exc: Exception, ctx: dict[str, Any]) -> Response:

    response = drf_exception_handler(exc=exc, context=ctx)

    if response is not None:
        response_payload = {
            "error": response.data,
            "status_code": response.status_code,
            "reason": http.client.responses.get(response.status_code),
            "view_name": ctx["view"].__class__.__name__, # only for ctx usage demonstration
            "view_desc": ctx["view"].__class__.__doc__, # only for ctx usage demonstration
        }

        response.data = response_payload

    return response

yukarıdaki handler, ek bilgi olarak isteğin durum kodunu, human readble sebebini ve pek iyi bir fikir olmasa da exceptionın handle edildiği view hakkında bir takım bilgileri oluşturulan cevaba ekliyor.

Yerleşik handlerı, bir response nesnesi oluşturmaya çalışması için çağırıyoruz. Bunun yanında yerleşik handler, atomik bir blokta hata meydana gelmiş ise gerekli rollback işlemini yapıyor.

Sonrasında dönen response nesnesinin verisini değiştiriyor ve geri dönüyoruz.

Eğer bir response oluşturulamaz ve None dönülürse, DRF’ın handle edebildiği bir exception oluşmamış demektir.

Bu durumda, exception tekrar raise edilecek, standart Django 500 hatası yanıt olarak dönülecektir.

Bu handleri DRF settingsine belirtelim;

REST_FRAMEWORK = {
    "EXCEPTION_HANDLER": "<your_project>.<your_exc_handler_path>.<your_exception_handler_name>",
}

GET /api/users/
HTTP 401 Unauthorized
Allow: GET, POST, HEAD, OPTIONS
Content-Type: application/json
Vary: Accept
WWW-Authenticate: Token
{
    "error": {
        "detail": "Authentication credentials were not provided."
    },
    "status_code": 401,
    "reason": "Unauthorized",
    "view_name": "UserViewset",
    "view_desc": "Manage users."
}

 

POST /api/users/
{
    "error": {
        "non_field_errors": [
            "Both password fields must match."
        ]
    },
    "status_code": 400,
    "reason": "Bad Request",
    "view_name": "UserViewset",
    "view_desc": "Manage users."
}

Daha anlaşılır mesajlar!


Not. Yazının başında belirttiğimiz tiplerden olmayan hataların oluşması durumunda, response None dönecektir. Hatayı tekrar raise edildiğinde standart Django 500 sayfasını göstermektense, 500 durum kodunu handle edecek viewida settings üzerinde belirtebiliriz. (Backendi sadece API olarak kullanıyorsanız.)

# settings.py

handler500 = 'rest_framework.exceptions.server_error'

Custom exception sınıflarını handle etmek.

Uygulamanız içerisinde, iş akışı ile alakalı custom exception sınıfları kullanıyorsanız, raise ettiğiniz sırada bir handling yaparak kullanıcıya açıklayıcı bir response dönebilirsiniz.

Örneğin, adres takibi yaptığımız bir uygulama var ve bir kullanıcının oluşturabileceği maksimum adres sayısını 3 ile sınırlamak istiyorsunuz.

Basit bir base exception ve adres sınırına ulaşıldığında raise edilen exception sınıfı; 👇🏻

from django.utils.translation import gettext_lazy as _
from rest_framework import status


class ProjectBaseException(Exception):
    message = "Project Base Exception"
    status_code = status.HTTP_406_NOT_ACCEPTABLE

    def __str__(self) -> str:
        return self.message


class AddressMaxLimitExceeded(ProjectBaseException):
    message = _("Max address limit exceeded. Try inactive your addresses or delete.")

İlgili view 👇🏻

# address/views.py

from django.conf import settings
from rest_framework import authentication, permissions, serializers, viewsets

class AddressViewset(viewsets.ModelViewSet):
    "Manage user addresses."
    serializer_class = AddressSerializer
    queryset = Address.objects.all()
    permission_classes = (permissions.IsAuthenticated,)
    authentication_classes = (authentication.TokenAuthentication,)

    def get_queryset(self):
        return super().get_queryset().filter(owner=self.request.user)

    def perform_create(self, serializer):
        user = self.request.user
        if not self.new_address_createble_for_user(user=user):
            raise AddressMaxLimitExceeded
        serializer.save(owner=user)

    @staticmethod
    def new_address_createble_for_user(user):
        return user.address_set.filter(is_active=True).count() < settings.MAX_ADDRESS_COUNT_PER_USER


Hatayı artık adres oluşturma sırasında yaptığımız kontrolde raise ediyoruz. Fakat handler fonksiyona, uygulamanın sahip olduğu exceptionlarıda dikkate alması için bir blok eklememiz gerekiyor.

Handleri genişletelim 👇🏻

def custom_exception_handler(exc: Exception, ctx: dict[str, Any]) -> Response:

    response = drf_exception_handler(exc=exc, context=ctx)

    if response is not None:
        response_payload = {
            "error": response.data,
            "status_code": response.status_code,
            "reason": http.client.responses.get(response.status_code),
            "view_name": ctx["view"].__class__.__name__,
            "view_desc": ctx["view"].__class__.__doc__,
        }

        response.data = response_payload
    else: 
        if isinstance(exc, ProjectBaseException):
            response_payload = {
                "details": exc.message,
                "status_code": exc.status_code,
                "reason": http.client.responses.get(exc.status_code),
                "view_name" : ctx["view"].__class__.__name__,
                "view_desc" : ctx["view"].__class__.__doc__,
            }
        return Response(data=response_payload, status=exc.status_code)

    return response

3 adresi olan bir kullanıcı ile yeni bir adres kayıt etmek istersek;

POST /api/address/
HTTP 406 Not Acceptable
Allow: GET, POST, HEAD, OPTIONS
Content-Type: application/json
Vary: Accept
{
    "details": "Max address limit exceeded. Try inactive your addresses or delete.",
    "status_code": 406,
    "reason": "Not Acceptable",
    "view_name": "AddressViewset",
    "view_desc": "Manage user addresses."
}

Sonuç.

DRF’in exceptionları nasıl handle ettiği ile alakalı genel bir resim çizebildik diye düşünüyorum. Yukarıdaki basit örnekler pekala gelişime ve değişime açık. Örneğin exception sınıflarınıza level belirten bir belirteç ekleyip farklı log mekanizmalarına bu exceptionları loglarsanız, loggerdan alınan raporlar doğrultusunda uygulamanın hangi exceptiona ne sıklıkla takıldığı konusunda çıkarımlar yapabilirsiniz.

Şimdilik bu kadar 🙂 İyi tatiller, görüşmek üzere!

Tarih:Blog

İlk Yorumu Siz Yapın

Bir cevap yazın

E-posta hesabınız yayımlanmayacak.

Göster
Gizle