본문 바로가기
Spring

WebClient 인코딩 중복 이슈

by ybs 2023. 3. 13.
반응형

이전글 에서 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.

 

반응형