Python3.6を境にjson.loads()の動きが変わっていたことで1週間ハマりました ToT

## TL;DR
Python3.5以前は`json.loads()`にbytes型を渡すとエラーとなる。
Python3.6以降は`json.loads()`にbytes型を渡してもエラーとならない。[^1]

>バージョン 3.6 で変更: s には bytes 型と bytearray 型も使えるようになりました。 入力エンコーディングは UTF-8, UTF-16, UTF-32 のいずれかでなければなりません。

https://docs.python.jp/3/library/json.html#json.loads

追記: Flaskの`request.get_json()`を使って回避することができる。このメソッドはPython3.4系と3.6系とで動きに違いはなかった。

## はじめに
AzureのWebAppsにPythonのアプリケーション(Flask製)をデプロイしました。
GETとPOSTのエンドポイントを用意したのですが、
WebAppsではPOSTのエンドポイントだけ 500 Internal Server エラーになるという事象が発生しました。

|環境|GET|POST|
|:-:|:-:|:-:|
|ローカル|200 OK|200 OK|
|WebApps|200 OK|500 内部サーバエラー|

原因は、Python3.6系のローカルとPython3.4系のWebAppsとで
POSTのエンドポイントに仕込んだ`json.loads()`の動きが違ったことでした。

WebAppsはあまり詳しくないので、解決までの期間を短縮できそうな方法がありましたら、ご指摘いただけるとうれしいです。

## 動作環境
- ローカル開発環境
  - Python 3.6.5
  - Flask==0.12.2
- WebApps
  - Python 3.4.1
  - Flask==0.12.2

## ハマってから解決するまで
#### ハマるまで (WebAppsにデプロイ)
MicrosoftのFlaskアプリデプロイチュートリアルに沿って進めました。
[Azure に Python Web アプリを作成する](https://docs.microsoft.com/ja-jp/azure/app-service/app-service-web-get-started-python)

- チュートリアルでは Azure Cloud Shell を使っていますが、ポータルのGUI操作で進めています。
- web.2.7.config と web.3.4.config が共存しているとPython2.7系でビルドされるようです。web.2.7.configは削除しました。[^2]
- Hello Worldはできたので、POSTのエンドポイントをmain.pyに追加しました。POSTされたJSONをログに出力しようとしています。

```python:main.py
import json

from flask import Flask
from flask import jsonify
from flask import request


app = Flask(__name__)

@app.route('/')
def hello_world():
    return 'Hello, World!'

@app.route('/response', methods=['POST'])
def print_data():
    parsed = json.loads(request.data)
    # TODO: parsedをログに出力
    response = jsonify({'foo': 'bar'})
    response.status_code = 200
    return response


if __name__ == '__main__':
    app.run()
```

そして、JSONデータをPOSTしたときだけ500エラーという事態が発生したのです。。

![WebApps 500 Internal Server Error.png](https://qiita-image-store.s3.amazonaws.com/0/82132/941f2d54-8afb-5b82-901a-d488b44f12c7.png)

```html
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<title>500 Internal Server Error</title>
<h1>Internal Server Error</h1>
<p>The server encountered an internal error and was unable to complete your request.  Either the server is overloaded or there is an error in the application.</p>
```

POSTにはAdvanced REST Clientを使用。ヘッダは`Content-Type: application/json`のみ。

```json
{
  "property": {
    "otherProperty": {
      "value": 123456
    }
  }
}
```

#### ハマってから
- [Troubleshoot- logging python Application errors on Azure Web/API Apps](https://blogs.msdn.microsoft.com/azureossds/2015/07/14/troubleshoot-logging-python-application-errors-on-azure-webapi-apps/)をもとにログファイルのパスを環境変数に追加するも解決せず。。
- [Azure App Service の Web アプリの診断ログの有効化](https://docs.microsoft.com/ja-jp/azure/app-service/web-sites-enable-diagnostic-log)を行うも、500 Internal Server エラー以上の情報を理解できず。。(IISの内部の動きはちんぷんかんぷんでした。スタックトレースが見たかったんですが結局見られませんでした)
- ローカルで動くからPythonコードの問題ではなくweb.configの問題と考え[^3]、web.configの設定を調べてみるも同様の事象は見つからず。。
- VMでFlaskアプリを動かす方法[^4][^5]をやってみるも、WSGIの設定でつまづいているようでうまくいかず。。

#### 1週間かけての脱出
`json.loads()`の動きが違うという天啓を得たので、以下のように書き換えて解決。

```python
parsed = json.loads(request.data.decode())
```

今回は`request.data`がbytes型になっていたんですね。[^6]

## 終わりに
開発環境とデプロイ先の環境のバージョンを揃えるのが重要ということを身をもって知りました。
WebApps側をPython3.6系で調べる方法や、開発環境をPython3.4系で揃える方法を今後調べて記事にします。
WebAppsでのスタックトレースを確認する方法など、解決までの期間を短縮できそうな方法をご指摘いただけるとうれしいです。

## 代案: `json.loads()` を使わない
`json.loads()`の代わりに Flask に用意された`request.get_json()`[^7]を使う方法が見つかりました。
`request.get_json()`を使えば、Python のマイナーバージョンの違いは気にしなくてよさそうです。

```python
@app.route('/response', methods=['POST'])
def print_data():
    parsed = request.get_json()
    # TODO: parsedをログに出力
    response = jsonify({'foo': 'bar'})
    response.status_code = 200
    return response
```

## 脚注
[^1]: アウトプットのネタで調べたことがありました。[Python入門者でもComputer Vision APIを叩くのは怖くない](https://qiita.com/ftnext/items/970069ff509f69812f23#%E4%BD%99%E8%AB%87-2-python363%E3%81%A7%E3%81%AFjsonloads%E3%81%8Cbytes%E5%9E%8B%E3%81%AB%E3%82%82%E5%AF%BE%E5%BF%9C%E3%81%97%E3%81%A6%E3%81%84%E3%81%9F)
[^2]: runtime.txtを用意しても解決できるようです。参考: [Azure App Service Web Apps による Python の構成](https://docs.microsoft.com/ja-jp/azure/app-service/web-sites-python-configure#python-version)
[^3]: [StackOverflowの回答](https://stackoverflow.com/a/37651478)より。結果的には「code logic error」でした。。
[^4]: CentOSでFlaskを動かそうとしてみました。[イベントレポート | 第27回 Pythonもくもく会 #mokupy （Azure VMにFlaskアプリをデプロイ）](http://nikkie-ftnext.hatenablog.com/entry/2018/04/30/232812)
[^5]: Ubuntuでもやってみました。参考:[Install Apache Web Server on Azure Virtual Machine](http://leifengblog.net/blog/how-to-install-apache-http-web-server-on-azure-ubuntu-virtual-machines/)
[^6]: 「Contains the incoming request data as string in case it came with a mimetype Flask does not handle.」http://flask.pocoo.org/docs/0.12/api/#flask.Request.data （in case周りの英語の意味がとれなかったので後日手を動かして確認します）
[^7]: `get_json()`の説明は以下のドキュメントにあります: http://flask.pocoo.org/docs/0.12/api/#flask.Request.get_json