概要
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
します。
requirements.txt
flask gunicorn
> pip install -r requirements.txt
ステータスコードが正しく返せるかの検証なので、メソッドはGET
にしています。
返却する内容を''
とすると、Content-Type
がtext/html
になるので、make_response
のあとに、mimetype
を指定しています。
だめな方は''
ではなく、None
としています。
app.py
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
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
を利用するので、ローカルでも確認しておきます。
flask_runしてたコンソール
> gunicorn -b 127.0.0.1:5000 app:app --log-level DEBUG
gunicornしてないコンソール
> 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
としているのは任意で変更してください。
app.yaml
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のフレキシブル環境だとどうなるか設定を変更してみました。
app.yaml
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