🎲

RestTemplateで+(plus/プラス)記号を含むクエリを送ろうとするとうまくエンコードされない問題

2021/06/02に公開

やろうとしたこと

チャットアプリにダイスボットを導入したかったので、内部でダイスボットAPI(bcdice-api)の公開サーバを叩き、結果を受け取ろうとしました。

ダイスボットAPIはTRPGのオンラインセッションなどで使うAPIです。
例えば、2d6+5というクエリを送ると、6面ダイスを2つ振った合計値に5を足した数を返してくれます。(他にも色々機能がありますが、今回の記事に関係あるのはこのくらいです)

やったこと

RestTemplateを使って、ダイスボットAPIにGETリクエストを送ります。

DiceRollRepository.java
@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エンコードすればいいじゃん! と思って下記のようなコードを書いてみました。

DiceRollRepository.java
        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+52d6%2B5にエンコードされます。

起こったこと2

ダイスボットAPIにコマンドが認識されなくなってしまいました。(BadRequestが返ってくる)

なぜなのか2

RestTemplateは、リクエスト送信時に自動でエンコードをかけています。
2d6+52d6%2B5にエンコードした文字列をRestTemplateに渡すと、RestTemplate2d6%2B5をさらにエンコードしてしまいます。(即ち、文字列が二重エンコードされた状態になる)

RestTemplateから送られてきたリクエストを受信したダイスボットAPIは文字列をデコードしますが、デコードした結果は2d6%2B5という文字列になります。
この文字列はダイスボットAPIのコマンドとしては不正ですので、BadRequestになってしまいます。

やったこと(うまくいったこと)

これです。
https://stackoverflow.com/questions/54294843/plus-sign-not-encoded-with-resttemplate-using-string-url-but-interpreted

RestTemplateInterceptorを渡しておくことで、RestTemplateがエンコードを行ったあとに自前で定義したintercept()処理を走らせることができます。
今回の場合は、エンコードされたURIに対して、更にプラス記号をエンコードする(実際には、+%2Bに文字列置換する)処理を挿入します。

DiceRollRepository.java
    public DiceRollRepository(RestTemplateBuilder restTemplateBuilder) {
        this.restOperations = restTemplateBuilder
+           .interceptors(new PlusEncoderInterceptor())
	    .build();
    }
PlusEncoderInterceptor.java
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