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

## 概要

Flaskで実装したRESTfulなAPIをローカルで検証してから、Google App Engine(GAE)にデプロイしたところ、謎の500エラーに遭遇しました。
調べてみたら、```No Content(204)``` ステータスコードでレスポンスする実装がよろしくなかったのが原因でした。

下記は問題がない実装です。

```python
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上でも

```sh
> 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``` を利用して仮想環境を作ってます。

```sh
> mkdir 任意のディレクトリ
> cd 任意のディレクトリ
> python -m venv venv
> . venv/bin/activate
> touch app.py
> touch requirements.txt
```

あとで、GAEにデプロイするので、```requirements.txt``` ファイルを作成してから```pip install``` します。

```txt:requirements.txt
flask
gunicorn
```

```sh
> 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()

```

環境が用意できたので、ローカル上で動作確認します。

```sh
> flask run
```

```sh: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のログをみても問題ありません。

```sh
 * 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``` を利用するので、ローカルでも確認しておきます。

```sh:flask_runしてたコンソール
> gunicorn -b 127.0.0.1:5000 app:app --log-level DEBUG
```

```sh: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
```

こちらも両方とも動作します。

```sh
[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``` がインストール済み

```sh
> touch app.yaml
```

ここではスタンダード環境にデプロイします。
```service``` を指定しないと、```default``` にデプロイされますので、ご注意ください。```how-to-use-no-content-status``` としているのは任意で変更してください。

```yaml: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
```

デプロイします。

```sh
> 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
```

デプロイできたらアクセスしてみます。

```sh
> 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のログをみても詳細はわかりません。。。むむむ。。。

```sh
> 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のフレキシブル環境だとどうなるか設定を変更してみました。

```yaml: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

```

```sh
> gcloud app deploy
```

```sh
> 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``` で。

```sh
> 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)で返すときは下記のようにするのがよさそうです。

```python
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