RestTemplateで+(plus/プラス)記号を含むクエリを送ろうとするとうまくエンコードされない問題
やろうとしたこと
チャットアプリにダイスボットを導入したかったので、内部でダイスボットAPI(bcdice-api)の公開サーバを叩き、結果を受け取ろうとしました。
ダイスボットAPIはTRPGのオンラインセッションなどで使うAPIです。
例えば、2d6+5というクエリを送ると、6面ダイスを2つ振った合計値に5を足した数を返してくれます。(他にも色々機能がありますが、今回の記事に関係あるのはこのくらいです)
やったこと
RestTemplateを使って、ダイスボットAPIにGETリクエストを送ります。
@Repository
public class DiceRollRepository {
// (略)
public DiceRollRepository(RestTemplateBuilder restTemplateBuilder) {
this.restOperations = restTemplateBuilder
.build();
}
public Optional<DiceRollResult> tryDiceRoll(String command) {
DiceRollResult diceRollResult;
try {
diceRollResult = this.restOperations
.getForObject(dicebotUrl + "?command=" + command, DiceRollResult.class);
} catch (HttpClientErrorException exception) {
// (略)
}
起こってほしいこと
commandに設定した値が2d6+5だったら、下記のようなレスポンスが返ってきてほしいです。
{
"ok": true,
"text": "(2D6+5) > 5[2,3]+5 > 10",
(略)
}
ミソになるのはtextフィールドの値です。
ここにcommandで送信した式が反映されるはずで、今回であれば(2D6+5)が入ってくれば意図通りです。
起こったこと
下記のようなレスポンスが返ってきます。
{
"ok": true,
"text": "(2D) > 3[1,2] > 3",
(略)
}
commandに設定した文字列2d6+5のうち、+5部分が(即ち、プラス記号以降が)無視されているのが分かります。
なぜなのか
プラス記号が正常にURLエンコードされず、スペースとして解釈されていることが原因です。
やったこと(だめだったこと)
じゃあcommandをURLエンコードすればいいじゃん! と思って下記のようなコードを書いてみました。
DiceRollResult diceRollResult;
+ String encodedCommand = URLEncoder.encode(command, "UTF-8");
try {
diceRollResult = this.restOperations
- .getForObject(dicebotUrl + "?command=" + command, DiceRollResult.class);
+ .getForObject(dicebotUrl + "?command=" + encodedCommand, DiceRollResult.class);
こうすると、RestTemplateに渡す前に2d6+5は2d6%2B5にエンコードされます。
起こったこと2
ダイスボットAPIにコマンドが認識されなくなってしまいました。(BadRequestが返ってくる)
なぜなのか2
RestTemplateは、リクエスト送信時に自動でエンコードをかけています。
2d6+5→2d6%2B5にエンコードした文字列をRestTemplateに渡すと、RestTemplateは2d6%2B5をさらにエンコードしてしまいます。(即ち、文字列が二重エンコードされた状態になる)
RestTemplateから送られてきたリクエストを受信したダイスボットAPIは文字列をデコードしますが、デコードした結果は2d6%2B5という文字列になります。
この文字列はダイスボットAPIのコマンドとしては不正ですので、BadRequestになってしまいます。
やったこと(うまくいったこと)
これです。
RestTemplateにInterceptorを渡しておくことで、RestTemplateがエンコードを行ったあとに自前で定義したintercept()処理を走らせることができます。
今回の場合は、エンコードされたURIに対して、更にプラス記号をエンコードする(実際には、+を%2Bに文字列置換する)処理を挿入します。
public DiceRollRepository(RestTemplateBuilder restTemplateBuilder) {
this.restOperations = restTemplateBuilder
+ .interceptors(new PlusEncoderInterceptor())
.build();
}
public class PlusEncoderInterceptor implements ClientHttpRequestInterceptor {
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
return execution.execute(new HttpRequestWrapper(request) {
@Override
public URI getURI() {
URI u = super.getURI();
String strictlyEscapedQuery = StringUtils.replace(u.getRawQuery(), "+", "%2B");
return UriComponentsBuilder.fromUri(u)
.replaceQuery(strictlyEscapedQuery)
.build(true).toUri();
}
}, body);
}
}
これによって、RestTemplateによるエンコードから漏れたプラス記号もちゃんとエンコードされます。
起こったこと3
返ってきてほしいレスポンスが返ってきました。
{
"ok": true,
"text": "(2D6+5) > 5[2,3]+5 > 10",
(略)
}
やったね!
Discussion