You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
We use feign-java11 and feign-gson dependencies in our project and after we migrated from 13.1 to 13.5 one of our tests started failing.
This test uses WireMock and verifies that if malformed response is sent to the client then RetryableException is thrown.
In 13.1 this test passed but in 13.5 fails. Our investigation shown that in both Feign versions IOException is thrown but in 13.1 it's correctly caught in SynchronousMethodHandler.executeAndDecode:
java.io.IOException: chunked transfer encoding, state: READING_LENGTH
And in 13.5 it's not caught. The last line that was executed is
I'm fine with that but it's breaking change and should be mentioned in release notes and documentation.
This should be a bug -> #2419 . If it's version 13.1, then using other clients(eg: apache httpclient or okhttp3) should yield different expected results.
Thanks for narrowing this to the Http2Client change from BodyHandlers.ofByteArray() to BodyHandlers.ofInputStream() in 13.3. That makes this useful as a regression candidate, especially because the exception now escapes after the Feign invocation when the caller reads the response body.
A compatible fix needs to preserve existing streaming behavior while restoring the expected retry/exception handling for malformed responses. I would gladly review a PR that adds a focused regression test around the WireMock malformed chunk case and keeps the public API unchanged.
Dug into this a bit more since @velo asked for a focused regression test — wanted to share what I found before opening a PR, because the root cause and the "correct" fix are more entangled than they look.
executeAndDecode wraps client.execute() IOExceptions via FeignException.errorExecuting(...) → RetryableException. But once client.execute() returns successfully, decoding happens in InvocationContext.decode():
FeignException.errorReading(...) returns a plain FeignException, not a RetryableException, so it never reaches the retry catch block. Before #2420 switched Http2Client from BodyHandlers.ofByteArray() to BodyHandlers.ofInputStream(), the chunked-encoding validation happened duringclientForRequest.send(...), i.e. inside execute(), so it went through errorExecuting and was retryable. After #2420, that same validation is deferred until the decoder actually reads the (now lazy) InputStream, which routes through errorReading instead. Same underlying IOException, different classification, purely because of when the bytes get validated — that's the regression.
Why the "obvious" fix isn't safe
The naive fix — make errorReading produce a RetryableException too — directly conflicts with an existing, deliberately-named test:
That test (and its name) encodes an intentional principle: once a response has come back from the server, Feign won't auto-retry on a subsequent decode failure, presumably because the server may have already applied a non-idempotent side effect, and blindly retrying at that point isn't safe in general. Since InvocationContext.decode() can't distinguish "transport truncated the stream" from "a custom Decoder threw IOException for its own reasons," making all errorReading IOExceptions retryable would quietly reverse that safety principle for every Client/Decoder, not just Http2Client + malformed chunked encoding.
A narrower option that doesn't touch that principle
InvocationContext already has precedent for a size-bounded eager-read: disconnectResponseBodyIfNeeded buffers raw Response bodies up to 8KB before handing them back, specifically so small bodies behave synchronously. The same idea applied to Http2Client itself — eagerly read (and validate) small/typical response bodies (e.g. via BodyHandlers.ofByteArray() under a size threshold, falling back to ofInputStream() above it) — would restore the pre-#2420 retry behavior for the common "small JSON API response" case reported here, without reintroducing the large-body memory issue #2420 was fixing, and without changing errorReading's classification for everyone else.
Happy to put together a PR for that narrower version (Http2Client-scoped, size-bounded eager read + a regression test reproducing this issue's WireMock malformed-chunk case) if that direction sounds right — didn't want to send a patch that either quietly reverts #2420 or contradicts doesntRetryAfterResponseIsSent without checking first. cc @hdfg159 since you authored #2420 and know the original memory-usage motivation best.
Activity
sergey-morenets commentedon Dec 9, 2024
Our deeper analysis shown that this regression appeared in 13.3 and possible reason is the replacement
httpResponse = clientForRequest.send(httpRequest, BodyHandlers.ofByteArray());with
httpResponse = clientForRequest.send(httpRequest, BodyHandlers.ofInputStream());in Http2Client.
sergey-morenets commentedon Dec 10, 2024
So IOException actually happens then we try to read response body (in IOUtils.toString):
And main issue is that this exception occurs outside of Feign client and that means retry logic would not be executed.
hdfg159 commentedon Dec 27, 2024
If
cityClient.create(city)returns afeign.Response, it should indeed handle theinputStreamrelated exceptions by itself.If you want to reproduce the previous logic, the return type should be filled with
byte[].eg:
cityClientsergey-morenets commentedon Dec 28, 2024
Hi @hdfg159
I'm fine with that but it's breaking change and should be mentioned in release notes and documentation.
hdfg159 commentedon Dec 28, 2024
This should be a bug -> #2419 . If it's version 13.1, then using other clients(eg: apache httpclient or okhttp3) should yield different expected results.
hdfg159 commentedon Dec 28, 2024
#1474 (comment)
velo commentedon Jun 18, 2026
Thanks for narrowing this to the Http2Client change from
BodyHandlers.ofByteArray()toBodyHandlers.ofInputStream()in 13.3. That makes this useful as a regression candidate, especially because the exception now escapes after the Feign invocation when the caller reads the response body.A compatible fix needs to preserve existing streaming behavior while restoring the expected retry/exception handling for malformed responses. I would gladly review a PR that adds a focused regression test around the WireMock malformed chunk case and keeps the public API unchanged.
seonwooj0810 commentedon Aug 5, 2026
Dug into this a bit more since @velo asked for a focused regression test — wanted to share what I found before opening a PR, because the root cause and the "correct" fix are more entangled than they look.
Where the exception actually gets (mis)classified
The retry loop only retries
RetryableException:executeAndDecodewrapsclient.execute()IOExceptions viaFeignException.errorExecuting(...)→RetryableException. But onceclient.execute()returns successfully, decoding happens inInvocationContext.decode():FeignException.errorReading(...)returns a plainFeignException, not aRetryableException, so it never reaches the retry catch block. Before #2420 switchedHttp2ClientfromBodyHandlers.ofByteArray()toBodyHandlers.ofInputStream(), the chunked-encoding validation happened duringclientForRequest.send(...), i.e. insideexecute(), so it went througherrorExecutingand was retryable. After #2420, that same validation is deferred until the decoder actually reads the (now lazy)InputStream, which routes througherrorReadinginstead. Same underlying IOException, different classification, purely because of when the bytes get validated — that's the regression.Why the "obvious" fix isn't safe
The naive fix — make
errorReadingproduce aRetryableExceptiontoo — directly conflicts with an existing, deliberately-named test:That test (and its name) encodes an intentional principle: once a response has come back from the server, Feign won't auto-retry on a subsequent decode failure, presumably because the server may have already applied a non-idempotent side effect, and blindly retrying at that point isn't safe in general. Since
InvocationContext.decode()can't distinguish "transport truncated the stream" from "a customDecoderthrew IOException for its own reasons," making allerrorReadingIOExceptions retryable would quietly reverse that safety principle for everyClient/Decoder, not justHttp2Client+ malformed chunked encoding.A narrower option that doesn't touch that principle
InvocationContextalready has precedent for a size-bounded eager-read:disconnectResponseBodyIfNeededbuffers rawResponsebodies up to 8KB before handing them back, specifically so small bodies behave synchronously. The same idea applied toHttp2Clientitself — eagerly read (and validate) small/typical response bodies (e.g. viaBodyHandlers.ofByteArray()under a size threshold, falling back toofInputStream()above it) — would restore the pre-#2420 retry behavior for the common "small JSON API response" case reported here, without reintroducing the large-body memory issue #2420 was fixing, and without changingerrorReading's classification for everyone else.Happy to put together a PR for that narrower version (
Http2Client-scoped, size-bounded eager read + a regression test reproducing this issue's WireMock malformed-chunk case) if that direction sounds right — didn't want to send a patch that either quietly reverts #2420 or contradictsdoesntRetryAfterResponseIsSentwithout checking first. cc @hdfg159 since you authored #2420 and know the original memory-usage motivation best.