Skip to content

f/error-as-a-value-even-when-error-occurs: init - #3559

Draft
ihsansfd wants to merge 1 commit into
OpenFeign:masterfrom
ihsansfd:f/error-as-a-value-even-when-error-occurs
Draft

f/error-as-a-value-even-when-error-occurs: init#3559
ihsansfd wants to merge 1 commit into
OpenFeign:masterfrom
ihsansfd:f/error-as-a-value-even-when-error-occurs

Conversation

@ihsansfd

@ihsansfd ihsansfd commented Sep 5, 2026

Copy link
Copy Markdown

…o return error bodies as values instead of throwing

Feign hands every non-2xx response to the ErrorDecoder and throws, so a
service that describes its failures in the response body rather than by
status alone cannot reach that body from a Feign client. The workarounds
are declaring Response as the return type and decoding by hand, or
writing a ResponseInterceptor that skips the chain.

BaseBuilder#decodeErrorResponses() makes Feign decode the error body
into the method's declared return type and return it, rather than
throwing. The signature is the declaration of what an error body should
decode to, so no type is configured on the builder.

The flag engages only when all of the following hold, so the default
behaviour is untouched everywhere else:

  - the status is >= 400. 3xx stays with RedirectionInterceptor.
  - the decoder accepts the response, via PredicatedDecoder#canDecode,
    so a JSON decoder is not handed an HTML error page from a proxy.
  - the ErrorDecoder did not classify the response as retryable, so
    Retryer keeps working on 503/Retry-After as it does today.
  - the body actually decodes, otherwise the ErrorDecoder's exception is
    thrown with the decode failure attached as suppressed.

The flag applies to every method on the client. It deliberately does not
check that the return type is one an error body makes sense as: most
decoders ignore unknown properties, so an error body decodes into an
unrelated type and yields an all-null object rather than failing, which
makes such a check unreliable. This is documented on the builder method,
with the guidance to use a separate client for methods that should keep
throwing.

The body is buffered once, since both the ErrorDecoder and the decoder
read it and a response body is not generally replayable. This adds no
memory exposure: DefaultErrorDecoder already reads the whole body to
build its message on every non-2xx response.

The response passed to the decoder has its status rewritten to 200.
JacksonDecoder, Jackson3Decoder and OptionalDecoder all short-circuit
404 and 204 to an empty value without reading the body, which would
discard the very body being asked for. Doing it here rather than in each
decoder also covers third-party decoders. TypedResponse is still built
from the real response, so its status() stays truthful.

Because the logic lives in InvocationContext#proceed, AsyncFeign gets
the same behaviour. The new ResponseHandler and AsyncResponseHandler
constructors are overloads, so the existing ones are unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FJJJkfmVGccG7XPEeGFosm
@ihsansfd
ihsansfd force-pushed the f/error-as-a-value-even-when-error-occurs branch from 445782b to ea9a37e Compare September 5, 2026 16:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant