bottleでresponseを返すとき日本語を含めたい場合
問題の背景
pythonのフレームワークにbottleというのがある。
これは非常に軽量なフレームワークであるのだが、
これを使っているときにちょっと日本語まわりの取り扱いで問題が生じたので解決方法を備忘録としてメモ。
余談だがこれはpython3系にバージョンアップするときに発生した問題。
python2系なら問題なく動いたのだが、
3系にあげるときに読み込むライブラリ等も諸々アップデートしたところ発生した。
そのため正直直接的に何が関係してこれが発生したのかは断言できない、悪しからず。
環境
ubuntu 16.04(諸事情でちょっと古い)
python3.9.9
bottle=0.12.9
gunicorn=20.1.0
発生した事象
set_cookieを用いて日本語文字列をcookieに登録しページを表示しようとしたところ、
from bottle import response
user_name = '管理者'
response.set_cookie('user_name', user_name, max_age=26784000)
以下のようなエラーが発生した。
UnicodeEncodeError: 'latin-1' codec can't encode characters in position 349-351: ordinal not in range(256)
エンコードの問題か!ならencodeすればええ!と安直に思ったけど、
response.set_cookie('user_name', user_account.user_name.encode('utf-8'), max_age=26784000)
結果は
TypeError: Secret key missing for non-string Cookie.
stringじゃないもの(byte形式とか)はcookieに登録できませんと。
対応手法
厳密には解決したわけではないので対応としているが、
以下のようにURLエンコードをかけてパースすることで対応した。
response.set_cookie('user_name', urllib.parse.quote(user_account.user_name), max_age=26784000)
URLエンコードなら比較的メジャーであるので対応できる。
いちいちデコードしなければならない手間はあるが、致し方ない。
ついでなのでもう1パターン
こちらはファイルをダウンロードする時、ファイル名に日本語を含めたいとき。
例えばKMLの情報がDBに保存されており、それを取得してファイルとして返却する、
こんなソースコードがあったとする。(一部簡略化)
with self.session() as s:
mime_type = 'application/vnd.google-earth.kml+xml'
# DBに保存してあるKMLファイルの情報を取得
geo_layer = s.query(GeoLayer).filter_by(id=kml_id).first()
# httpresponseインスタンスを生成(予めimport済)
resp = HTTPResponse(status=200, body=geo_layer.content)
resp.set_header('Content-Type', mime_type)
resp.set_header('Content-Length', str(geo_layer.file_size))
fname = geo_layer.name + '.kml' # このnameに日本語が入っている
resp.set_header('Content-Disposition', 'attachment; filename="%s"' % fname)
return resp
このままダウンロードしようとすると、
UnicodeEncodeError: 'latin-1' codec can't encode characters in position 209-211: ordinal not in range(256)
さっきも見たよあんた!
ってなる。
そこで後ろから2行目、fnameをパースしてやるとうまくいく。
これより前で予め変換してあげても問題ない。
resp.set_header('Content-Disposition', 'attachment; filename="%s"' % urllib.parse.quote(fname))
ダウンロード時はしっかり日本語名に変換され、無事ダウンロードに対応完了。
ちなみに
このエラーのTracebackを見てみるとgunicorn由来であることがわかる。
Traceback (most recent call last):
File "/usr/local/rakutoban-web-viewer/.venv/lib/python3.9/site-packages/gunicorn/workers/sync.py", line 136, in handle
self.handle_request(listener, req, client, addr)
File "/usr/local/rakutoban-web-viewer/.venv/lib/python3.9/site-packages/gunicorn/workers/sync.py", line 185, in handle_request
resp.write(item)
File "/usr/local/rakutoban-web-viewer/.venv/lib/python3.9/site-packages/gunicorn/http/wsgi.py", line 326, in write
self.send_headers()
File "/usr/local/rakutoban-web-viewer/.venv/lib/python3.9/site-packages/gunicorn/http/wsgi.py", line 322, in send_headers
util.write(self.sock, util.to_bytestring(header_str, "latin-1"))
File "/usr/local/rakutoban-web-viewer/.venv/lib/python3.9/site-packages/gunicorn/util.py", line 565, in to_bytestring
return value.encode(encoding)
UnicodeEncodeError: 'latin-1' codec can't encode characters in position 209-211: ordinal not in range(256
pythonを2系から3系にバージョンアップする作業をした時にgunicornのバージョンも上げた。
様々な要因が重なって、めちゃめちゃ調べても全く引っかからないようなエラーに出くわしたのかもしれない。
同じような境遇に出会う人は稀だろうけど、一応対応策として残しておく。