概要
Flaskで実装したRESTfulなAPIをローカルで検証してから、Google App Engine(GAE)にデプロイしたところ、謎の500エラーに遭遇しました。
調べてみたら、No Content(204)
ステータスコードでレスポンスする実装がよろしくなかったのが原因でした。
下記は問題がない実装です。
from flask import Flask, make_response
app = Flask(__name__)
@app.route('/good_no_content', methods=['GET'])
def good_no_content():
response = make_response('', 204)
response.mimetype = app.config['JSONIFY_MIMETYPE']
return response
if __name__ == '__main__':
app.run()
これだと、ローカル上でもGAE上でも
> curl http://~~~/good_no_content -o /dev/null -w '%{content_type}\n%{http_code}\n' -s
application/json
204
っていい感じにレスポンスが返ってきます。
再現してみる
GitHubにもソースをアップしていますので、ご参考ください。
https://github.com/kai-kou/how-to-use-gae-no-content
ローカルで動作確認
まずはローカルで動作するようにします。
Pythonの環境は直でも仮想環境上でもDocker上でもご自由にどうぞ。
ここではvenv
を利用して仮想環境を作ってます。
> mkdir 任意のディレクトリ
> cd 任意のディレクトリ
> python -m venv venv
> . venv/bin/activate
> touch app.py
> touch requirements.txt
あとで、GAEにデプロイするので、requirements.txt
ファイルを作成してからpip install
します。
flask
gunicorn
> pip install -r requirements.txt
ステータスコードが正しく返せるかの検証なので、メソッドはGET
にしています。
返却する内容を''
とすると、Content-Type
がtext/html
になるので、make_response
のあとに、mimetype
を指定しています。
だめな方は''
ではなく、None
としています。
from flask import Flask, jsonify, make_response
app = Flask(__name__)
# GAEでも動作する
@app.route('/good_no_content', methods=['GET'])
def good_no_content():
response = make_response('', 204)
response.mimetype = app.config['JSONIFY_MIMETYPE']
return response
# GAEで500エラーになる
@app.route('/bad_no_content', methods=['GET'])
def bad_no_content():
response = make_response(jsonify(None), 204)
return response
if __name__ == '__main__':
app.run()
環境が用意できたので、ローカル上で動作確認します。
> flask run
> curl 127.0.0.1:5000/good_no_content -o /dev/null -w '%{content_type}\n%{http_code}\n' -s
application/json
204
> curl 127.0.0.1:5000/bad_no_content -o /dev/null -w '%{content_type}\n%{http_code}\n' -s
application/json
204
両方とも正しく動作します。Flaskのログをみても問題ありません。
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
127.0.0.1 - - [30/Oct/2018 11:32:00] "GET /good_no_content HTTP/1.1" 204 -
127.0.0.1 - - [30/Oct/2018 11:32:23] "GET /bad_no_content HTTP/1.1" 204 -
GAEではgunicorn
を利用するので、ローカルでも確認しておきます。
> gunicorn -b 127.0.0.1:5000 app:app --log-level DEBUG
> curl 127.0.0.1:5000/good_no_content -o /dev/null -w '%{content_type}\n%{http_code}\n' -s
application/json
204
> curl 127.0.0.1:5000/bad_no_content -o /dev/null -w '%{content_type}\n%{http_code}\n' -s
application/json
204
こちらも両方とも動作します。
[2018-10-30 11:37:53 +0900] [62134] [INFO] Starting gunicorn 19.9.0
[2018-10-30 11:37:53 +0900] [62134] [DEBUG] Arbiter booted
[2018-10-30 11:37:53 +0900] [62134] [INFO] Listening at: http://127.0.0.1:5000 (62134)
[2018-10-30 11:37:53 +0900] [62134] [INFO] Using worker: sync
[2018-10-30 11:37:53 +0900] [62139] [INFO] Booting worker with pid: 62139
[2018-10-30 11:37:53 +0900] [62134] [DEBUG] 1 workers
[2018-10-30 11:37:54 +0900] [62139] [DEBUG] GET /good_no_content
[2018-10-30 11:38:02 +0900] [62139] [DEBUG] GET /bad_no_content
GAEで確認
GAEにデプロイして確認してみます。以下前提の手順となります。
- GCPプロジェクトでGAEが利用可能
-
gcloud
がインストール済み
> touch app.yaml
ここではスタンダード環境にデプロイします。
service
を指定しないと、default
にデプロイされますので、ご注意ください。how-to-use-no-content-status
としているのは任意で変更してください。
runtime: python37
env: standard
service: how-to-use-no-content-status
entrypoint: gunicorn -b :$PORT app:app --log-level DEBUG
runtime_config:
python_version: 3
automatic_scaling:
min_idle_instances: automatic
max_idle_instances: automatic
min_pending_latency: automatic
max_pending_latency: automatic
デプロイします。
> gcloud app deploy
Services to deploy:
descriptor: [任意のディレクトリ/app.yaml]
source: [任意のディレクトリ]
target project: [GCPのプロジェクトID]
target service: [how-to-use-no-content-status]
target version: [20181030t114334]
target url: [https://how-to-use-no-content-status-dot-[GCPのプロジェクトID].appspot.com]
Do you want to continue (Y/n)? Y
Beginning deployment of service [how-to-use-no-content-status]...
(略)
Deployed service [how-to-use-http-status-code] to [https://how-to-use-no-content-status-dot-[GCPのプロジェクトID].appspot.com]
You can stream logs from the command line by running:
$ gcloud app logs tail -s how-to-use-no-content-status
To view your application in the web browser run:
$ gcloud app browse -s how-to-use-no-content-status
デプロイできたらアクセスしてみます。
> curl https://how-to-use-no-content-status-dot-[GCPのプロジェクトID].appspot.com/good_no_content -o /dev/null -w '%{content_type}\n%{http_code}\n' -s
application/json
204
> curl https://how-to-use-no-content-status-dot-[GCPのプロジェクトID].appspot.com/bad_no_content -o /dev/null -w '%{content_type}\n%{http_code}\n' -s
text/html; charset=UTF-8
500
bad_no_content
でエラーになりました。。。
GAEのログをみても詳細はわかりません。。。むむむ。。。
> gcloud app logs read -s how-to-use-no-content-status
2018-10-30 02:46:21 how-to-use-http-status-code[20181030t114334] "GET /good_no_content HTTP/1.1" 204
2018-10-30 02:46:31 how-to-use-http-status-code[20181030t114334] "GET /bad_no_content HTTP/1.1" 500
GAEの環境を変更してみる
GAEのフレキシブル環境だとどうなるか設定を変更してみました。
runtime: python
env: flex
service: how-to-use-http-status-code
entrypoint: gunicorn -b :$PORT app:app --log-level DEBUG
runtime_config:
python_version: 3
> gcloud app deploy
> curl https://how-to-use-no-content-status-dot-[GCPのプロジェクトID].appspot.com/good_no_content -o /dev/null -w '%{content_type}\n%{http_code}\n' -s
application/json
204
> curl https://how-to-use-no-content-status-dot-[GCPのプロジェクトID].appspot.com/bad_no_content -o /dev/null -w '%{content_type}\n%{http_code}\n' -s
000
Content-Type
とHttp-Code
が取得できなったので、-v
で。
> curl https://how-to-use-no-content-status-dot-[GCPのプロジェクトID].appspot.com/bad_no_content -v
(略)
* Connection state changed (MAX_CONCURRENT_STREAMS updated)!
* http2 error: Invalid HTTP header field was received: frame type: 1, stream: 1, name: [content-length], value: [5]
* HTTP/2 stream 1 was not closed cleanly: PROTOCOL_ERROR (err 1)
* Closing connection 0
* TLSv1.2 (OUT), TLS alert, Client hello (1):
curl: (92) HTTP/2 stream 1 was not closed cleanly: PROTOCOL_ERROR (err 1)
むむむ。フレキシブル環境だとDockerコンテナで動作するのでいけるかな?と思ったのですが、なにか違ったエラーがでてきました。
まとめ
原因がつかめずモヤモヤしますが、ひとまず、HTTPステータスコードをNo Content(204)で返すときは下記のようにするのがよさそうです。
from flask import Flask, make_response
app = Flask(__name__)
@app.route('/good_no_content', methods=['GET'])
def good_no_content():
response = make_response('', 204)
response.mimetype = app.config['JSONIFY_MIMETYPE']
return response
参考
Flask return 204 No Content response
https://www.erol.si/2018/03/flask-return-204-no-content-response/
cURLでHTTPステータスコードだけを取得する
https://qiita.com/mazgi/items/585348b6cdff3e320726