For WIremock's Fault.MALFORMED_RESPONSE_CHUNK Feign's retry mechanisms does not kick in and the exception is thrown without a retry. For Fault.CONNECTION_RESET_BY_PEER the retry mechanism works as expected.

I'm using the default Retryer (feign. Retryer.Default) and a custom ErrorDecoder (both are not invoked)

I've noticed the org.springframework.cloud.openfeign.support.SpringDecoder class mentioned in the stacktrace, but also raised an issue in the feign-core project: https://github.com/OpenFeign/feign/issues/2129

My test case is similar to this:

stubFor(get(urlPathEqualTo("/test-url"))
  .inScenario("retry test")
  .willReturn(aResponse().withFault(Fault.MALFORMED_RESPONSE_CHUNK))
  .willSetStateTo("failed once"));
stubFor(get(urlPathEqualTo("/test-url"))
  .inScenario("retry test")
  .whenScenarioStateIs("failed once")
  .willReturn(okJson(fromFile("response.json"))));

var response = feignClient.getTestUrl();
assertThat(response).isPresent();
verify(exactly(2), getRequestedFor(urlPathEqualTo("/test-url")));

I get an exception that is similar to this:

Feign.FeignException: Premature EOF reading GET http://localhost:11390/test-url

    at feign.FeignException.errorReading(FeignException.java:167)
    at feign.InvocationContext.proceed(InvocationContext.java:42)
    at feign.ResponseHandler.decode(ResponseHandler.java:122)
    at feign.ResponseHandler.handleResponse(ResponseHandler.java:73)
    at feign.SynchronousMethodHandler.executeAndDecode(SynchronousMethodHandler.java:114)
    at feign.SynchronousMethodHandler.invoke(SynchronousMethodHandler.java:70)
    at feign.ReflectiveFeign$FeignInvocationHandler.invoke(ReflectiveFeign.java:96)
    ...

Caused by: java.io.IOException: Premature EOF
    at java.base/sun.net.www.http.ChunkedInputStream.readAheadBlocking(ChunkedInputStream.java:567)
    at java.base/sun.net.www.http.ChunkedInputStream.readAhead(ChunkedInputStream.java:611)
    at java.base/sun.net.www.http.ChunkedInputStream.read(ChunkedInputStream.java:705)
    at java.base/java.io.FilterInputStream.read(FilterInputStream.java:119)
    at java.base/sun.net.www.protocol.http.HttpURLConnection$HttpInputStream.read(HttpURLConnection.java:3674)
    at java.base/sun.net.www.protocol.http.HttpURLConnection$HttpInputStream.read(HttpURLConnection.java:3667)
    at java.base/sun.net.www.protocol.http.HttpURLConnection$HttpInputStream.read(HttpURLConnection.java:3655)
    at java.base/java.io.FilterInputStream.read(FilterInputStream.java:71)
    at java.base/java.io.PushbackInputStream.read(PushbackInputStream.java:147)
    at org.springframework.web.client.IntrospectingClientHttpResponse.hasEmptyMessageBody(IntrospectingClientHttpResponse.java:100)
    at org.springframework.web.client.HttpMessageConverterExtractor.extractData(HttpMessageConverterExtractor.java:90)
    at org.springframework.cloud.openfeign.support.SpringDecoder.decode(SpringDecoder.java:75)
    at org.springframework.cloud.openfeign.support.ResponseEntityDecoder.decode(ResponseEntityDecoder.java:61)
    at feign.optionals.OptionalDecoder.decode(OptionalDecoder.java:42)
    at feign.InvocationContext.proceed(InvocationContext.java:36)

Tested this on :

Spring Cloud Open Feign 4.0.2 Feign Core 12.4 (bumped from spring's 12.1)

Comment From: OlgaMaciaszek

Hello @elmozgo, please provide a minimal, complete, verifiable example that reproduces the issue.

Comment From: spring-cloud-issues

If you would like us to look at this issue, please provide the requested information. If the information is not provided within the next 7 days this issue will be closed.

Comment From: elmozgo

Dependencies

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

org.springframework.cloud:spring-cloud-openfeign-core:jar:4.0.4:compile io.github.openfeign:feign-core:jar:12.4:compile

client under test - Issue877FeignClient.java:

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;

import java.util.Optional;

@FeignClient(
        name = "issue-877-feign-client",
        url = "${issue877-feign.url}",
        dismiss404 = true)
public interface Issue877FeignClient {

    @GetMapping(value = "/test-url")
    Optional<Issue877Response> get();
}

Issue877Response.java:

public record Issue877Response(String value) {}

IT with partial context - Issue877FeignClientTest.java

import com.github.tomakehurst.wiremock.http.Fault;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.contract.wiremock.AutoConfigureWireMock;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.cloud.openfeign.FeignAutoConfiguration;
import org.springframework.cloud.openfeign.FeignClientsConfiguration;
import org.springframework.context.annotation.Import;

import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
import static com.github.tomakehurst.wiremock.client.WireMock.exactly;
import static com.github.tomakehurst.wiremock.client.WireMock.get;
import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor;
import static com.github.tomakehurst.wiremock.client.WireMock.okJson;
import static com.github.tomakehurst.wiremock.client.WireMock.reset;
import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo;
import static com.github.tomakehurst.wiremock.client.WireMock.verify;
import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest(
        classes = {
                HttpMessageConvertersAutoConfiguration.class,
                FeignAutoConfiguration.class,
                FeignClientsConfiguration.class
        },
        properties = {
                "issue877-feign.url=http://localhost:${wiremock.server.port}/",
                "logging.level.WireMock=WARN"
        })
@AutoConfigureWireMock(port = 0)
@Import(Issue877FeignClientTest.TestConfig.class)
class Issue877FeignClientTest {

    @BeforeEach
    void resetWiremock() {
        reset();
    }

    @Autowired
    Issue877FeignClient issue877FeignClient;

    private static final String RESPONSE = """
            {
             "value": "hello test"
            }
            """;

    @Test
    void shouldDecode() {

        stubFor(get(urlPathEqualTo("/test-url"))
                .willReturn(okJson(RESPONSE)));

        var response = issue877FeignClient.get();
        assertThat(response).isPresent();
        assertThat(response.get().value()).isEqualTo("hello test");
        verify(exactly(1), getRequestedFor(urlPathEqualTo("/test-url")));
    }

    @Test
    void shouldDecode404() {

        stubFor(get(urlPathEqualTo("/test-url"))
                .willReturn(aResponse().withStatus(404)));

        var response = issue877FeignClient.get();
        assertThat(response).isEmpty();
        verify(exactly(1), getRequestedFor(urlPathEqualTo("/test-url")));
    }

    @Test
    void shouldRetryConnectionResetByPeer() {

        stubFor(get(urlPathEqualTo("/test-url"))
                .inScenario("retry test")
                .willReturn(aResponse().withFault(Fault.CONNECTION_RESET_BY_PEER))
                .willSetStateTo("failed once"));
        stubFor(get(urlPathEqualTo("/test-url"))
                .inScenario("retry test")
                .whenScenarioStateIs("failed once")
                .willReturn(okJson(RESPONSE)));

        var response = issue877FeignClient.get();
        assertThat(response).isPresent();
        assertThat(response.get().value()).isEqualTo("hello test");
        verify(exactly(2), getRequestedFor(urlPathEqualTo("/test-url")));
    }

    // this fails
    @Test
    void shouldRetryRandomDataThenClose() {

        stubFor(get(urlPathEqualTo("/test-url"))
                .inScenario("retry test")
                .willReturn(aResponse().withFault(Fault.RANDOM_DATA_THEN_CLOSE))
                .willSetStateTo("failed once"));
        stubFor(get(urlPathEqualTo("/test-url"))
                .inScenario("retry test")
                .whenScenarioStateIs("failed once")
                .willReturn(okJson(RESPONSE)));

        var response = issue877FeignClient.get();
        assertThat(response).isPresent();
        assertThat(response.get().value()).isEqualTo("hello test");
        verify(exactly(2), getRequestedFor(urlPathEqualTo("/test-url")));
    }

    @Test
    void shouldRetryEmptyResponse() {

        stubFor(get(urlPathEqualTo("/test-url"))
                .inScenario("retry test")
                .willReturn(aResponse().withFault(Fault.EMPTY_RESPONSE))
                .willSetStateTo("failed once"));
        stubFor(get(urlPathEqualTo("/test-url"))
                .inScenario("retry test")
                .whenScenarioStateIs("failed once")
                .willReturn(okJson(RESPONSE)));

        var response = issue877FeignClient.get();
        assertThat(response).isPresent();
        assertThat(response.get().value()).isEqualTo("hello test");
        verify(exactly(2), getRequestedFor(urlPathEqualTo("/test-url")));
    }

    // this fails
    @Test
    void shouldRetryMalformedResponseChunk() {

        stubFor(get(urlPathEqualTo("/test-url"))
                .inScenario("retry test")
                .willReturn(aResponse().withFault(Fault.MALFORMED_RESPONSE_CHUNK))
                .willSetStateTo("failed once"));
        stubFor(get(urlPathEqualTo("/test-url"))
                .inScenario("retry test")
                .whenScenarioStateIs("failed once")
                .willReturn(okJson(RESPONSE)));

        var response = issue877FeignClient.get();
        assertThat(response).isPresent();
        assertThat(response.get().value()).isEqualTo("hello test");
        verify(exactly(2), getRequestedFor(urlPathEqualTo("/test-url")));
    }

    @EnableFeignClients
    static class TestConfig {
    }
}

Comment From: OlgaMaciaszek

@elmozgo can you please provide the sample as a link to a GH repo with a small executable, minimal app, so that all the dependencies are in place and everything compiles without having to make changes and only the necessary components are there (for example, the test uses methods from Spring Cloud Contract, which probably are not related to your issue?)?

Comment From: spring-cloud-issues

If you would like us to look at this issue, please provide the requested information. If the information is not provided within the next 7 days this issue will be closed.

Comment From: elmozgo

https://github.com/elmozgo/spring-cloud-openfeign-issue877

The spring-cloud-contract-wiremock dependency is used in the test to simulate server or network faults that are to be retried by spring-cloud-openfeign mechanisms in the http client.

Looks like fault simulation types that are related to network/io are being retried, but those that are related to incorrect server responses (in this case: empty response and corrupted data in the payload) are not. Is this behaviour by design?

Comment From: OlgaMaciaszek

Hello @elmozgo, sure, I know it's to simulate faults, but it'd still be easier to use to reproduce the issue if a sample with necessary dependencies has been provided. When it comes to the FeignException you've pasted, it's probably being thrown from here. That's within the OpenFeign/feign project and not this one. Spring Cloud OpenFeign, however, allows you override Retryer and ErrorDecoder with your custom one, but if this is causing an issue, we'll really need a sample to to verify your setup. Please add a sample as an a link to a small repo with executable and compiling code (app or tests) - we can then take a look at it.

Comment From: spring-cloud-issues

If you would like us to look at this issue, please provide the requested information. If the information is not provided within the next 7 days this issue will be closed.

Comment From: spring-cloud-issues

Closing due to lack of requested feedback. If you would like us to look at this issue, please provide the requested information and we will re-open the issue.