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