exceptions handler 를 왜 커스텀 하는지 알아보자.
필자는 이 3가지로 뽑을 수 있을 것 같다.
우선 custom_exception_handler 를 담기 위한 모듈을 생성한다.
(api_exception.py 파일 생성)
api_exception.py
from rest_framework.views import exception_handler
def custom_exception_handler(exc, context):
# Call REST framework's default exception handler first,
# to get the standard error response.
response = exception_handler(exc, context)
# Now add the HTTP status code to the response.
if response is not None:
response.data['status_code'] = response.status_code
return response
이런 식으로 custom_exception_handler를 생성한다.
이 custom_exception_handler로 적용하는 방법은 settings.py 파일 REST_FRAMEWORK 설정에 EXCEPTION_HANDLER를 추가하면 된다.
EXCEPTION_HANDLER는 생성한 모듈을 위치와 custom_exception_handler으로 적용하면 된다.
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'config.authorization.authentication.DefaultAuthentication',
'rest_framework.authentication.BasicAuthentication',
'rest_framework.authentication.SessionAuthentication',
],
'EXCEPTION_HANDLER': 'config.exceptions.api_exception.custom_exception_handler'
}
from rest_framework.views import exception_handler
def custom_exception_handler(exc, context):
# Call REST framework's default exception handler first,
# to get the standard error response.
response = exception_handler(exc, context)
# Now add the HTTP status code to the response.
if response is not None:
response.data['status_code'] = response.status_code
return response
▶ exc: Exception 클래스가 들어있다.
(특정 APIException 반환)
▶ context: request, view 등 url에 접근 했을 때, 정보들이 들어있다.
exception_handler는 exc, context를 받아서 에러를 처리한다.
def exception_handler(exc, context):
"""
Returns the response that should be used for any given exception.
By default we handle the REST framework `APIException`, and also
Django's built-in `Http404` and `PermissionDenied` exceptions.
Any unhandled exceptions may return `None`, which will cause a 500 error
to be raised.
"""
if isinstance(exc, Http404):
exc = exceptions.NotFound()
elif isinstance(exc, PermissionDenied):
exc = exceptions.PermissionDenied()
if isinstance(exc, exceptions.APIException):
headers = {}
if getattr(exc, 'auth_header', None):
headers['WWW-Authenticate'] = exc.auth_header
if getattr(exc, 'wait', None):
headers['Retry-After'] = '%d' % exc.wait
if isinstance(exc.detail, (list, dict)):
data = exc.detail
else:
data = {'detail': exc.detail}
set_rollback()
return Response(data, status=exc.status_code, headers=headers)
return None
총 3가지 분기가 일어난다.
만약 에러가 Http404, PermissionDenied, APIException 일 경우를 판단한다.
만약 None을 반환하면 모르는 에러라는 것이다.
그리고 각각 exc에 에러를 넣는다.
(참고, APIException 인스턴스를 생성할 때, exc.detial를 만든다. 아래 추가 설명)
class APIException(Exception):
"""
Base class for REST framework exceptions.
Subclasses should provide `.status_code` and `.default_detail` properties.
"""
status_code = status.HTTP_500_INTERNAL_SERVER_ERROR
default_detail = _('A server error occurred.')
default_code = 'error'
def __init__(self, detail=None, code=None):
if detail is None:
detail = self.default_detail
if code is None:
code = self.default_code
self.detail = _get_error_details(detail, code)
def __str__(self):
return str(self.detail)
def get_codes(self):
"""
Return only the code part of the error details.
Eg. {"name": ["required"]}
"""
return _get_codes(self.detail)
def get_full_details(self):
"""
Return both the message & code parts of the error details.
Eg. {"name": [{"message": "This field is required.", "code": "required"}]}
"""
return _get_full_details(self.detail)
APIException은 _get_error_details(detail, code)를 이용하여 APIExcetion 객체를 만들 때 exc.detail 즉, 인스턴스변수 detail을 같이 만든다.
예로 어디에 APIException()을 정의한다면 __init__ 함수를 실행하여, 아무것도 없으니 detail과 code가 default_detail, default_code로 _get_error_details 파라미터로 사용되어 결과적인 exc.detail을 만드는 것이다.
그렇다면 _get_error_details 함수는 어떤 기능을 할까?
_get_error_details 함수
def _get_error_details(data, default_code=None):
"""
Descend into a nested data structure, forcing any
lazy translation strings or strings into `ErrorDetail`.
"""
if isinstance(data, (list, tuple)):
ret = [
_get_error_details(item, default_code) for item in data
]
if isinstance(data, ReturnList):
return ReturnList(ret, serializer=data.serializer)
return ret
elif isinstance(data, dict):
ret = {
key: _get_error_details(value, default_code)
for key, value in data.items()
}
if isinstance(data, ReturnDict):
return ReturnDict(ret, serializer=data.serializer)
return ret
text = force_str(data)
code = getattr(data, 'code', default_code)
return ErrorDetail(text, code)
만약 _get_error_details 함수 안에 detail로 들어간 파라미터 즉, data 인자가 list 혹은 tuple일 경우, 혹은 dictionary일 경우로 분기 처리된다.
직접 list, tuple/dictionary/str인 경우를 예를 들어보자.
- list, tuple인 경우
raise APIException(detail=[1, 2, 3], code=714)
exc.detail
[ErrorDetail(string='1', code=714),
ErrorDetail(string='2', code=714),
ErrorDetail(string='3', code=714)]
- dictionary인 경우
raise APIException(detail={
"에러1": "에러1 내용입니다.",
"에러2": "에러2 내용입니다.",
"에러3": "에러3 내용입니다."
}, code=714)
exc.detail
{'에러1': ErrorDetail(string='에러1 내용입니다.', code=714),
'에러2': ErrorDetail(string='에러2 내용입니다.', code=714),
'에러3': ErrorDetail(string='에러3 내용입니다.', code=714)}
- str인 경우
raise APIException(detail="에러1", code=714)
exc.detail
ErrorDetail(string='에러1', code=714)
인스턴스변수 detail은 이렇게 만들어 진다.
왜 굳이 복잡하게 ErrorDetail 클래스로 반환을 하는 것일까?
그리고 _get_error_details 함수에서 만들어지는 ErrorDetail 클래스가 뭘까?
ErrorDetail 클래스
class ErrorDetail(str):
"""
A string-like object that can additionally have a code.
"""
code = None
def __new__(cls, string, code=None):
self = super().__new__(cls, string)
self.code = code
return self
def __eq__(self, other):
r = super().__eq__(other)
if r is NotImplemented:
return NotImplemented
try:
return r and self.code == other.code
except AttributeError:
return r
def __ne__(self, other):
return not self.__eq__(other)
def __repr__(self):
return 'ErrorDetail(string=%r, code=%r)' % (
str(self),
self.code,
)
def __hash__(self):
return hash(str(self))
이 클래스는 간단하게 code라는 것이 추가로 가질 수 있는 문자열(str)이라고 생각하면 된다.
이렇게 ErrorDetail로 결과를 반환하는 이유는
APIException 클래스에 정의된 메서드 get_codes와 get_full_details를 실행하기 위해서다.
_get_codes 함수
def _get_codes(detail):
if isinstance(detail, list):
return [_get_codes(item) for item in detail]
elif isinstance(detail, dict):
return {key: _get_codes(value) for key, value in detail.items()}
return detail.code
ErrorDetail 객체에서 인스턴스변수 code를 반환한다.
_get_full_details 함수
def _get_full_details(detail):
if isinstance(detail, list):
return [_get_full_details(item) for item in detail]
elif isinstance(detail, dict):
return {key: _get_full_details(value) for key, value in detail.items()}
return {
'message': detail,
'code': detail.code
}
ErrorDetail 객체에서 인스턴스변수 code와 detail(string, 내용은 string 인스턴스 변수에 들어간다) 을 반환한다.
디테일하게 알아보자!
아래 사진을 보면 어떤 식으로 APIException이 처리되는지 알 수 있다.
APIException을 raise하는 경우 exc가 만들어지는 과정을 알게 되었다.
exc가 만들어지면 exc.detail이 생성되는데, 이때 이 exc.detail을 이용해서 exception_handler에서 Response를 생성한다.
(exception_handler 함수 참고: 바로가기)
- list, tuple 인 경우
- dictionary 인 경우
- str 인 경우
dictionary 형태로 반환하는 이유는 exception_handler 함수에서 isinstance로 분기처리하고 있기 때문이다.
if isinstance(exc.detail, (list, dict)):
data = exc.detail
else:
data = {'detail': exc.detail}
(exception_handler 함수 참고: 바로가기)
- api_exception.py:
커스텀 exception handler 정의
- custom_exceptions.py:
APIException 상속받은 커스텀 Exception 정의
- exception_codes.py:
커스텀 Exception 에 들어갈 에러 코드 상수 정의
from datetime import datetime
from rest_framework.views import exception_handler
from rest_framework import exceptions
from rest_framework.response import Response
from config.exceptions.custom_exceptions import CustomDictException
from config.settings import logger
from config.exceptions.exception_codes import STATUS_RSP_INTERNAL_ERROR
def custom_exception_handler(exc, context):
logger.error(f"[CUSTOM_EXCEPTION_HANDLER_ERROR]")
logger.error(f"[{datetime.now()}]")
logger.error(f"> exc")
logger.error(f"{exc}")
logger.error(f"> context")
logger.error(f"{context}")
response = exception_handler(exc, context)
if response is not None:
if isinstance(exc, exceptions.ParseError):
code = response.status_code
msg = exc.detail
elif isinstance(exc, exceptions.AuthenticationFailed):
code = response.status_code
msg = exc.detail
elif isinstance(exc, exceptions.NotAuthenticated):
code = response.status_code
msg = exc.detail
elif isinstance(exc, exceptions.PermissionDenied):
code = response.status_code
msg = exc.detail
elif isinstance(exc, exceptions.NotFound):
code = response.status_code
msg = exc.detail
elif isinstance(exc, exceptions.MethodNotAllowed):
code = response.status_code
msg = exc.detail
elif isinstance(exc, exceptions.NotAcceptable):
code = response.status_code
msg = exc.detail
elif isinstance(exc, exceptions.UnsupportedMediaType):
code = response.status_code
msg = exc.detail
elif isinstance(exc, exceptions.Throttled):
code = response.status_code
msg = exc.detail
elif isinstance(exc, exceptions.ValidationError):
code = response.status_code
msg = exc.detail
elif isinstance(exc, CustomDictException):
"""
APIException dictionary instance process
For Localization Error Control
아래와 같은 형태 필요
STATUS_RSP_INTERNAL_ERROR = {
'code': 'internal-error',
'default_message': 'unknown error occurred.',
'lang_message': {
'ko': '알 수 없는 오류.',
'en': 'unknown error occurred.',
}
}
CustomDictException(STATUS_RSP_INTERNAL_ERROR, {"키": "내용", "키": "내용"}) 으로 추가 가능
code 부분을 추가적인 내용을 넣는 방식으로 사용
"""
code = exc.detail.get('code')
if hasattr(context['request'], 'LANGUAGE_CODE'):
language_code = context['request'].LANGUAGE_CODE
msg = exc.detail.get(
'lang_message'
).get(
language_code
)
else:
msg = exc.detail.get(
'default_message'
)
if exc.args[1:]:
for key, val in exc.args[1].items():
response.data[key] = val
response.data.pop('default_message', None)
response.data.pop('lang_message', None)
else:
code = response.status_code
msg = "unknown error"
response.status_code = 200
response.data['code'] = code
response.data['message'] = msg
response.data['data'] = None
response.data.pop('detail', None)
return response
else:
STATUS_RSP_INTERNAL_ERROR['message'] = STATUS_RSP_INTERNAL_ERROR.pop('default_message', None)
STATUS_RSP_INTERNAL_ERROR['data'] = None
STATUS_RSP_INTERNAL_ERROR.pop('lang_message', None)
return Response(STATUS_RSP_INTERNAL_ERROR, status=200)
custom_exceptions.py
from rest_framework.exceptions import APIException
class CustomDictException(APIException):
status_code = 200
default_detail = 'unknown error.'
default_code = 'unknown-error'
exception_codes.py
STATUS_RSP_INTERNAL_ERROR = {
"code": "internal-error",
"default_message": "unknown error occurred.",
"lang_message": {
"ko": "알 수 없는 오류.",
"en": "unknown error occurred.",
}
}
에러는 django logger를 이용하여 남겼다.
custom_exception_handler 함수에서 에러가 발생하는 경우 매번 로그를 남긴다.
(코드로 가기)
(이 부분이 로그를 남기는 부분이다.)
custom_exception_handler 함수에서 response를 가공시켰다.
(코드로 가기)
예로 아래와 같은 APIView를 생성했다고 하자.
만약 접근하면 아래와 같이 api 예외 처리가 잘 잡힌다.
하지만 custom_exception_handler를 적용하지 않았으면 아래 사진과 같이 django 에러 혹은 django DEBUG=False로 했을 경우, Server 500 에러가 나왔을 것이다.
[작성기간]
2022-03-19 ~ 2022-03-20 19:47 (완)
정리하면서 그냥 무심코 지나갔던 지식을 DRF 코드를 까보고, 알게 재미있었다.
꽤 정리하는데 시간이 오래 걸렸고 힘들었지만 보람찬 공부였다!!