1. はじめに
前回の記事ではflask、cerberus、peeweeを利用したマイクロサービスのコンセプトアプリを作成しました。今回はその際省略したflaskによるContent-Typeのチェック方法について説明します。また、Content-Typeをチェックする関数デコレータの作り方についても紹介したいと思います。
1.1. 今回紹介する@content_type
関数デコレータの仕様
- flaskの
route()
関数デコレータを付与する関数(つまりREST API)に対して付与する - 許可するContent-Typeを関数デコレータの引数に指定する
- HTTPリクエストヘッダのContent-Typeと引数で指定された値を比較する
- 異なる場合はHTTPステータスの400エラーをレスポンスとして返却する
# 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をチェックするサンプルコード
# -*- 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. クライアントのソースコード
# -*- 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のように後から機能を追加できるため、他でもいろいろと使えそうな感じがしました。