LoginSignup
4
3

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-10-31

概要

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

4
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
3