dev_appserver.pyが"TypeError: environment can only contain strings"で動かないのをなんとかする

久々にGoogle AppEngine/PHPのプログラムを修正することになって、ローカルテストしようとdev_appserver.pyを起動したのですが、

ERROR:root:Failure to start PHP with: ['C:\\Users\\FOO\\scoop\\apps\\gcloud\\current\\platform\\google_appengine\\php\\php-5.5-Win32-VC11-x86\\php-cgi.exe', '-d', 'include_path=".;C:\\Users\\FOO\\source\\repos\\appengine-php55;C:\\Users\\FOO\\scoop\\apps\\gcloud\\current\\platform\\google_appengine\\php\\sdk"', '-c', 'C:\\Users\\FOO\\source\\repos\\appengine-php55', '-d', 'zend_extension="C:\\Users\\FOO\\scoop\\apps\\gcloud\\current\\platform\\google_appengine\\php\\php-5.5-Win32-VC11-x86\\php_xdebug.dll"', '-d', 'extension="php_gae_runtime_module.dll"', '-d', 'extension_dir="C:\\Users\\FOO\\scoop\\apps\\gcloud\\current\\platform\\google_appengine\\php\\php-5.5-Win32-VC11-x86"']
Traceback (most recent call last):
  File "C:\Users\FOO\scoop\apps\gcloud\current\platform\google_appengine\google\appengine\tools\devappserver2\php\runtime\runtime.py", line 269, in __call__
    stdout=subprocess.PIPE)
INFO     2022-04-09 20:34:10,601 module.py:890] default: "GET /_ah/warmup HTTP/1.1" 500 734
  File "C:\Users\FOO\scoop\apps\gcloud\current\platform\google_appengine\google\appengine\tools\devappserver2\safe_subprocess.py", line 83, in start_process
    shell=shell)
  File "C:\Users\FOO\scoop\apps\gcloud\current\platform\bundledpython2\lib\subprocess.py", line 390, in __init__
    errread, errwrite)
    startupinfo)
TypeError: environment can only contain strings

で起動せず。gcloud componentは最新化したし、「パスに日本語(非ASCII文字)が含まれてませんか?」もNo。 仕方がないのでエラーが出ている runtime.py を解析するハメに。

  def __call__(self, environ, start_response):
    """Handles an HTTP request for the runtime using a PHP executable.

    Args:
      environ: An environ dict for the request as defined in PEP-333.
      start_response: A function with semantics defined in PEP-333.

    Returns:
      An iterable over strings containing the body of the HTTP response.
    """
    user_environ = self.make_php_cgi_environ(environ)

    if 'CONTENT_LENGTH' in environ:
      content = environ['wsgi.input'].read(int(environ['CONTENT_LENGTH']))
    else:
      content = ''

    args = [six.ensure_str(arg) for arg in self.make_php_cgi_args()]

    # Handles interactive request.
    request_type = environ.pop(http_runtime_constants.REQUEST_TYPE_HEADER, None)
    if request_type == 'interactive':
      args.extend(['-d', 'html_errors="0"'])
      user_environ[http_runtime_constants.REQUEST_TYPE_HEADER] = request_type

    try:
      # stderr is not captured here so that it propagates to the parent process
      # and gets printed out to consle.
      p = safe_subprocess.start_process(
          args,
          input_string=content,
          env=user_environ,
          cwd=six.ensure_text(self.config.application_root),
          stdout=subprocess.PIPE)    # ← ここで例外発生
      stdout, _ = p.communicate()
    except Exception as e:
      logging.exception('Failure to start PHP with: %s', args)
      start_response('500 Internal Server Error',
                     [(http_runtime_constants.ERROR_CODE_HEADER, '1')])
      return ['Failure to start the PHP subprocess with %r:\n%s' % (args, e)]

というコードだったので、まずはuser_environ変数の中身をprint表示してみると

{u'TMP': u'C:\\Users\\FOO\\AppData\\Local\\Temp', u'REQUEST_ID_HASH': u'7CBB4402', u'REDIRECT_STATUS': u'1', u'SERVER_SOFTWARE': u'Development/2.0', u'REQUEST_METHOD': u'GET', u'PATH_INFO': u'/_ah/warmup', u'SERVER_PROTOCOL': u'HTTP/1.1', u'QUERY_STRING': u'', u'SYSTEMROOT': u'C:\\WINDOWS', u'DEFAULT_VERSION_HOSTNAME': u'localhost:8080', u'USER_IS_ADMIN': u'0', u'CONTENT_LENGTH': u'0', u'APPENGINE_RUNTIME': u'php', u'TZ': u'UTC', u'APPLICATION_ROOT': u'C:\\Users\\FOO\\source\\repos\\appengine-php55', u'SERVER_NAME': u'localhost', u'REMOTE_ADDR': u'0.1.0.3', u'INSTANCE_ID': u'a3f67df098d059011b5dafbdc3d41e4aacc2', u'REQUEST_LOG_ID': u'05e41c3d8cc239c03f0c8d4ccdeb797', u'AUTH_DOMAIN': u'gmail.com', u'SERVER_PORT': u'8080', u'CURRENT_MODULE_ID': u'default', u'CURRENT_VERSION_ID': u'None.918090198333333552', u'SCRIPT_FILENAME': u'C:\\Users\\FOO\\scoop\\apps\\gcloud\\current\\platform\\google_appengine\\google\\appengine\\tools\\devappserver2\\php\\runtime\\setup.php', u'USER_ORGANIZATION': u'', u'HTTP_HOST': u'localhost:8080', u'HTTPS': u'off', u'USER_ID': u'', u'REAL_SCRIPT_FILENAME': u'C:\\Users\\FOO\\source\\repos\\appengine-php55\\403.php', u'USER_EMAIL': u'', u'REQUEST_URI': u'/_ah/warmup', u'STDERR_LOG_LEVEL': u'1', u'DATACENTER': u'us1', u'APPLICATION_ID': u'dev~None', u'TEMP': u'C:\\Users\\FOO\\AppData\\Local\\Temp', u'HTTP_X_APPENGINE_COUNTRY': u'ZZ', u'USER_NICKNAME': u'', u'REMOTE_API_PORT': u'52479', u'HTTP_CONTENT_LENGTH': u'0', u'REMOTE_API_HOST': u'localhost', u'REMOTE_REQUEST_ID': u'eRGVBeUVhC'}

という値。「やっぱりどこにも日本語なんか含まれてないじゃん」ということで行き詰りかけたのですが、よくよく見ると

「なんで全部の文字列の前に"u"が付いてるの?」

というわけで、こんなパッチを当ててみました。

--- gcloud/current/platform/google_appengine/google/appengine/tools/devappserver2/php/runtime/runtime.py.orig    Tue Jan  1 17:00:00 1980
+++ gcloud/current/platform/google_appengine/google/appengine/tools/devappserver2/php/runtime/runtime.py  Sat Apr  9 21:35:32 2022
@@ -259,6 +259,10 @@
       user_environ[http_runtime_constants.REQUEST_TYPE_HEADER] = request_type
 
     try:
+      # XXX: fix to convert all env keys and values to str.
+      # https://stackoverflow.com/a/38476472
+      user_environ = { str(key):str(value) for key,value in user_environ.items() }
+
       # stderr is not captured here so that it propagates to the parent process
       # and gets printed out to consle.
       p = safe_subprocess.start_process(

これを当てて再度user_environ変数を確認すると

{'TMP': 'C:\\Users\\FOO\\AppData\\Local\\Temp', 'REQUEST_ID_HASH': '7CBB4402', 'REDIRECT_STATUS': '1', 'SERVER_SOFTWARE': 'Development/2.0', 'REQUEST_METHOD': 'GET', 'PATH_INFO': '/_ah/warmup', 'SERVER_PROTOCOL': 'HTTP/1.1', 'QUERY_STRING': '', 'SYSTEMROOT': 'C:\\WINDOWS', 'DEFAULT_VERSION_HOSTNAME': 'localhost:8080', 'USER_IS_ADMIN': '0', 'CONTENT_LENGTH': '0', 'APPENGINE_RUNTIME': 'php', 'TZ': 'UTC', 'APPLICATION_ROOT': 'C:\\Users\\FOO\\source\\repos\\appengine-php55', 'SERVER_NAME': 'localhost', 'REMOTE_ADDR': '0.1.0.3', 'INSTANCE_ID': 'a3f67df098d059011b5dafbdc3d41e4aacc2', 'REQUEST_LOG_ID': '05e41c3d8cc239c03f0c8d4ccdeb797', 'AUTH_DOMAIN': 'gmail.com', 'SERVER_PORT': '8080', 'CURRENT_MODULE_ID': 'default', 'CURRENT_VERSION_ID': 'None.918090198333333552', 'SCRIPT_FILENAME': 'C:\\Users\\FOO\\scoop\\apps\\gcloud\\current\\platform\\google_appengine\\google\\appengine\\tools\\devappserver2\\php\\runtime\\setup.php', 'USER_ORGANIZATION': '', 'HTTP_HOST': 'localhost:8080', 'HTTPS': 'off', 'USER_ID': '', 'REAL_SCRIPT_FILENAME': 'C:\\Users\\FOO\\source\\repos\\appengine-php55\\403.php', 'USER_EMAIL': '', 'REQUEST_URI': '/_ah/warmup', 'STDERR_LOG_LEVEL': '1', 'DATACENTER': 'us1', 'APPLICATION_ID': 'dev~None', 'TEMP': 'C:\\Users\\FOO\\AppData\\Local\\Temp', 'HTTP_X_APPENGINE_COUNTRY': 'ZZ', 'USER_NICKNAME': '', 'REMOTE_API_PORT': '52479', 'HTTP_CONTENT_LENGTH': '0', 'REMOTE_API_HOST': 'localhost', 'REMOTE_REQUEST_ID': 'eRGVBeUVhC'}

と無事に"u"が外れ、dev_appserver.pyも正常起動するようになりました。めでたし、めでたし。

本来のPHPアプリの修正より、こっちのほうがはるかに時間がかかった...