UriComponentsBuilder Encoding
작성한 코드에서는 외부 API를 GET 방식으로 호출할 때 아래와 같은 코드로 uri를 String 타입으로 RestTemplate.exchange() 메소드를 이용해 호출하는 방식으로 사용하고 있었습니다.
기존 코드
String uriBuilder = UriComponentsBuilder.fromHttpUrl(apiURL)
.queryParam("param1", param1)
.queryParam("DateTime", DateTime)
.toUriString();
HttpEntity<Map<String, Object>> requestEntity = new HttpEntity<>(headers);
try {
responseEntity = TestRestTemplate.exchange(URLDecoder.decode(uriBuilder, "UTF-8"), HttpMethod.GET, requestEntity, Map.class);
} catch (Exception e) {
e.printStackTrace();
}
로그를 통해 확인해보니 두가지 문제가 있었는데요.
-
DateTime 파라미터에 포함된 + 기호를 uriBuilder는 공백으로 인식하여 %2B(+)가 아닌 %20(공백)으로 인코딩
-
apiURL에 +를 %2B로 치환하여 넣을 경우 %2B를 한번 더 인코딩
spring 공식 문서 를 확인해보니, UriComponentsBuilder는 공백과 +의 경우 인코딩을 제대로 처리하지 않았습니다.
public UriComponentsBuilder queryParam(String name,
@Nullable
Object... values)
Description copied from interface: UriBuilder
Append the given query parameter. Both the parameter name and values may contain URI template variables to be expanded later from values. If no values are given, the resulting URI will contain the query parameter name only, e.g. "?foo" instead of "?foo=bar".
Note: encoding, if applied, will only encode characters that are illegal in a query parameter name or value such as "=" or "&". All others that are legal as per syntax rules in RFC 3986 are not encoded.
This includes "+" which sometimes needs to be encoded to avoid its interpretation as an encoded space.
Stricter encoding may be applied by using a URI template variable along with stricter encoding on variable values.
For more details please read the "URI Encoding" section of the Spring Framework reference.
Specified by:
queryParam in interface UriBuilder
Parameters:
name - the query parameter name
values - the query parameter values
See Also:
UriBuilder.queryParam(String, Collection)
Spring 공식 문서에 따르면
-
UriComponentsBuilder의 queryParam 메서드는 URI 템플릿 변수로 확장될 수 있는 파라미터 이름과 값을 추가합니다.
-
인코딩은 불법적인 문자(예: = 또는 &)만 처리하며, + 같은 합법적인 문자는 인코딩되지 않습니다.
-
엄격한 인코딩이 필요한 경우, URI 템플릿 변수와 변수 값에 대해 엄격한 인코딩을 적용해야 합니다.
-
이로 인해 RestTemplate와 WebClient는 URI 타입으로 요청을 받으면 인코딩을 진행하지 않지만, String 타입으로 받으면 한 번 더 인코딩하여 호출하게 됩니다.
-
두 번 인코딩을 진행하면 %2B의 % 기호가 한 번 더 인코딩되어 잘못된 요청을 하게 됩니다.
따라서, +기호를 직접 인코딩하고, URI 타입으로 호출하는 방식으로 문제를 해결할 수 있었습니다.
수정한 코드
UriComponents uriBuilder = UriComponentsBuilder.fromHttpUrl(apiURL)
.queryParam("param1", param1)
.queryParam("DateTime", DateTime)
.build(true);
HttpEntity<Map<String, Object>> requestEntity = new HttpEntity<>(headers);
responseEntity = TestRestTemplate.exchange(uriBuilder.toUri(), HttpMethod.GET, requestEntity, Map.class);
+기호 치환 메소드
private String encodePlus(String containPlusSymbol) {
return containPlusSymbol.replace("+", "%2B");
}
요약
- UriComponentsBuilder는 공백과 + 기호를 인코딩하지 않아 문제가 발생했습니다.
- String 타입으로 URI를 전달하면 인코딩이 두 번 적용되어 문제를 일으킬 수 있습니다.
- URI 타입으로 변환하여 RestTemplate를 사용하면 이중 인코딩 문제를 해결할 수 있습니다.
참고
https://jackjeong.tistory.com/entry/Issue-UriComponentsBuilder-Encoding-특수문자-예약어-이중인코딩-방지