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!
Egehan yazılarını takip ediyor ve çok beğeniyorum. İçerik üretmeye devam etmen gerek. Django konusunda Türkiye’de ciddi eksiklik var. Bir boşluğu dolduruyorsun.
Tebrikler