概要

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-Typetext/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-TypeHttp-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

元記事はこちら

Google App Engine上のFlaskでレスポンスをNo Content(204)で返す方法を調べた