うさぎ組

ソフトウェア開発、チームによる製品開発、アジャイル、ソフトウェアテスト

HerokuでGrailsを使うとクエリパラメータが文字化けするのでbuildpack直しました。

TL;DR

HerokuではGrailsを簡単に(普通のGrailsプロジェクトをgit pushするだけで)デプロイすることができます。ですが、(少なくともHerokuがデフォルトで使う)Tomcat7ではクエリパラメータが文字化けします。Grailsで使うTomcat7のクエリパラメータをUTF-8エンコードするようにするビルドパックをつくりました。(フォークして一行変えただけだけど)

問題

ローカルでGrailsを使って開発しているときには気づかなかったのですが、Herokuにデプロイするとクエリパラメータに日本語を使うとめっちゃ化けてしまいました。もうTomcatのことなんてすっかりわすれていたので、原因調査にめっちゃ時間かかりました。。。

それに付随していろいろわかったのでまとめておきます。

調査方針

次の方針で調査しました。結果としては最後の他の何かで、それはTomcatの設定だったんですけど、これをHerokuの場合にはどうするか?というのがHeroku初心者ながらにがんばった。

  • Viewの文字コードUTF-8になっていないのではないか
  • サーバーサイドでクエリパラメータをとってくるときにUTF-8になっていないのではないか
  • 他のなにか

Viewの文字コード

Grails2.3.11でプロジェクトを新規作成すると、すでにConfig.groovyにUTF-8指定されています!

grails {
    views {
        gsp {
            encoding = 'UTF-8'
            htmlcodec = 'xml' // use xml escaping instead of HTML4 escaping
            codecs {
                expression = 'html' // escapes values inside ${}
                scriptlet = 'html' // escapes output from scriptlets in GSPs
                taglib = 'none' // escapes output from taglibs
                staticparts = 'none' // escapes output from static template parts
            }
        }
        // escapes all not-encoded output at final stage of outputting
        filteringCodecForContentType {
            //'text/html' = 'html'
        }
    }
}

また、念のためgspのmetaタグにUTF-8を入れてみたけど変わりませんでした。

サーバーサイドでクエリパラメータをとってくるときにUTF-8になっていないのではないか

これもGrails2.3.11でプロジェクトを新規作成すると、既にConfig.groovyにUTF-8指定されています。

 grails.converters.encoding = "UTF-8"

他のなにか

GrailsではクエリパラメータはContoller内でparamsというプロパティに格納されています。そこで次のような取得をすると文字化けのタイミングがわかりました。

  • params → 文字化けしている
  • request.getQueryString() → 文字化けしていない(requestから直接クエリーパラメータ部分の文字列を取得する)

paramsはGrailsParameterMapというクラスなのですが、コンストラクタでHttpServletRequestクラスのgetParameterMapメソッドから値を取得しています。具象クラスはこの場合は実際に利用しているサーバになってくるので、この場合はTomcatです。

ということで、Tomcatのクエリパラメータのエンコードをいじればよさそうです。

ここで、Tomcatの設定を変更する方法なのですが、組み込みTomcatの場合には次の方法がありました。

  • org.grails.plugins.tomcat.ForkedTomcatCustomizerというクラスをつくる。 つくる場所はsrc/main/groovy配下になります。イメージとしてはこんな感じで、void customize(Tomcat tomcat) というメソッドを実装して設定を変更します。
package org.grails.plugins.tomcat

import org.apache.catalina.connector.Connector
import org.apache.catalina.startup.Tomcat

class ForkedTomcatCustomizer{
    void customize(Tomcat tomcat) {
        def c = new Connector(protocol: "HTTP/1.1", port: 8080, URIEncoding: "utf-8", redirectPort: 8443)
        tomcat.service.findConnectors().each {println "${it.protocol} ${it.URIEncoding}"}
        tomcat.service.addConnector(c)
    }

}

前まではEvents.groovyで出来た気がしたのですが、どうもEvents.groovyに以前のようなTomcat用のイベントがこなくなったらしく、上記のような方法になったようです。

でも、私がやりたいのはHerokuのTomcatでした。

最初、この方法で組み込みTomcat以外の設定もオーバーライドされるのかと思ったのですが、そんな感じではなかったようです。で、ローカルにあるTomcatならserver.xmlを変更すればいいのですが、HerokuにあるTomcatを変更するとなると。。。って思い、buildpackを変更することにしました。

HerokuのGrailsビルドパックはJettyもしくはTomcatを利用するようになっていて、Tomcatの場合には別の場所からwebapp-runnerという別のリポジトリで管理されているtomcatをまるっとくるめたjarをダウンロードしてきて使っています。tomcatの設定を変更するにはこのwebapp-runnerをいじればよさそうです。

最新版である7.0.40.1をダウンロードしてきてヘルプを見てみます。

curl http://repo2.maven.org/maven2/com/github/jsimone/webapp-runner/7.0.40.1/webapp-runner-7.0.40.1.jar > webapp-runner.jar 

java -jar webapp-runner.jar  --help

するとなんと--uri-encodigっていうオプションで指定できるよ!とか書いてあるじゃないですか!! やったね!

ということで、Grailsのbuildpackで指定しているRUNNER_OPTSという環境変数に--uri-encoding utf-8って指定をすればいいのですが、buildpack見てみると、ダウンロードしているwebapp-runner.jarが7.0.40.0で古くて、--uri-encodingに対応していない。。。

ということで、これをフォークして7.0.40.1のjarをダウンロードするようにしました。あとはフォークしたbuildpackを使えば問題ない感じです。

forkしたプロジェクトはこちら。(Pull Reqしたほうがいい気はしているが、フォークもとのブランチ戦略がよくわかっていなくっていま出していいのかよくわからん。

まとめ

2014/10/13現在で、Grails + Tomcatで出来るだけ簡単にHerokuにデプロイしてクエリパラメータを文字化けさせないためには次をする。

環境変数を追加する(コマンドで追加する場合)

heroku config:add BUILDPACK_URL=https://github.com/kyonmm/heroku-buildpack-grails
heroku config:add RUNNER_OPTS=--uri-encoding utf-8

Githubのherokuボタンを利用しているなら、app.jsonにも同じように追加しましょう。

{
    "env": {
        "BUILDPACK_URL": "https://github.com/kyonmm/heroku-buildpack-grails",
        "RUNNER_OPTS": "--uri-encoding utf-8",
    }
}

補足

Jettyを使えばこんな問題とはおさらば出来ます!!ですが、Grailsのビルドパックで利用しているjetty-runnerの実装がイケていなくって、起動時にタイムアウトが頻発してしまいます(Herokuではアプリケーション起動に60秒以上かかってはいけない)。これは、環境変数PORTがjetty-runnerなかなか割り当てられないためのようです。これを解決するrunnerを書いている人もいるのですが、いまいち最新に追いついているのかわからなかったので今回は利用を見送りました。

そのうちJettyを使うならこうやるよ!っていうのを試してみたいです。

超補足

Heroku本買いました。わかりやすくて助かります。

Grails in Action

Grails in Action

Programming Grails

Programming Grails