Here is the problem my colleague Sergey faced yesterday (http://stackoverflow.com/questions/41744542/spring-cloud-feign-client-requestparam-with-list-parameter-creates-a-wrong-requ)
We have a Spring Clound Feign Client mapping defined as following
@RequestMapping(method = RequestMethod.GET, value = "/search/findByIdIn")
Resources<MyClass> get(@RequestParam("ids") List<Long> ids);
by calling
feignClient.get(Arrays.asList(1L,2L,3L))
according to what I can see in the debugger, the feign-core library forms the following request:
/search/findByIdIn?ids=1&ids=2&ids=3
instead of expected
/search/findByIdIn?ids=1,2,3
which would be correct for the server Spring Data REST endpoint declared in the same way as my Feign client method.
Spring Data REST Service does not accept parameters like ids=1&ids=2 and throws an Exception. But the multiple sort parameters will be accepted.
We've tried both versions Camden.RELEASE and Dalston.BUILD-SNAPSHOT without success.
Here you can find both client and server to reproduce the problem:
https://github.com/abinet/demo
Comment From: ryanjbaxter
It looks like the logic in RequestTemplate.queryLine is what is creating the query string. Maybe @adriancole or @spencergibb know why we decided to create the string this way.
https://github.com/OpenFeign/feign/blob/master/core/src/main/java/feign/RequestTemplate.java#L653
Comment From: spencergibb
It's not built for Spring Data, it's built for general HTTP requests. A list of parameters to a get is naturally split into repeated query parameters, which are allowed.
Comment From: codefromthecrypt
what @spencergibb said :P repeated query parameters are permitted, and while there's no standard, many libraries repeat rather than assume the other side uses comma encoding.
In feign upstream there's Param.Encoder annotation which would allow you to customize a list and decide to join on string. This is set in contract parsing time in the MethodMetadata
Comment From: ryanjbaxter
Thanks @spencergibb and @adriancole!
@adriancole when you say Param.Encoder do you mean this Param class?
Comment From: codefromthecrypt
no feign.Param.Expander which ends up in MethodMetadata.indexToExpanderClass
Comment From: abinet
@adriancole Param.Expander does not help here. It will be applied to each the element of the collection but not to collection itself.
Comment From: codefromthecrypt
well, at the moment, the only way I can think of is to use a request interceptor to rewrite the queries
ex.
for each entry with more than one value in template.queries() template.query(entry.key, joinOnComma(entry.value))
Comment From: ryanjbaxter
@adriancole after I shut down my computer last night I had the same idea. I will try the request interceptor approach later on.
Comment From: abinet
thank you guys. the solution with a request interceptor works like a charm.
Comment From: postalservice14
Here is my request interceptor that worked. In case someone else is trying to solve this:
public class ListToStringCommaRequestInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
template.queries().forEach((key, value) -> {
if (value.size() > 1) {
template.query(key, String.join(",", value));
}
});
}
}
Comment From: BohdanKorinnyi
@postalservice14 this request interceptor produces duplicates of data in 10.7.4 version. I added some logs and it appeared that values are not replaced for given key but they just appended to query.
public void apply(RequestTemplate requestTemplate) {
log.info("Before RequestInterceptor query line: {}", requestTemplate.path() + requestTemplate.queryLine());
requestTemplate.queries().forEach((key, value) -> {
if (value.size() > 1) {
requestTemplate.query(key, String.join(",", value));
}
});
log.info("After RequestInterceptor query line: {}", requestTemplate.path() + requestTemplate.queryLine());
}
As a result the query line looks like:
Before RequestInterceptor query line: /path?params=param1¶ms=param2
After RequestInterceptor query line: /path?params=param1¶ms=param2¶ms=param1,param2
Comment From: Alexalexale
var queries = template.queries().entrySet().stream().collect(Collectors.toMap(
Map.Entry::getKey,
entry -> {
if (entry.getValue().size() > 1) {
return List.of(String.join(",", entry.getValue()));
}
return entry.getValue();
}
));
template.queries(null);
template.queries(queries);