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

## 1. はじめに
[前回の記事](http://qiita.com/5zm/items/d5375a4ad95fd67b8960)ではflask、cerberus、peeweeを利用したマイクロサービスのコンセプトアプリを作成しました。今回はその際省略したflaskによるContent-Typeのチェック方法について説明します。また、Content-Typeをチェックする関数デコレータの作り方についても紹介したいと思います。

### 1.1. 今回紹介する`@content_type`関数デコレータの仕様
* flaskの`route()`関数デコレータを付与する関数（つまりREST API）に対して付与する
* 許可するContent-Typeを関数デコレータの引数に指定する
* HTTPリクエストヘッダのContent-Typeと引数で指定された値を比較する
* 異なる場合はHTTPステータスの400エラーをレスポンスとして返却する

```python:@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をチェックするサンプルコード（いまいち）

```python:いまいちなサンプルコード
# -*- 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をチェックするサンプルコード
```python: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. クライアントのソースコード

```python: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を指定しないように
[前回の記事](http://qiita.com/5zm/items/d5375a4ad95fd67b8960)にも記載していますが、`json=データ`のようにjson引数にデータを指定すると、requestsが自動でapplication/jsonのContent-Typeを設定します。
今回はこの機能を利用しないため、第二引数に直接送信データ（`json.dumps(question)`）を設定しました。


### 4.2. サーバの起動

```console:サーバの起動（と実行ログ）
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. クライアントの起動

```console:クライアントの実行
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のように後から機能を追加できるため、他でもいろいろと使えそうな感じがしました。