✂️

[Django]http_method_namesの指定は通常不要な理由

2021/09/22に公開

はじめに

Djangoのクラスベースビューでは http_metho_names という属性が定義されています。これはサポートしているHTTPメソッドを定義するためのものです。

ですが通常は設定不要です。この理由について記載します。

以下は Django 3.2.7で確認しています。

http_method_names による挙動の違い

まずシンプルなクラスベースビューの1つ、TemplateViewを使って挙動を確認してみます。TemplateViewは次のように、 get() インスタンスメソッドが定義されています。

class TemplateView(TemplateResponseMixin, ContextMixin, View):
    """
    Render a template. Pass keyword arguments from the URLconf to the context.
    """
    def get(self, request, *args, **kwargs):
        context = self.get_context_data(**kwargs)
        return self.render_to_response(context)

この TemplateView を使ったViewの使用例は次のようになります。

from django.views.generic import TemplateView

class VanillaTemplateView(TemplateView):
    template_name = "example.html"

このViewに対してアクセスすると、次のようになります。

  • GET: 成功
  • POST: 失敗(405 Method Not Allowed)

正確には GETメソッドだけでなく、HEAD, OPTIONSメソッドも成功します。ただしHEADメソッドはヘッダだけ返すのではなく、GETメソッドと同じ挙動になります。

$ telnet localhost 8000
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
OPTIONS /vanilla/ HTTP/1.1
Host: 127.0.0.1

HTTP/1.1 200 OK
Date: Mon, 20 Sep 2021 12:06:02 GMT
Server: WSGIServer/0.2 CPython/3.9.7
Content-Type: text/html; charset=utf-8
Allow: GET, HEAD, OPTIONS
(以下略)

post() メソッドを実装したときの挙動の変化

このViewに post() インスタンスメソッドの定義を加えてみます。

from django.views.generic import TemplateView

class PostSupportView(TemplateView):
    template_name = "example.html"

    def post(self, request, *args, **kwargs):
        return self.get(request, *args, **kwargs)

すると、POSTメソッドも成功します。OPTIONSメソッドを実行すると、先ほどと比べ、POSTメソッドが加わっていることが分かります。

$ telnet localhost 8000
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
OPTIONS /post_support/ HTTP/1.1
Host: 127.0.0.1

HTTP/1.1 200 OK
Date: Mon, 20 Sep 2021 12:08:22 GMT
Server: WSGIServer/0.2 CPython/3.9.7
Content-Type: text/html; charset=utf-8
Allow: GET, POST, HEAD, OPTIONS

http_method_names を定義してみる

このViewに対して http_metho_names を定義するとどうなるでしょうか。

from django.views.generic import TemplateView

class PostWithHttpMethodNamesView(TemplateView):
    http_method_names = ["get"]
    template_name = "example.html"

    def post(self, request, *args, **kwargs):
        return self.get(request, *args, **kwargs)

すると、GETメソッドのみが有効になり、POSTメソッドが無効になります。HEAD, OPTIONSメソッドも無効になります。

$ telnet localhost 8000
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
OPTIONS /post_with_http_method_names/ HTTP/1.1
Host: 127.0.0.1

HTTP/1.1 405 Method Not Allowed

http_method_names = ["get", "options"] とすると、OPTIONSメソッドも使えます。ただし、HEADメソッドは使えません。

$ telnet localhost 8000
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.

OPTIONS /post_with_http_method_names/ HTTP/1.1
Host: 127.0.0.1

HTTP/1.1 200 OK
Date: Mon, 20 Sep 2021 12:13:41 GMT
Server: WSGIServer/0.2 CPython/3.9.7
Content-Type: text/html; charset=utf-8
Allow: GET, OPTIONS

これらの挙動から、次のことが推測されます。

  1. HTTPメソッドに対応したインスタンスメソッドを定義することで、そのHTTPメソッドが有効になる。
  2. ただしHEAD, OPTIONSメソッドについてはデフォルトで定義されている。
  3. インスタンスメソッドが未定義の場合、405 Method Not Allowed になる。
  4. http_method_namesを定義すると、1.〜3.の挙動を上書きする。

この挙動をソースコードで確認してみます。

Djangoでの http_method_names の定義

Django本体(テスト・ドキュメント除く)で http_method_names が出てくるのは django/views/generic/base.py 1ファイルだけです。このファイルに4回出てきます。

http_method_names と関係する箇所のみ抜き出したコードは次になります。

class View:
    # ①
    http_method_names = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace']

    @classonlymethod
    def as_view(cls, **initkwargs):
        """Main entry point for a request-response process."""
        for key in initkwargs:
            # ②
            if key in cls.http_method_names:
                raise TypeError(
                    'The method name %s is not accepted as a keyword argument '
                    'to %s().' % (key, cls.__name__)
                )
            if not hasattr(cls, key):
                raise TypeError("%s() received an invalid keyword %r. as_view "
                                "only accepts arguments that are already "
                                "attributes of the class." % (cls.__name__, key))

        # ここからは無関係なので省略

    def dispatch(self, request, *args, **kwargs):
        # Try to dispatch to the right method; if a method doesn't exist,
        # defer to the error handler. Also defer to the error handler if the
        # request method isn't on the approved list.
        # ③
        if request.method.lower() in self.http_method_names:
            handler = getattr(self, request.method.lower(), self.http_method_not_allowed)
        else:
            handler = self.http_method_not_allowed
        return handler(request, *args, **kwargs)

    def options(self, request, *args, **kwargs):
        """Handle responding to requests for the OPTIONS HTTP verb."""
        response = HttpResponse()
        response.headers['Allow'] = ', '.join(self._allowed_methods())
        response.headers['Content-Length'] = '0'
        return response

    def _allowed_methods(self):
        # ④
        return [m.upper() for m in self.http_method_names if hasattr(self, m)]

まず①で http_method_names のデフォルトが定義されています。これは HTTP リクエストメソッド のうち、 CONNECT を除く9個が定義されています。

②では、 as_view() の引数のキーに対し、 http_method_names に含まれていないことを検証しています。クラスベースビューでは TemplateView.as_view(template_name="example.html") のように as_view() に引数を渡すことができるのですが、 TemplateView.as_view(post=post_method) のような書き方を禁止しています。

③はリクエストが到達した際に最初に呼ばれるメソッドである、 dispatch() の内容です。リクエストメソッドを小文字にしたものが http_method_names に含まれていれば、対応するクラスメソッドを取得しています。すなわち、次のような挙動になります。

  1. http_method_names に定義されていないHTTPメソッドは、インスタンスメソッドの有無に関わらず、 405 Method Not Allowed になる。
  2. http_method_names に定義されているHTTPメソッドは、インスタンスメソッドの存在をチェックし、それを呼び出す。

①で http_method_names のデフォルトとしてほとんどのHTTPメソッドが定義されているため、実質的にはHTTPメソッドに対応するインスタンスメソッドが定義されていれば有効になります。Viewクラスには options() メソッドが定義されているので、OPTIONSメソッドがデフォルトで有効になっていることが分かります。

④はその options() メソッドで呼び出されるインスタンスメソッドで、 http_method_names の各メソッドに対し、インスタンスメソッドが定義されているものを返すようになっています。

HEADメソッドがデフォルトで有効な理由

実際の挙動から、次のように推測しました。このうち1.と3.と4.については先ほど説明した通りです。残っているのは2.のうち、HEADメソッドが定義されている理由です。

  1. HTTPメソッドに対応したインスタンスメソッドを定義することで、そのHTTPメソッドが有効になる。
  2. ただしHEAD, OPTIONSメソッドについてはデフォルトで定義されている。
  3. メソッドが未定義の場合、405 Method Not Allowed になる。
  4. http_method_namesを定義すると、1.〜3.の挙動を上書きする。

HEADメソッドは、Viewクラスにある setup() メソッドで定義されています。

    def setup(self, request, *args, **kwargs):
        """Initialize attributes shared by all view methods."""
        if hasattr(self, 'get') and not hasattr(self, 'head'):
            self.head = self.get
        self.request = request
        self.args = args
        self.kwargs = kwargs

これを読み解くと、 get() が定義されていて、 head() が未定義のときは head() の定義は get() と同じになる となります。これで2.のHEADがデフォルトで定義されている理由も説明できました。

明示的に http_method_names の定義が必要な場面

Djangoのクラスベースビューでは、Viewクラス以外に http_method_names を定義、あるいは使用している箇所はありません。 get() メソッドを定義すれば自動的に GETメソッドが有効になり、 post() メソッドを定義すれば自動的に POSTメソッドが有効になります。そのため、 http_method_names の定義が必要な場面はほとんどありません。

例外は、クラスベースビューの機能を使いつつ、デフォルトのHTTPメソッドを無効にしたい場合です。例えば、UpdateViewを使ってモデルの変更を行いたいが、編集画面は表示させたくない場合です。このときは http_method_names を使って get() を無効化するのがいいでしょう。

サンプルコード

こちらに掲載しています。

https://github.com/ikemo3/django-example/pull/15

Discussion