이전글 에서 WebClient 인코딩 중복에 대해서 한번 다룬적 있다. 간단히 요약하면, uriBuilder 를 이용해서 만들면 이미 인코딩된 query string 값을 또 인코딩 해서 이중 인코딩 한 결과가 나온다. 그래서 미리 인코딩 할 필요가 없다.
// 아래처럼 만들면, 최종 request 는 이중 인코딩된 결과로 보내진다.
this.webClient
.get()
.uri(uriBuilder -> uriBuilder
.path("/api")
//.query("name=양봉수")
.query("name=%EC%96%91%EB%B4%89%EC%88%98")
.build()
)
.retrieve()
그런데 query string 이 json 일때는 미리 인코딩 해야만 한다. 안하면 에러가 발생한다.
cf) 왜 query string 에 json 을 담는지 이해가 안갈 수 있겠지만, 기존 시스템을 다시 만드는 상황이라 반드시 하위호환을 맞춰줘야 했다.
처음엔 인코딩을 안해도 된다고 생각했다. 왜냐하면 uriBuilder 내부적으로 인코딩을 한다는걸 알았기 때문이다.
하지만 아래 this.uriComponentsBuilder.build().expand(uriVars) 의 expand 로직에서 에러가 발생한다.
public class DefaultUriBuilderFactory implements UriBuilderFactory {
@Override
public URI build(Object... uriVars) {
...
UriComponents uric = this.uriComponentsBuilder.build().expand(uriVars);
...
}
에러 원인을 찾아보니 URI template 에 따른 처리 때문이었다. json 에 있는 중괄호를 template 으로 인식해 파싱을 하고, key 인 name 에 해당하는 value 를 찾았지만 값이 없어서 에러가 난것이다. 다시말해 기대했던 template 은 /test?key={name} 이런 형태인데 json format 은 그에 맞지 않으니 value 가 없다고 판단한것이다.
{
"name": "ybs"
}
에러 메세지는 아래와 같다.
msg: Not enough variable values available to expand '"name"'
java.lang.IllegalArgumentException: Not enough variable values available to expand '"name"'
at org.springframework.web.util.UriComponents$VarArgsTemplateVariables.getValue(UriComponents.java:370)
at org.springframework.web.util.HierarchicalUriComponents$QueryUriTemplateVariables.getValue(HierarchicalUriComponents.java:1093)
at org.springframework.web.util.UriComponents.expandUriComponent(UriComponents.java:263)
at org.springframework.web.util.HierarchicalUriComponents.lambda$expandQueryParams$5(HierarchicalUriComponents.java:456)
at java.base/java.util.Map.forEach(Map.java:713)
at org.springframework.web.util.HierarchicalUriComponents.expandQueryParams(HierarchicalUriComponents.java:452)
at org.springframework.web.util.HierarchicalUriComponents.expandInternal(HierarchicalUriComponents.java:441)
at org.springframework.web.util.HierarchicalUriComponents.expandInternal(HierarchicalUriComponents.java:53)
at org.springframework.web.util.UriComponents.expand(UriComponents.java:172)
at org.springframework.web.util.DefaultUriBuilderFactory$DefaultUriBuilder.build(DefaultUriBuilderFactory.java:403)
at org.springframework.web.util.DefaultUriBuilderFactory.expand(DefaultUriBuilderFactory.java:154)
at org.springframework.web.reactive.function.client.DefaultWebClient$DefaultRequestBodyUriSpec.uri(DefaultWebClient.java:194)
그래서 이 문제를 해결하기 위해 이중 인코딩 해서(json data 를 미리 한번 인코딩 수행) webClient 요청을 보내게 됐고, 해당 요청을 받는 서버는 두번 디코딩을 해야했다.
cf) path 와 query string 을 합친 string 을 uri 에 넣는 방법과 uriBuilder 를 이용하는 방법 모두 결과는 동일하게 에러가 발생한다.
// path 값은 /test?jsonParam=%7B%0A%20%20%22name%22%3A%20%22ybs%22%0A%7D
// jsonParam 은 아래 json 을 한번 URL encode 한 값이다.
// {
// "name": "ybs"
// }
this.webClient
.post()
.uri(path)
// path 값은 /test
// query 값은 %7B%0A%20%20%22name%22%3A%20%22ybs%22%0A%7D
this.webClient
.post()
.uri(uriBuilder -> {
return uriBuilder
.path(path)
.query(query)
.build();
다른 해결방법을 찾아보니, 아래와 같이 만들면 문제의 expand 로직을 피할 수 있어 에러는 발생하지 않는다.
그런데 이 방법을 쓰면 인코딩된 json 결과가 살짝 달라진다.
// 방법1
this.webClient
.post()
.uri(uriBuilder -> {
URI uri = UriComponentsBuilder.fromUriString(path)
.queryParam("query", query)
.build()
.toUri();
return uri;
}
)
// 방법2. uriBuilder 람다 대신에 직접 URI 를 넣어도 된다.
URI uri = UriComponentsBuilder.fromUriString(path)
.queryParam("query", query)
.build()
.toUri();
return this.webClient
.post()
.uri(uri)
만들어진 URI 객체의 query string 값을 보면 콜론(:) 이 인코딩이 안되어 있다. 다시말해 콜론(:) 을 URL encode 하면 '%3A' 값이 되는데 URI 내부적으로 인코딩을 시키면 적용 안된 결과가 나온다.
1. %7B%0A%20%20%22name%22:%20%22ybs%22%0A%7D (URI 내부적으로 인코딩된 결과)
2. %7B%0A%20%20%22name%22%3A%20%22ybs%22%0A%7D (미리 인코딩한 결과)
RFC 3986 Uniform Resource Identifier (URI) 스펙에서 허용하는 query 문자를 보면 콜론(:) 이 있다. 다시말해 콜론(:) 은 query 문자로써 허용된다.
query = *( pchar / "/" / "?" )
—————
pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
pct-encoded = "%" HEXDIG HEXDIG
sub-delims = "!" / "$" / "&" / "'" / "(" / ")"
/ "*" / "+" / "," / ";" / "="
물론 URL 인코딩(RFC 스펙에서는 percent-encode 이라 표현)을 안해도 되냐는 다른 문제로 볼 수 있다.
아래 정의된 reserved 문자는 URI syntax 로써, URI 내의 다른 데이터들과 구별할 수 있는 문자 집합이다.
reserved = gen-delims / sub-delims
—————
gen-delims = ":" / "/" / "?" / "#" / "[" / "]" / "@"
sub-delims = "!" / "$" / "&" / "'" / "(" / ")"
/ "*" / "+" / "," / ";" / "="
RFC 3986 스펙 문서에서는 reserved 문자는 인코딩 해야한다고(should) 했지만, query 에 해당하는 component 에서 특별하게 허용한다면 인코딩 안해도 된다고도 써져있다.
결국 component 를 어떻게 해석하냐에 따라 의미가 달라질 수 있긴 하지만, 나는 콜론(:) 을 인코딩 안해도 문제가 없는것으로 받아들여진다.
https://www.rfc-editor.org/rfc/rfc3986#section-2.2
URI producing applications should percent-encode data octets that
correspond to characters in the reserved set unless these characters
are specifically allowed by the URI scheme to represent data in that
component. If a reserved character is found in a URI component and
no delimiting role is known for that character, then it must be
interpreted as representing the data octet corresponding to that
character's encoding in US-ASCII.
'Spring' 카테고리의 다른 글
WebClient 에서 raw response body 로깅 (0) | 2023.07.15 |
---|---|
EDA 에서 NullPointerException 조심하기 (0) | 2023.04.15 |
WebClient + Retry + CircuitBreaker (1) | 2022.09.24 |
WebClient 사용할때 주의 (8편) (0) | 2022.09.18 |
Kafka 이벤트 발행과 DB 저장(redis) 트랜잭션 (0) | 2022.03.09 |