Do you know Feign? It is a Java library that allows you to write a REST client with minimal code. In my current work project, we are currently using it and as we had no experience with it, we are continually improving our knowledge on this library.
We recently encountered a particular use case, that although we could not find an example on the Internet, we were able to find a working solution that you might be interested in. 🥰
Context
We are currently using an external REST API which seems a bit special: for their GET endpoints, we are able to filter the returned ressources with their API, but we cannot pass these filters as normal GET parameters. We have to pass them directly in the URL with a special syntax!
Let’s see an example. If we want to filter the /activities
endpoint to get the activity with ID 65, we need to call the /activities/id;65
endpoint. A little weird, right? 🤔
OR filter
If we want to get two separate activities that have the ID 65 OR 76, the syntax is similar. We must add ;76
to the previous endpoint: /activities/id;65;76
.
AND filter
Unfortunately, the filters can be more complicated. If we want to have the activities for 2021 (therefore from January 1 to December 31), we must have an AND filter. And what do you think the syntax looks like?
The URL would be /activities/startDate;012F%012F%2021/endDate;122F%312F%2021
. I am pretty sure you did not guess it, right? 🤣 The 2F%
is the encoded version of a slash (/
) character. And yes, the dates must be in the MM/DD/YYYY
format…
Toward the solution
Since we did not want to manually create the filters for all endpoints before calling them, we started looking for a better solution. So we start with a single @GetMapping
endpoint:
@GetMapping("/activities/{filters}")
List<Activity> getActivities(
@PathVariable(value = "filters") final String filters
);
Enter fullscreen mode Exit fullscreen mode An excerpt of our Feign client, with a
String as a
@PathVariable
But although the API returns results when we call it from our code, we found that the filters were not applied and the API returned all activities instead of an error… 🤯
Encoding
It turns out that Feign automatically encodes the URL path variable, so the final URI looks like /activities/startDate%3B04%252F01%252F2021
instead of /activities/startDate;012F%012F%2021
that their application was expecting (I removed the endDate
filter for clarity).
Thus, semicolons (;
) and forward slashes (/
) were encoded, as they are generally not expected in a path variable. Fortunately, we quickly found this answer from StackOverflow that steers us on the concept of RequestInterceptor. We were able to decode the encoded values with the code below.
import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.stereotype.Component;
/** * Interceptor to alter the URL used for the external API. The API uses characters that are encoded by Feign, so * this interceptor decode them back to their original values. * * Inspired from https://stackoverflow.com/a/61901509 */
@Component
public class PathEncodingExclusionInterceptor implements RequestInterceptor {
private static final String SEMI_COLON = "%3B";
private static final String PERCENTAGE = "%25";
@Override
public void apply(final RequestTemplate template) {
final String path = template.path();
// Make sure to decode the URL only one time, as the uri function can takes only relative URLs
if (path.contains(SEMI_COLON) || path.contains(PERCENTAGE)) {
// SOURCE : startDate ; 04 % 2F 01 % 2F 2021 /
// BEFORE : startDate %3B 04 %25 2F 01 %25 2F 2021 /
// AFTER : startDate ; 04 % 2F 01 % 2F 2021 /
template.uri(path.replaceAll(SEMI_COLON, ";").replaceAll(PERCENTAGE, "%"));
}
}
}
Enter fullscreen mode Exit fullscreen mode Our special
RequestInterceptor to decode some characters
Now that this issue is resolved, let’s get back to creating filters.
Final solution
Use a custom class as @PathVariable
Our original solution was to create a custom builder class that would build a String based on certain values passed as parameters.
But going through the code created by my colleague, I wonder if we could just pass a custom class to Feign as a @PathVariable
instead. So we quickly test the following code, where FilterField
is our custom class:
@GetMapping("/activities/{filters}")
List<Activity> getActivities(
@PathVariable(value = "filters") final FilterField filters
);
Enter fullscreen mode Exit fullscreen mode We replace the type of the
@PathVariable
from
String
to
FilterField
Let’s see the final URL that Feign calls in the output:
/activities/ca.usherbrooke.filter.FilterField%407ab4ae59
Enter fullscreen mode Exit fullscreen mode The URL, with the class name and something that looks like a hash code…
Overriding the toString function
Interesting, isn’t it? This looks like the default implementation of an object’s toString function. And what happens when we override it into our class?
package ca.usherbrooke.filter;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public class FilterField {
private final String field;
private final List<String> values;
@Override
public String toString() {
final String joinedValues = String.join(";", values);
return String.format("%s;%s", field, joinedValues);
}
}
Enter fullscreen mode Exit fullscreen mode An excerpt of our
FilterField
class
You guessed it, our URL looks like exactly as we want!
/activities/startDate;012F%012F%2021
Enter fullscreen mode Exit fullscreen mode Much better, right?
Using a List
But since we want to have the possibility to use a list of FilterField
, let’s update our client t0 allow it:
@GetMapping("/activities/{filters}")
List<Activity> getActivities(
@PathVariable(value = "filters") final List<FilterField> filters
);
Enter fullscreen mode Exit fullscreen mode We replace the type of the
@PathVariable
from
FilterField
to
List<FilterField>
/activities/startDate;012F%012F%2021,endDate;122F%312F%2021
Enter fullscreen mode Exit fullscreen mode Meh, we have a comma between our filters…
This is almost correct, the separator between the filters should be a slash (/
) instead of a comma (,
)! Guess how we solved this problem? By creating a new class!
Creation of a FilterFieldList
First, let’s replace our code in our Feign client:
@GetMapping("/activities/{filters}")
List<Activity> getActivities(
@PathVariable(value = "filters") final FilterFieldList filters
);
Enter fullscreen mode Exit fullscreen mode We finally replace the type of the
@PathVariable
from
List<FilterField>
to
FilterFieldList
And now, the implementation of our FilterFieldList
class:
package ca.usherbrooke.filter;
import java.util.Arrays;
import java.util.Collections;
import java.util.stream.Collectors;
public class FilterFieldList {
private final List<FilterField> fields;
public FilterFieldList(final FilterField... filterFields) {
fields = Arrays.asList(filterFields);
}
@Override
public String toString() {
final List<String> strings =
fields.stream().map(FilterField::toString).collect(Collectors.toList());
return String.join("/", strings);
}
}
Enter fullscreen mode Exit fullscreen mode Implementation code of the
FilterFieldList
class
With this final class, we now have a complete working solution for the weird external API that we are using! 🥳
/activities/startDate;012F%012F%2021/endDate;122F%312F%2021
Enter fullscreen mode Exit fullscreen mode The final URL, exactly as we want, but without using a
String @PathVariable
!
We initially try to extend List<FilterField>
with the overridden function, but somehow, Feign did not call our toString
function… Maybe Feign is doing something different if the custom class is an instance of List
? 🤔
Conclusion
So, in this post, you learned about two Feign concepts: RequestInterceptor and PathVariable. Hope this would help you as we wish we had found this post before!
Note : part of the code displayed was made by my colleague, Alexandre Côté, and I could have found this solution without its help. I think that together we managed to find a better solution, so thanks Alex!
暂无评论内容