Warm tip: This article is reproduced from stackoverflow.com, please click
feign query-parameters spring wiremock

Special characters in @QueryParam name in Spring feign client

发布于 2020-05-12 14:46:21

One of our external API uses query param names with special characters (don't ask me why, I don't know). My feign client's method for this API is declared like this:

@GetMapping("/v1/user/{userId}/orders")
List<Order> getOrders(
    @PathVariable("userId") String userId,
    @RequestParam("page[number]") Integer pageNumber,
    @RequestParam("page[size]") Integer pageSize);

As I mentioned, request params contain special characters [ and ]. I'm using Spock for testing and I want to set up Wiremock stub like this:

wiremock.stubFor(
  get(urlPathMatching('/v1/users/' + userId + '/orders'))
    .withQueryParam("page[number]", new EqualToPattern("1"))
    .withQueryParam("page[size]", new AnythingPattern())
    .willReturn(
      status(200)
        .withHeader("Content-type", "application/json")
        .withBody("""[]""")
    ))

But I get:

--------------------------------------------------------------------------------------------------
| Closest stub                        | Request                                                  |
--------------------------------------------------------------------------------------------------
                                      |
GET                                   | GET
/v1/users/123/orders                  | /v1/users/123/orders?page%5Bnumber%5D=%7Bpage%5Bnumber%5
                                      | D%7D&page%5Bsize%5D=%7Bpage%5Bsize%5D%7D
                                      |
Query: page[number] = 1               |                                 <<<<< Query is not present
Query: page[size] [anything] (always) |                                 <<<<< Query is not present
                                      |
--------------------------------------------------------------------------------------------------

After lots of trials and errors, I came up with a solution to use @PathVariables instead of @RequestParams in feign client method:

@GetMapping("/v1/users/{userId}/orders?page[number]={pageNumber}&page[size]={pageSize}")
List<Order> getOrders(
    @PathVariable("userId") String userId,
    @PathVariable("pageNumber") Integer pageNumber,
    @PathVariable("pageSize") Integer pageSize);

and encode all query params in Wiremock

wiremock.stubFor(
    get(urlPathMatching('/v1/users/' + userId + '/orders'))
    .withQueryParam("page%5Bnumber%5D", new EqualToPattern("1"))
    .withQueryParam("page%5Bsize%5D", new AnythingPattern())
    .willReturn(
        status(200)
        .withHeader("Content-type", "application/json")
        .withBody("""[]""")
    ))

Then it works. But it looks like a kind of a hack. It is problematic to use an optional query params as well.

Is there a way to use @RequestParams with special characters? It looks like a bug in Spring?

In the mean time I will try to debug it to understand where is the problem.

Questioner
zolv
Viewed
610
amseager 2020-03-01 01:51

First of all, it's not a bug in Spring. The main goal of these spring-like annotations (@RequestParam, @RequestMapping etc. from spring-web) is to use them in Spring MVC controllers on the server-side and not for create urls for requests.

In the case of Feign clients, there is another library, spring-cloud-openfeign, which allows you to have them in Feign Clients for request mapping instead of original Feign annotations (like @RequestLine, @Param and so on). SpringMvcContract is responsible for it, and it is used by default in Spring applications. But it's just for the convenience of this specific group of developers who're using Feign with Spring, so it's not a reason to change signatures of these annotations or add them some new settings.

About described problem - seems like there isn't any "official" way to disable url encoding even if you're using original Feign annotations for request mapping - at the moment (version 10.7.4), you can only do it for slashes (check this issue, for example).

I've performed some further investigation and come to these points:

  1. While constructing the request query template, you cannot set the encoding option manually, because it's just hardcoded (look at UriTemplate and QueryTemplate source code).
  2. You can actually fix the constructed url later by writing your own RequestInterceptor (by reflection, f.e.), but it's actually pointless because after applying all interceptors the request is built and, at the moment, it implicitly encodes the brackets again (check what's happened under the hood, basically it uses queryTemplate.toString() which, in turn, encodes brackets while using methods of UriUtils class)
  3. After the request creation, it is started to execute by Client, and I actually managed to fix the url at this step. I inherited a class from the standard Client.Default, registered it instead of the original auto-configurated one, and then I did my job before actually invoking the client (ofc it's possible to do the same for a different client, f.e., for LoadBalancerFeignClient):
import feign.Client;
import feign.Request;
import feign.Response;
import org.springframework.stereotype.Component;
import org.springframework.util.ReflectionUtils;

import java.io.IOException;
import java.lang.reflect.Field;

@Component
public class MyClient implements Client {

    private Client delegate;

    public MyClient() {
        this.delegate = new Client.Default(null, null);
    }

    @Override
    public Response execute(Request request, Request.Options options) throws IOException {
        fixRequestUrlIfPossible(request);
        return delegate.execute(request, options);
    }

    private void fixRequestUrlIfPossible(Request request) {
        try {
            Field urlField = ReflectionUtils.findField(Request.class, "url");
            urlField.setAccessible(true);

            String encodedUrl = (String) urlField.get(request);
            String url = encodedUrl.replace("%5B", "[").replace("%5D", "]");
            urlField.set(request, url);
        } catch (Exception e) {
            /*NOP*/
        }
    }
}

But it's error prone because of the reflection (can stop work in later versions of Feign).

So, it's up to you - if you're not afraid of doing this kind of things, use this approach. I actually like your version more. The main question is: is your real server able to handle requests with encoded brackets? Problems with Wiremock are less important, in my opinion.