明後日の場所でUnicodeDecodeError

Google App Engine の webapp フレームワークを使ってコードを書く。

↓これは大丈夫だが、

class Page1(webapp.RequestHandler):
    def get(self):
        self.response.headers['Content-Type'] = 'text/plain'
        self.response.out.write('abc')
        self.response.out.write(u'あいう')

↓これはだめ。

class Page2(webapp.RequestHandler):
    def get(self):
        self.response.headers['Content-Type'] = 'text/plain'
        self.response.out.write(u'abc')
        self.response.out.write('あいう')

# GAE/Python 1.4.1

ファイルオブジェクトとはまったく逆の挙動だな。

traceback はこんな感じ。

<type 'exceptions.UnicodeDecodeError'>: 'ascii' codec can't decode byte 0x82 in position 0: ordinal not in range(128)

Traceback (most recent call last):
  File "/base/data/home/apps/<app name>/<version number>/<file name>", line 27, in main
    util.run_wsgi_app(application)
  File "/base/python_runtime/python_lib/versions/1/google/appengine/ext/webapp/util.py", line 97, in run_wsgi_app
    run_bare_wsgi_app(add_wsgi_middleware(application))
  File "/base/python_runtime/python_lib/versions/1/google/appengine/ext/webapp/util.py", line 115, in run_bare_wsgi_app
    result = application(env, _start_response)
  File "/base/python_runtime/python_lib/versions/1/google/appengine/ext/webapp/__init__.py", line 535, in __call__
    response.wsgi_write(start_response)
  File "/base/python_runtime/python_lib/versions/1/google/appengine/ext/webapp/__init__.py", line 248, in wsgi_write
    body = self.out.getvalue()
  File "/base/python_runtime/python_dist/lib/python2.5/StringIO.py", line 270, in getvalue
    self.buf += ''.join(self.buflist)

例外は自分が書いたコード以外の場所で発生しているようだ。

調べてみると StringIO が原因だった。 self.response.out には StringIOインスタンスが入っている。ソースを読むと StringIO は基本的に誰でもウェルカムで、 str でも unicode でも辞書でも数字でも何でも書き込めることがわかる。

from StringIO import StringIO
s = StringIO()
s.write('abc')
s.write(u'def')
s.write({'a' : 1})
s.write(123)
print repr(s.getvalue())  # u"abcdef{'a': 1}123"

basestring でないものが入力された場合は黙って str() にかけてからバッファに積む。 getvalue()read() されたらバッファを連結して返す、というシンプルな動作。

これで問題になるのは、ASCII 以外の文字を含む str と、unicode を書き込んだ時である。 StringIO はこの2つが入力されても特に気にすることなく連結しているので、当然 UnicodeDecodeError が発生する*1。ややこしいことに、実際の連結作業が行われるのは read()getvalue() した時で、 write() した時にはエラーにならない。

from StringIO import StringIO
s = StringIO()
s.write(u'abc')
s.write('あいう')
#
# 何か別の処理 ...
#
body = s.getvalue()  # ここでエラー

これは単純に strunicode を混ぜないことで防止できるわけだが、 Google App Engine が提供する機能の中には、どちらの文字列を返すのかドキュメントにはっきりと書かれていない部分もあるので注意した方がいい。例えばフォームの値を取得しようとすると意外にも unicode が返ってくる。

class FrontDesk(webapp.RequestHandler):
    def post(self):
        self.response.headers['Content-Type'] = 'text/plain'
        self.response.out.write(repr(type(self.request.get('value1'))))  # <type 'unicode'>


ちなみに self.response.out の話に戻るけど、 unicode も最終的にブラウザへ送るときは適当な文字コードで符号化されるはずである。そちらはどうなっているかというと、

if isinstance(body, unicode):
    body = body.encode('utf-8')

というコードが webapp フレームワークに埋め込まれているので大丈夫。フォームとは違って、なぜか任意の文字コードを指定できるようになってないんだな。

*1: UnicodeError が発生するかもしれないと StringIO のドキュメントに注意書きがある。