import json import copy from django.utils.translation import gettext_lazy from django.core.serializers.json import DjangoJSONEncoder from rest_framework.response import Response # origin # https://djangosnippets.org/snippets/10717/ # https://docs.djangoproject.com/en/4.0/ref/request-response/#httpresponse-object class ApiWrapperMiddleware: def __init__(self, get_response): self.get_response = get_response # One-time configuration and initialization. self.pagination_response_keys = ['pag_cnt', 'pag_page_size', 'pag_cur', 'pag_next', 'pag_prev'] self.default_response_keys = [ 'status', 'data', 'msg', # basic custom fields 'detail', 'non_field_errors', # DJANGO error indicators # 'pag_cnt', 'pag_page_size', 'pag_cur', 'pag_next', 'pag_prev', # pagination fields ] + self.pagination_response_keys # I suggest using message translation map self.lang_localization_map = { 'en-us': { "internal": "Internal Server Error", "unknown": "Unknown Server Error", "success": "Success", "pag_err": "Have you just tried to use pagination? \ Make sure you've provided all this fileds: " + ', '.join(self.pagination_response_keys), }, 'ru-ru': { "internal": "Сервис недоступен", "unknown": "Сервис недоступен", "success": "Успех", "pag_err": "Вы хотели использовать пагинацию? \ Убедитесь, что верно указали все следующие поля: " + ', '.join(self.pagination_response_keys), } } # probably you may like to use LANGUAGE_CODE from settings # than import it here and there from common config to # resolve cycle dependency self.LANGUAGE_CODE = 'ru-ru' if self.LANGUAGE_CODE == None: self.LANGUAGE_CODE = 'en-us' if self.LANGUAGE_CODE in self.lang_localization_map: self.localization = self.lang_localization_map[self.LANGUAGE_CODE] else: print('[ api_wrapper_middleware ]', self.LANGUAGE_CODE, 'encoding is not supported. English message description is used') self.localization = self.lang_localization_map['en-us'] print('[ api_wrapper_middleware ] is active!') def __call__(self, request): # Code to be executed for each request before # the view (and later middleware) are called. response = self.get_response(request) # Code to be executed for each request/response after # the view is called # We need to assure if DRF Response type AND is JSON AND data is really JSON-object # that might be FileResponnse or HTTPResponse from your other django-apps if isinstance(response, Response): print('[ api_wrapper_middleware ] [C-T]:',response.get('Content-Type')) # __ response.get('Content-Type') __ usually looks like for DRF: __ application/json; charset=utf-8 __ if response.get('Content-Type').lower().find('application/json') != -1 and \ isinstance(response.data, dict): print('[ api_wrapper_middleware ] FOUND JSON!!!') try: response_data = self.render_response(response) response.data = response_data response.content = json.dumps(response_data, cls=DjangoJSONEncoder) except Exception as e: print('[ api_wrapper_middleware ] [ERROR]', e) pass else: print('[ api_wrapper_middleware ] ignored') return response def render_response(self, response): """ function to fixed the response API following with this format: __Default Response Structure__ [1] success single { "status": int64, // , "msg": "", // "data": object, } [2] success list { "status": int64, // , "msg": "", // "data": object[], "count": int64, "page_size": int64, "cur_page": int64, "next": int64, // , "prev": null, } [3] failed { "status": int64, // 4** and 5**, "msg": string, // "data": {}, // empty object } """ data_copy = copy.deepcopy(response.data) response_data = { 'data': {}, 'msg': "", 'status': response.status_code, } # classic django error message propogation mechanism suggest using 'detail' key # https://www.django-rest-framework.org/api-guide/exceptions/#custom-exception-handling # 'non_field_errors' key appearce when dealing with forms # https://docs.djangoproject.com/en/4.0/ref/forms/api/#django.forms.Form.non_field_errors # updating the response msg if 'detail' in data_copy: response_data.update({'msg': data_copy.get('detail')}) del data_copy['detail'] # this may help to display form validation error | probably it is better to do in # your frontend part. I do so and I've commented corresponding strings elif 'non_field_errors' in data_copy: # response_errors = '
'.join(data_copy.get('non_field_errors')) # response_data.update({'msg': response_errors}) response_data.update({'msg': data_copy['non_field_errors']}) del data_copy['non_field_errors'] # store the internal errors messages. Responses with 4** and 5** codes are considered to be errors # https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#1xx_informational_response if response.status_code >= 400: response_errors_msgs = [] response_errors_keys = [] for (key, value) in data_copy.items(): # DRF places its error messages in JSON key-value pair as a value # of corresponing request key field. So all key-value pairs that # are not follow __Default Response Structure__ are error messages # E.g. I performed registration with 3 fields: email (unique), login (unique) and password. # When I try to register anather user with same parametres, DRF returns the following: # { "email": "user with this email already exists." } # and there is no other correct fields if key not in self.default_response_keys: errors = ' '.join([str(v) for v in value]) response_errors_msgs.append('%s: %s' % (key, errors)) response_errors_keys.append(key) if len(response_errors_msgs) > 0: # if you want to directly display it on form, uncomment the following # response_errors_msgs = '
'.join(response_errors_msgs) response_errors_msgs = '\n'.join(response_errors_msgs) response_data.update({'msg': response_errors_msgs}) # deleting the errors in the field keys. # makes no sence if all the extra fields are considered as errors # if len(response_errors_keys) > 0: # list(map(data_copy.pop, response_errors_keys)) if not response_data.get('msg'): if response.status_code < 500: response_data.update({'msg': gettext_lazy(self.localization['unknown'])}) else: response_data.update({'msg': gettext_lazy(self.localization['internal'])}) # 1** codes are information messages. I Consider data to be empty. Not so useful. elif response.status_code >= 100 and response.status_code < 200: if not response_data.get('msg'): response_data.update({'msg': gettext_lazy(self.localization['success'])}) # 2** and 3** codes else: if all([x in data_copy for x in self.pagination_response_keys]): for key in self.pagination_response_keys: response_data.update({key: data_copy[key]}) del data_copy[key] elif any([x in data_copy for x in self.pagination_response_keys]): err_msg = self.localization['pag_err'] print('[ api_wrapper_middleware ]', err_msg) response_data.update({'msg': '\n'.join([response_data.get('msg') + err_msg])}) response_data.update({'data': data_copy}) return response_data