19
26

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

FlaskでシンプルにContent-Typeをチェックする(@content_type)

Last updated at Posted at 2017-08-17

1. はじめに

前回の記事ではflask、cerberus、peeweeを利用したマイクロサービスのコンセプトアプリを作成しました。今回はその際省略したflaskによるContent-Typeのチェック方法について説明します。また、Content-Typeをチェックする関数デコレータの作り方についても紹介したいと思います。

1.1. 今回紹介する@content_type関数デコレータの仕様

  • flaskのroute()関数デコレータを付与する関数(つまりREST API)に対して付与する
  • 許可するContent-Typeを関数デコレータの引数に指定する
  • HTTPリクエストヘッダのContent-Typeと引数で指定された値を比較する
  • 異なる場合はHTTPステータスの400エラーをレスポンスとして返却する
@content_type関数デコレータの利用例
# rest api
@app.route('/questions', methods=['POST'])
@content_type('application/json')
def register_question():

1.2. 検証環境

  • Python 2.7.13
  • Flask 0.12.2

2. HTTPリクエストヘッダはrequest.headersで参照

flaskではHTTPリクエストヘッダの情報をrequest.headersで参照することができます。
request.headersは辞書型のオブジェクトで、ヘッダ名がキーになっています。
ですので、Content-Typeをチェックする場合、Content-Typeをキーに値を取得します。
詳細については http://flask.pocoo.org/docs/0.12/api/#incoming-request-data を参照ください。

2.1. Content-Typeをチェックするサンプルコード(いまいち)

いまいちなサンプルコード
# -*- coding: utf-8 -*-
import os
from flask import Flask, abort, request, make_response, jsonify

# flask
app = Flask(__name__)

# rest api
@app.route('/questions', methods=['POST'])
def register_question():
    # コンテンツタイプ(application/json)のチェック
    if not request.headers.get("Content-Type") == 'application/json':
        error_message = {
            'error':'not supported Content-Type'
        }
        return make_response(jsonify(error_message), 400)

    # omitted : 業務ロジックの実行
    # 処理結果の返却(JSON形式)
    return make_response(jsonify({'result': True}))

@app.route('/questions/<string:question_code>', methods=['PUT'])
def update_question(question_code):
    # コンテンツタイプ(application/json)のチェック
    if not request.headers.get("Content-Type") == 'application/json':
        error_message = {
            'error':'not supported Content-Type'
        }
        return make_response(jsonify(error_message), 400)
    
    # omitted : 業務ロジックの実行
    # 処理結果の返却(JSON形式)
    return make_response(jsonify({'result': True}))

# main
if __name__ == "__main__":
    app.run(host=os.getenv("APP_ADDRESS", 'localhost'), \
    port=os.getenv("APP_PORT", 3000))

2.2. サンプルコードの問題点

Content-Typeのチェックは実施する箇所が多いため、コードクローン(Code Clone)/重複コード(Duplicate Code)の問題が発生してしまいます。
ユーティリティ機能として定義することもできますが、APIを呼ぶ前の前提条件を満たしていないため、あくまで個人的な感想ですが、ユーティリティを呼び出す処理も記述させたくありませんでした。

関数デコレータであれば、チェックを通過した場合、つまり前提条件を満たした場合にのみ、対象の関数(ここではREST API)を呼び出すようにできます。
結果として、以下に示す関数デコレータを実装することにしました。

3. モダンな感じに@content_type関数デコレータを作成

3.1. Content-Typeをチェックするサンプルコード

content_type_check_app.py
# -*- coding: utf-8 -*-
import os
from flask import Flask, abort, request, make_response, jsonify
import functools

# flask
app = Flask(__name__)

# check content-type decorator
def content_type(value):
    def _content_type(func):
        @functools.wraps(func)
        def wrapper(*args,**kwargs):
            if not request.headers.get("Content-Type") == value:
                error_message = {
                    'error': 'not supported Content-Type'
                }
                return make_response(jsonify(error_message), 400)

            return func(*args,**kwargs)
        return wrapper
    return _content_type

# rest api
@app.route('/questions', methods=['POST'])
@content_type('application/json')
def register_question():
    # omitted : 業務ロジックの実行
    # 処理結果の返却(JSON形式)
    return make_response(jsonify({'result': True}))

@app.route('/questions/<string:question_code>', methods=['PUT'])
@content_type('application/json')
def update_question(question_code):
    # omitted : 業務ロジックの実行
    # 処理結果の返却(JSON形式)
    return make_response(jsonify({'result': True}))

# main
if __name__ == "__main__":
    app.run(host=os.getenv("APP_ADDRESS", 'localhost'), \
    port=os.getenv("APP_PORT", 3000))

3.2. @content_typeを利用した結果

REST APIの関数に@content_type('application/json')を付与するだけになりました。
コードクローンの問題もなくなり、Content-Typeがapplication/jsonでなければならないことが直観的に分かるかと思います。
(必要であればapplication/jsonの定数化を行ってください)

ちなみに@app.route@content_typeの記述順を入れ替えるとエラーになります。
Pythonの関数デコレータはJavaのアノテーションのように目印ではなく関数の入れ子のため、入れ子にする順番が処理フローに影響します。
@content_typeの処理でflaskの機能を呼び出しているため、入れ子としては先にflaskが実行されている必要があるからです。

4. 動作確認

4.1. クライアントのソースコード

democlient.py
# -*- coding: utf-8 -*-
import requests
import json

def register_question_none_content_type(url, data):
    print("[POST] /questions : none content-type")
    response = requests.post(url, json.dumps(question))
    print(response.status_code)
    print(response.content)

def register_question(url, data):
    print("[POST] /questions : content-type application/json")
    response = requests.post(url, json.dumps(question), \
        headers={'Content-type': 'application/json'})
    print(response.status_code)
    print(response.content)

# main
if __name__ == "__main__":
    url = 'http://localhost:3000/questions'
    question = {
        'question_code' : '9999999999',
        'category' : 'demo',
        'message' : 'hello'
    }
    # Content-Typeなし
    register_question_none_content_type(url, question)
    # Content-Type : application/json
    register_question(url, question)

4.1.1. requestsでContent-Typeのapplication/jsonを指定しないように

前回の記事にも記載していますが、json=データのようにjson引数にデータを指定すると、requestsが自動でapplication/jsonのContent-Typeを設定します。
今回はこの機能を利用しないため、第二引数に直接送信データ(json.dumps(question))を設定しました。

4.2. サーバの起動

サーバの起動(と実行ログ)
C:\tmp\>python content_type_check_app.py
 * Running on http://localhost:3000/ (Press CTRL+C to quit)
127.0.0.1 - - [16/Aug/2017 20:02:30] "POST /questions HTTP/1.1" 400 -
127.0.0.1 - - [16/Aug/2017 20:02:30] "POST /questions HTTP/1.1" 200 -

実行ログの1行目がContent-Typeを指定していないHTTPリクエストで、HTTPステータスの400になっています。
2行目が指定したHTTPリクエストで、この場合は正常に処理を実行して200が返っています。

4.3. クライアントの起動

クライアントの実行
C:\tmp\python democlient.py
[POST] /questions : none content-type
400
{
  "error": "not supported Content-Type"
}

[POST] /questions : content-type application/json
200
{
  "result": true
}

Content-Typeを指定しなかったHTTPリクエストは設計通りにHTTPステータスの400エラーと、エラー内容の{'error':'not supported Content-Type'}が返却されることを確認しました。
次のリクエストはContent-Typeにapplication/jsonを指定しているため、Content-Typeチェックを通過し、HTTPステータスが200の正しい処理結果が返却されています。

5. さいごに

今回はflaskでContent-Typeをチェックする方法について説明しました。
flaskがContent-Typeでもリクエストマッピングができるようになれば、REST APIが呼ばれた時点でContent-Typeが保障されるので、今回作成した@content_typeは不要になります。

また、今回初めて関数デコレータを作成しましたが、AOPのように後から機能を追加できるため、他でもいろいろと使えそうな感じがしました。

19
26
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
19
26

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?