HTTP リクエストとレスポンス

Description

Accessing and manipulating Zope’s HTTP request and response objects programmatically.

はじめに

この章では Zope での HTTP リクエストとレスポンスの基本について説明します。

  • リクエストとレスポンスオブジェクトのライフサイクル
  • 抽出されて編集できるデータ

ライフサイクル

他のいくつかの Web フレームワークとは異なり、Plone では HTTP レスポンスのオブジェクトを明示的に作成したり応答することができません。 HTTP リクエストのオブジェクトは常に HTTP レスポンスのオブジェクトと関連付けられます。また、レスポンスのオブジェクトは、リクエストを Web サーバーが受け取ったときに生成されます。

レスポンスはリクエストが処理されている間中、常に利用可能です。 これは、処理コードのどの段階でもレスポンスのヘッダーを設定したり修正ができるということです。

Web サーバ

通常 Plone は Zope の ZServer (Medusa として知られています) で動作します。 他には WSGI に対応した Repoze のような Web サーバーがあります。

Web サーバーは HTTP オブジェクトを構築する方法に影響を与える可能性があります。

HTTP リクエスト

全ての外から入ってくる HTTP リクエストは (Zope の) ZPublisherHTTPRequest オブジェクトで覆われます。

通常は構成パラメーターとして探索されたコンテクストとともに、view 機能またはインスタンスは HTTP リクエストオブジェクトを受け取ります。

view では HTTP リクエストに以下のようにアクセスできます。:

from Products.Five.browser import BrowserView

class SampleView(BrowserView):

    def __init__(context, request):
        # Each view instance receives context and request as construction parameters
        self.context = context
        self.request = request

    def __call__(self):
        # Entry point of request processing
        # Dump out incoming request variables
        print self.request.items()

リクエストメソッド

リクエストメソッド(GET または POST)は以下のようにして取得できます。:

request["REQUEST_METHOD"] == "POST" # or "GET"

リクエストURL

問い合わせられた URL を取得します。:

>>> request["ACTUAL_URL"]
'http://localhost:8080/site'

応答したオブジェクトの URL を取得できます。 デフォルトページとデフォルトビューの処理により、問い合わせ URL とは異なる場合があります。

>>> request["URL"]
'http://m.localhost:8080/site/matkailijallefolder/@@frontpage'

ノート

URLs, as accessed above, do not contain query string.

Query string

Unparsed query string can be accessed.

E.g. if you go to http://localhost:8080/site?something=foobar

 >>> self.request["QUERY_STRING"]
'something=foobar'

Query string is an empty string if it’s not present in HTTP request.

ノート

You can use request.form dictionary to access parsed query string content.

リクエストのパス

リクエスト URI のパスは request.path から取得できます。 request.path はパスの構成要素のリストです。 request.path はバーチャルパスであり、サイト id は含まれていません。

例:

reconstructed_path = "/".join(request.path) # will be

他に以下のヘッダーから取得が可能です。:

('PATH_INFO', '/plonecommunity/Members')
('PATH_TRANSLATED', '/plonecommunity/Members')

REQUEST_URI

PHP の REQUEST_URI に対応すると値は以下のようにすると取得できます。:

# Concatenate the user visible URL and query parameters
full_url = request.ACTUAL_URL + "?" + request.QUERY_STRING
parsed = urlparse.urlsplit(full_url)

# Extract path part and add the query if it existed
uri = parsed[2]
if parsed[3]:
    uri += "?" + parsed[3]

詳細な情報は以下を参照して下さい。

リクエストのクライアントIP

例:

def get_ip(request):
    """  Extract the client IP address from the HTTP request in proxy compatible way.

    @return: IP address as a string or None if not available
    """
    if "HTTP_X_FORWARDED_FOR" in request.environ:
        # Virtual host
        ip = request.environ["HTTP_X_FORWARDED_FOR"]
    elif "HTTP_HOST" in request.environ:
        # Non-virtualhost
        ip = request.environ["REMOTE_ADDR"]
    else:
        # Unit test code?
        ip = None

    return ip

以下を参照してください。

GET 変数

REQUEST_METHOD が GET のときは HTTP GET 変数は request.form で取得できます。

例:

# http://yoursite.com/@@testview/?my_param_id=something
print self.request.form["my_param_id"]

POST 変数

HTTP POST 変数は request.form に辞書型で保存されます。:

print request.form.items() # Everything POST brought to us

GET と POST の変数へは同じようにアクセスできます。

HTTP ヘッダー

HTTP ヘッダーは request.get_header()request.environ 辞書で取得できます。

例:

referer = self.request.get_header("referer") # Page referer (the page from user came from)

if referer == None: # referer will be none if it was missing
    pass

全てのヘッダーを出力します。:

for name, value in request.environ.items():
    print "%s: %s" % (name, value)

問い合わせ文字列

HTTP GET の問い合わせ文字列を取得できます。:

query_string = request["QUERY_STRING"]

Web の環境変数

Web サーバーの環境変数は request.other (ZServer) または request.environ (Repoze と他の WSGI ベースの Web サーバー) で参照できます。:

print request.other.items()

user_agent = request.other["HTTP_USER_AGENT"]

user_agent = request.environ["HTTP_USER_AGENT"] # WSGI or Repoze server

ホスト名

以下はバーチャルホストに対しても安全な方法で HTTP サーバーの名前をを取得する例です。

def get_host_name(request):
    """ Extract host name in virtual host safe manner

    @param request: HTTPRequest object, assumed contains environ dictionary

    @return: Host DNS name, as requested by client. Lowercased, no port part.
             Return None if host name is not present in HTTP request headers
             (e.g. unit testing).
    """

    if "HTTP_X_FORWARDED_HOST" in request.environ:
        # Virtual host
        host = request.environ["HTTP_X_FORWARDED_HOST"]
    elif "HTTP_HOST" in request.environ:
        # Direct client request
        host = request.environ["HTTP_HOST"]
    else:
        return None

    # separate to domain name and port sections
    host=host.split(":")[0].lower()

    return host

Request port

It is possible to extract Zope instance port from the request. This is useful e.g. for debugging purposes if you have multiple ZEO front ends running and you want to identify them easily.

port = request.get(“SERVER_PORT”, None)

ノート

SERVER_PORT variable returns port as a string, not integer.

ノート

This port number is not the one visible to the external traffic (port 80, HTTP)

フラットなアクセス

GET、POST と Web の環境変数はリクエストオブジェクトにフラットに保存されており辞書形式で参照できます。:

# Comes from POST
request["input_username"] == request.form["input_username"]

# Comes from environ
request.get('HTTP_USER_AGENT') == request.environ["HTTP_USER_AGENT"]

リクエストの mutability

HTTP リクエストオブジェクトを書き換えたり変数を追加しても、影響を与えることができません。 キャッシュ変数を作成する必要がある場合は、 annotations をリクエストのライフサイクルに使用します。 「TODO: annotaition の例を書いたら内部リンクを追加します。」

外部のコンテンツへ HTTP リクエストでアクセスする

HTTP リクエストオブジェクトをつかみたいと思う場合がしばしばありますが、フレームワークの下では取得できません。 そのような場合にリクエストオブジェクトにアクセスする2種類の方法があります。

  • 獲得を使用してサイトのルートからリクエストオブジェクトを取得します。 Plone サイトの探索を開始するとき、HTTP リクエストは不思議な site.REQUEST 変数に配置されます。
  • http://pypi.python.org/pypi/five.globalrequest

獲得を使用した HTTP リクエストを取得する例です。:

# context is any traversed Plone content item
request = getattr(context, "REQUEST", None)
if request is not None:
        # Do the normal flow
        pass
else:
        # This code path was not initiated by an incoming web server request
        # Handle cases like
        # - command line scripts
        # - add-on installer
        # - code called during Zope start up
        # -etc.
        pass

HTTP レスポンス

通常 view から HTTP レスポンスを直接返すことはありません。 その代わりに既存の(リクエストに付属した)HTTP レスポンスオブジェクトを変更して、 HTTPレスポンスのペイロードを返します。

応答するペイロードオブジェクトは以下が使用できます。

  • 文字列(str) - 8ビットの生データ
  • 反復可能(iterable) - レスポンスはバッファに蓄積されたメモリの代わりにストリームになります

レスポンスにアクセスする

リクエストを知っていれば HTTP レスポンスにアクセスできます。:

from Products.Five.browser import BrowserView

class SampleView(BrowserView):

    def __init__(context, request):
        # Each view instance receives context and request as construction parameters
        self.context = context
        self.request = request

    def __call__(self):
        response = self.request.response
        return "<html><body>Hello world!</body></html>"

レスポンスヘッダー

HTTPResponse の setHeader() メソッドを使用してヘッダーを設定します。:

# Response dynamically generated image
self.request.response.setHeader("Content-type", "image/jpeg")
return image_data

Content-Disposion ヘッダー

Content-Disposion ヘッダーはダウンロードのファイル名をセットするときに使用します。 Flash ダウンロードが有効かどうか確認するときに Flash 10 にも使用されます。

例としてダウンロード時にダウンロード対象となるファイル名を強制する方法を載せます。:

response = self.request.response
response.setHeader("Content-type", "text/x-vCard; charset=utf-8")
response.setHeader("Content-Transfer-Encoding", "8bit")

cd = 'attachment; filename=%s.vcf' % (context.id)
response.setHeader('Content-Disposition', cd)

その他の情報は以下を参照してください。

HTTP ステータスコード

HTTPResponse.setStatus(self, status, reason=None, lock=None) を使用して HTTP の応答ステータス(404 Not Found、500 Internal error等)を設定します。

lock=True を指定した場合は HTTPResponse をさらに修正することはできずエラーを返さずに失敗して終了します。

レスポンスの本文

レスポンスの本文を出力後のフックで読み込んだり操作したい場合があります。

レスポンスの本文は文字列や basestring ではないかも知れません。 BLOB データのためにジェネレーターや反復可能オブジェクトかも知れません。

本文は responce.body 属性に有効になります。

リダイレクト

responce.redirect() メソッドを使用します。:

# This will do 302 Temporary Redirect
response.redirect(new_url)

# This will do 301 Permantent Redirect
response.redirect(new_url, status=301)

ミドルウェアのようなフック

Plone はすべてを探索して実行するように、ミドルウェアの概念がありません。 ミドルウェアのような振る舞いを 探索の前 のフックで真似ることができます。 探索の前のフックは探索グラフ中のどんな永続的なオブジェクトにもインストールすることが可能です。 フックは永続的であるので、データベースの変更と特別な GenericSetup の Python コードを使用してインストールしなければなりません。

警告

探索の前のフックは新しい HTTP レスポンスを作成できないか、代わりの HTTP レスポンスを返すことができません。 HTTP のリダイレクトのような、例外的な HTTP レスポンスの変更のみに対応しています。 レスポンス全体を書き換えたい場合は出力後のフックを使用する必要があります。

より詳細な情報は以下を参照してください。

出力後のフック

出力後のフックは以下の時に実行されます。

  • コンテクストのオブジェクトが探索された後
  • view が呼ばれてレスポンスを生成した後
  • ブラウザーにレスポンスを送信する前
  • トランザクションがコミットされる前

これはキャッシュを行うために役立ちます。 レスポンスにキャッシュ用のヘッダーの内容を決定して挿入する理想的な場所です。

詳細は plone.postpublicationhook パッケージのページ を読んでください。

出力後のフックの便利な情報の抜粋

例:

from zope.component import adapter, getUtility, getMultiAdapter
from plone.postpublicationhook.interfaces import IAfterPublicationEvent

from Products.CMFCore.interfaces import IContentish

def get_contentish(object):
    """
    Traverse acquisition upwards until we get contentish object used for the HTTP response.
    """

    contentish = object
    while not IContentish.providedBy(contentish):

        if hasattr(contentish, "aq_parent"):
            contentish = contentish.aq_parent
        else:
            break

    return contentish


# This must be refered in configure.zcml
@adapter(Interface, IAfterPublicationEvent)
def language_fixer(object, event):
    """ Redirect mobile users to mobile site using gomobile.mobile.interfaces.IRedirector.

    Note: Plone does not provide a good hook doing this before traversing, so we must
    do it in post-publication. This adds extra latency, but is doable.
    """

    request = event.request
    response = request.response

    # object can be a page template, view, whichever happens to be at the very end of traversed acquisition chain
    context = get_contentish(object)