Google App Engine上でFlaskを利用してNo Content(204)を返すとき、 ```Content-Length``` が0じゃないとエラーになる


概要

前回、Google App EngineでFlaskを利用する際のNo Content(204)ステータスコードの利用方法を調査したけれど、原因がいまいち特定できていませんでした。
原因がわかりました!

Google App Engine上のFlaskでレスポンスをNo Content(204)で返す方法を調べた
https://qiita.com/kai_kou/items/801ae9715b5b8f4736b8

原因

No Content(204)を返すとき、Content-Length0 じゃないと、GAEでgunicornプロセスが落ちる(白目

検証

前回の記事のソースを利用して検証します。

GitHubにもソースをアップしていますので、ご参考ください。
https://github.com/kai-kou/how-to-use-gae-no-content

環境設定
> git clone https://github.com/kai-kou/how-to-use-gae-no-content.git
> cd how-to-use-gae-no-content
> python -m venv venv
> . venv/bin/activate
> pip install -r requirements.txt

環境が用意できたらflaskを起動します。

> flask run

curl で確認してみます。

flask_runしていないコンソール
> curl 127.0.0.1:5000/good_no_content -i
HTTP/1.0 204 NO CONTENT
Content-Type: application/json
Content-Length: 0
Server: Werkzeug/0.14.1 Python/3.6.6

> curl 127.0.0.1:5000/bad_no_content -i
HTTP/1.0 204 NO CONTENT
Content-Type: application/json
Content-Length: 5
Server: Werkzeug/0.14.1 Python/3.6.6

はい。GAEにもデプロイしています。
GitHubからソース取得している場合、app.yamlservice を変更するか削除してください。

> touch app.yaml
> gcloud app deploy

デプロイできたら確認してみます。

> curl https://[GAEのサービス名]-dot-[GCPのプロジェクトID].appspot.com/good_no_content -i
HTTP/2 204
content-type: application/json
x-cloud-trace-context: 436098c2c9196bd34b10448cb57643b0;o=1
server: Google Frontend
alt-svc: quic=":443"; ma=2592000; v="44,43,39,35"

> curl https://[GAEのサービス名]-dot-[GCPのプロジェクトID].appspot.com/bad_no_content -i
HTTP/2 500
x-cloud-trace-context: 0f98972aa581c03afa7af2d39596b8af;o=1
content-type: text/html; charset=UTF-8
server: Google Frontend
content-length: 323
alt-svc: quic=":443"; ma=2592000; v="44,43,39,35"
()

はい。bad_no_content はやはりエラーになります。

前回はHTTPステータスコードとContent-Type しかみてなかったのですが、ローカル実行時のContent-Length に違いがあることのがわかりました。

# Good
Content-Length: 0

# Bad
Content-Length: 5

そのへんから情報を漁っていると、Flask-RESTfulというライブラリのGitHubにそれらしきIssueがありました。。。

204 status returns non-zero content-length
https://github.com/flask-restful/flask-restful/issues/736

試しに、だめな方の実装を変更してみます。headersContent-Length を追加します。

app.py(一部)
# GAEでエラーになる
@app.route('/bad_no_content', methods=['GET'])
def bad_no_content():
  response = make_response(jsonify(None), 204)
  response.headers['Content-Length'] = 0
  return response

> flask run
flask_runしていないコンソール
> curl 127.0.0.1:5000/bad_no_content -i
HTTP/1.0 204 NO CONTENT
Content-Type: application/json
Content-Length: 0
Server: Werkzeug/0.14.1 Python/3.6.6

GAEにデプロイして確認します。

> gcloud app deploy

> curl https://[GAEのサービス名]-dot-[GCPのプロジェクトID].appspot.com/bad_no_content -i
HTTP/2 204
content-type: application/json
x-cloud-trace-context: b26c9b04d914cbc0e38de16f8d7fe28f;o=1
server: Google Frontend
alt-svc: quic=":443"; ma=2592000; v="44,43,39,35"

おぅ。。。エラーが発生しなくなりましたぁぁぁ。

嫌がらせに以下のように変更して実行してみます。コンテンツを明示的に返すようにします。

app.py(一部)
# GAEでエラーになる
@app.route('/bad_no_content', methods=['GET'])
def bad_no_content():
  # コンテンツを設定してみる
  response = make_response(jsonify({'message':'hoge'}), 204)
  response..headers['Content-Length'] = 0
  return response

> flask run
flask_runしていないコンソール
> curl 127.0.0.1:5000/bad_no_content -i
HTTP/1.0 204 NO CONTENT
Content-Type: application/json
Content-Length: 0
Server: Werkzeug/0.14.1 Python/3.6.6

GAEにデプロイして確認します。

> gcloud app deploy

> curl https://[GAEのサービス名]-dot-[GCPのプロジェクトID].appspot.com/bad_no_content -i
HTTP/2 204
content-type: application/json
x-cloud-trace-context: 27edb7f43586aec504497cc61b46be4d
date: Tue, 30 Oct 2018 08:48:21 GMT
server: Google Frontend
alt-svc: quic=":443"; ma=2592000; v="44,43,39,35"

ヘッダー優先みたいですね。

ついでに''None とでレスポンス内容の際をみてみました。
jsonify を利用すると改行が入ってしまう模様。イラナイヨォ
Nonenull に変換されます。

レスポンス実装 レスポンス内容
make_response('', 204).data b''
make_response(None, 204).data エラー
make_response(jsonify(''), 204).data b'""\n'
make_response(jsonify(None), 204).data b'null\n'

make_responseが面倒だ!

make_response を利用せず、以下のように実装することもできます。

headers = {
  'Content-Type': app.config['JSONIFY_MIMETYPE'],
  'Content-Length': 0
}
return '', 204, headers

まとめ

No Content(204)を返すとき、Content-Length0 じゃないと、GAEでgunicornプロセスが落ちるから気をつけましょう(再掲

前回は原因不明のまま、回避策だけを見出して終わったのですが、今回、なんとか原因がわかって、モヤモヤが晴れました^^

こういった問題が起こり得るので、Dockerイメージでほぼ同一環境!最強!ヒャッハーと油断しているといざクラウド環境に上げてから、痛い目に合いそうで(実際合った)怖いですね。

早めに運用環境で検証しておくのに越したことはないですね。教訓!

参考

204 status returns non-zero content-length
https://github.com/flask-restful/flask-restful/issues/736