Solve the problem of FeignClient sending post request exception

  • 2021-10-24 23:02:58
  • OfStack

FeignClient send post request exception

This question is actually very basic. But it stumped me. Record 1

Specify the message format when sending post requests

The correct way to write it is like this


@PostMapping(value = "/test/post", consumes = "application/json")
 String test(@RequestBody String name);

Invalid writing


@PostMapping(value = "/test/post", produces= "application/json")

About this distinction

produces Its function is to specify the return value type, not only can set the return value type can also set the character code of the return value;

consumes Specify the type of submission content to process the request (Content-Type), such as application/json, text/html;

The foundation is really important ~

Analysis and Treatment of Query Parameters Missing When FeignClient Calls POST Request

This article does not introduce the knowledge points of FeignClient in detail. There are many excellent articles on the Internet that introduce the knowledge points of FeignCient. I will not repeat them here, but focus on this problem.

Query parameter loss scenario

Service Description: The service system needs to update the A resource in the user system. Since only one field information of the A resource is updated to B, it is not selected to encapsulate B through entity, but directly transfer B information through query parameters

Text description: When using FeignClient to make a remote call, if there are query parameters in the POST request and there is no request entity (body is empty), then the query parameters are lost and the service provider cannot get the value of the query parameters.

Code description: The value of B is lost, and the service provider cannot get the value of B


@FeignClient(name = "a-service", configuration = FeignConfiguration.class)
public interface ACall {
 
    @RequestMapping(method = RequestMethod.POST, value = "/api/xxx/{A}", headers = {"Content-Type=application/json"})
    void updateAToB(@PathVariable("A") final String A, @RequestParam("B") final String B) throws Exception;
}

Problem analysis

Background Using the FeignClient client Use ApacheHttpClient in feign-httpclient to make the actual request call

<dependency>
    <groupId>com.netflix.feign</groupId>
    <artifactId>feign-httpclient</artifactId>
    <version>8.18.0</version>
</dependency>
Direct source code

Through reading the source code of FeignClient, it is found that the problem is not parsing the parameters, but when using ApacheHttpClient to request, it puts the query parameters into the request body. See how the source code is handled concretely below

feign. httpclient. ApacheHttpClient This is how feign-httpclient makes the actual request


@Override
  public Response execute(Request request, Request.Options options) throws IOException {
    HttpUriRequest httpUriRequest;
    try {
      httpUriRequest = toHttpUriRequest(request, options);
    } catch (URISyntaxException e) {
      throw new IOException("URL '" + request.url() + "' couldn't be parsed into a URI", e);
    }
    HttpResponse httpResponse = client.execute(httpUriRequest);
    return toFeignResponse(httpResponse);
  }
 
  HttpUriRequest toHttpUriRequest(Request request, Request.Options options) throws
          UnsupportedEncodingException, MalformedURLException, URISyntaxException {
    RequestBuilder requestBuilder = RequestBuilder.create(request.method());
 
    //per request timeouts
    RequestConfig requestConfig = RequestConfig
            .custom()
            .setConnectTimeout(options.connectTimeoutMillis())
            .setSocketTimeout(options.readTimeoutMillis())
            .build();
    requestBuilder.setConfig(requestConfig);
 
    URI uri = new URIBuilder(request.url()).build();
 
    requestBuilder.setUri(uri.getScheme() + "://" + uri.getAuthority() + uri.getRawPath());
 
    //request query params
    List<NameValuePair> queryParams = URLEncodedUtils.parse(uri, requestBuilder.getCharset().name());
    for (NameValuePair queryParam: queryParams) {
      requestBuilder.addParameter(queryParam);
    }
 
    //request headers
    boolean hasAcceptHeader = false;
    for (Map.Entry<String, Collection<String>> headerEntry : request.headers().entrySet()) {
      String headerName = headerEntry.getKey();
      if (headerName.equalsIgnoreCase(ACCEPT_HEADER_NAME)) {
        hasAcceptHeader = true;
      }
 
      if (headerName.equalsIgnoreCase(Util.CONTENT_LENGTH)) {
        // The 'Content-Length' header is always set by the Apache client and it
        // doesn't like us to set it as well.
        continue;
      }
 
      for (String headerValue : headerEntry.getValue()) {
        requestBuilder.addHeader(headerName, headerValue);
      }
    }
    //some servers choke on the default accept string, so we'll set it to anything
    if (!hasAcceptHeader) {
      requestBuilder.addHeader(ACCEPT_HEADER_NAME, "*/*");
    }
 
    //request body
    if (request.body() != null) {
 
    //body Is empty, then HttpEntity Empty 
 
      HttpEntity entity = null;
      if (request.charset() != null) {
        ContentType contentType = getContentType(request);
        String content = new String(request.body(), request.charset());
        entity = new StringEntity(content, contentType);
      } else {
        entity = new ByteArrayEntity(request.body());
      }
 
      requestBuilder.setEntity(entity);
    }
 
    // Call org.apache.http.client.methods.RequestBuilder#build Method 
    return requestBuilder.build();
  }

org. apache. http. client. methods. RequestBuilder This class is the Builder class of HttpUriRequest. See the build method below


public HttpUriRequest build() {
        final HttpRequestBase result;
        URI uriNotNull = this.uri != null ? this.uri : URI.create("/");
        HttpEntity entityCopy = this.entity;
        if (parameters != null && !parameters.isEmpty()) {
    //  Here: If HttpEntity Is empty and is POST Request or for PUT When requested, this method will take out the query parameters and encapsulate them as HttpEntity
    //  It is here that the query parameters are discarded, to be exact, the location is converted 
            if (entityCopy == null && (HttpPost.METHOD_NAME.equalsIgnoreCase(method)
                    || HttpPut.METHOD_NAME.equalsIgnoreCase(method))) {
                entityCopy = new UrlEncodedFormEntity(parameters, charset != null ? charset : HTTP.DEF_CONTENT_CHARSET);
            } else {
                try {
                    uriNotNull = new URIBuilder(uriNotNull)
                      .setCharset(this.charset)
                      .addParameters(parameters)
                      .build();
                } catch (final URISyntaxException ex) {
                    // should never happen
                }
            }
        }
        if (entityCopy == null) {
            result = new InternalRequest(method);
        } else {
            final InternalEntityEclosingRequest request = new InternalEntityEclosingRequest(method);
            request.setEntity(entityCopy);
            result = request;
        }
        result.setProtocolVersion(this.version);
        result.setURI(uriNotNull);
        if (this.headergroup != null) {
            result.setHeaders(this.headergroup.getAllHeaders());
        }
        result.setConfig(this.config);
        return result;
    }

Solutions

Now that you know the reason, there are many solutions. Here are some general solutions:

Use feign-okhttp to request calls, here does not column source code, interested in you can see, feign-okhttp bottom did not judge if body is empty then put the query parameters into body. Using io. github. openfeign: feign-httpclient: 9.5. 1 dependencies, the reasons for intercepting some source codes are as follows:

HttpUriRequest toHttpUriRequest(Request request, Request.Options options) throws
          UnsupportedEncodingException, MalformedURLException, URISyntaxException {
    RequestBuilder requestBuilder = RequestBuilder.create(request.method());
 
   // Omit part of the code 
    //request body
    if (request.body() != null) {
      // Omit part of the code 
    } else {
    //  Here, if the null Will be stuffed into the 1 A byte Array is 0 Object of 
      requestBuilder.setEntity(new ByteArrayEntity(new byte[0]));
    }
 
    return requestBuilder.build();
  }

Recommended dependency


<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-httpclient</artifactId>
    <version>9.5.1</version>
</dependency>

Or


<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-okhttp</artifactId>
    <version>9.5.1</version>
</dependency>

Summarize

At present, most articles on feign are recommended com. netflix. feign: feign-httpclient: 8.18. 0 and com. netflix. feign: feign-okhttp: 8.18. 0. If you happen to use com. netflix. feign: feign-httpclient: 8.18. 0, the problem of missing query parameters will occur when POST requests and body is empty.

It is recommended that you do not rely on com. netflix. feign when using feign-httpclient or feign-okhttp, but choose io. github. openfeign, because it seems that Netflix has not maintained these two components for a long time, but OpenFeign has maintained them.


Related articles: