発生した問題
- Djangoで404のときのエラーメッセージをデフォルトから変更したかったが、変更されなかった。
前提
Django : 2.x djangorestframework: 3.9.x djangorestframework-jsonapi: 2.4.0 ( これは最新でも同じソースコードになってた)
また、 GitHub - django-json-api/django-rest-framework-json-api: JSON API support for Django REST Framework に則って、 少なくとも以下のREST_FRAMEWORK の設定を行ってる。
REST_FRAMEWORK = { 'EXCEPTION_HANDLER': 'rest_framework_json_api.exceptions.exception_handler' }
再現した手順
元々のコードとしては以下のようなコードであった。
※ なお、ここで紹介するのは実コードでは無く、今回と同じ現象が発生するサンプルコード
from django.http import Http404 def some_method(self): object = self.get_some_object() if object is None: raise Http404(detail='データにアクセスする権限がありません。') return object
この場合、Django Rest Frameworkで設定した exception_handler にて 適切にハンドリングされて 以下のようにエラーが返った。
{"errors": [{"detail": "見つかりませんでした。", "source": {"pointer": "/data"}, "status": "404"}]}
本来は detail に設定したように 以下のメッセージが返ると期待していたが、何故か指定されなかった。
{"errors": [{"detail": "データにアクセスする権限がありません。", "source": {"pointer": "/data"}, "status": "404"}]}
対応した手順
from rest_framework.exceptions import NotFound def some_method(self): object = self.get_some_object() if object is None: raise NotFound(detail='データにアクセスする権限がありません。') return object
exception の クラスを Http404
クラス から NotFound
に変える事で正常稼働した。
何故そうなったのか
NotFound クラスについて
そもそも、 見つかりませんでした。
はどこが決めているのか? を考える必要がある。
これは、 実は NotFound()
クラスが持つ デフォルトの エラーメッセージである。
raise NotFound()
とした時は 上記ロジックの default_detail
のプロパティが各実行環境に応じて localize されてエラーメッセージが決まる。
NotFound() クラス と言わず rest_framework.exceptions
内に定義された APIException
クラスを継承したクラス全般において、上記のような default_detail
プロパティを持ち、もし指定されなければ デフォルトエラーメッセージが指定されて、指定されれば 上書きされる。
これは、 APIExceptionのコンストラクタから理解出来る.
if detail is None: detail = self.default_detail
なので raise Http404('hogehoge')
とした時は 'hogehoge'
が無視されて、 NotFound()
のエラーメッセージがデフォルトで入るみたいなロジックになっているのでは? みたいに当たりを付けた
エラー時のレスポンスについて
では、実際に エラーハンドリングがどのようにされて、結果、以下のレスポンスはどのように決まっているのかを考える。
{"errors": [{"detail": "***", "source": {"pointer": "/data"}, "status": "404"}]}
これは、exception_handler のロジックを読むことで理解が出来た。
まず、 setting に追加した exception_hanlder の所だが、以下となっている。
def exception_handler(exc, context): # Import this here to avoid potential edge-case circular imports, which # crashes with: # "ImportError: Could not import 'rest_framework_json_api.parsers.JSONParser' for API setting # 'DEFAULT_PARSER_CLASSES'. ImportError: cannot import name 'exceptions'.'" # # Also see: https://github.com/django-json-api/django-rest-framework-json-api/issues/158 from rest_framework.views import exception_handler as drf_exception_handler # Render exception with DRF response = drf_exception_handler(exc, context) if not response: return response # 略 ... return some_code()
rest_framework_json_api.exceptions.exception_handler
から rest_framework.views.exception_handler
を呼び出して、 そこで第一回目の判定を行っている。
まず一個目のif分が True なのか False なのか、ここを考える事で切り分けができていきそうだ。 当の rest_framework.views.exception_handler
のロジックはここにある。
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
という事で... 大体これで、現象の発生理由が理解出来た。
発生してた元々のコードは raise Http404
としてたが、 rest_framework.views.exception_handler
は そのクラスが throw された時は内部で raise NotFound()
に置き換える。
これにより、先述の通り Http404 に指定されたエラーメッセージは破棄されて、 NotFound クラスのデフォルトのメッセージがあたっていた、というわけだ。
NotFoundクラスは これも先述の通り APIExceptionの子クラスなので if isinstance(exc, exceptions.APIException):
の中に入り、結果的に エラーメッセージは デフォルトのNotFound() のエラーメッセージとなっていた。
逆に成功したパターンは if isinstance(exc, Http404):
は False だし elif isinstance(exc, PermissionDenied):
も該当しない。
結果 if isinstance(exc, exceptions.APIException):
に該当して、何もオーバーライドされる事もなく、正常に動作した、という事だった。
ちなみに理論上 raise APIException('データにアクセスする権限がありません。', status_code=404)
とかでもきっと カスタムエラーメッセージの404 エラーがはけそう。
ということで、微妙なこだわりの為 はまった、という事でしたw