OpenFeign/feign · error · DecodeException

Unable to decode response ( ) ...

Error message

Unable to decode {status} response ({headers}) ...

What it means

Feign's MultiDecoder iterates its registered PredicatedDecoders and throws this DecodeException when none of them declares it can decode the response for the requested type. It is a configuration/coverage error: the decoder chain simply has no entry matching the response's Content-Type and the target Java type. The message includes the response status and headers so you can see which Content-Type went unmatched.

Solutions

  1. Register a decoder whose canDecode matches the actual response Content-Type and return type, e.g. .decoder(new JacksonDecoder()) or a custom PredicatedDecoder
  2. Add a catch-all/fallback decoder (e.g. new Decoder() returning String or the default decoder) as the last entry in the MultiDecoder builder
  3. Check the Content-Type header in the message and confirm the server is returning the format you expect; fix the endpoint or add the matching codec module (gson/jackson/etc.)
  4. Decode to String first to inspect the body if the format is unexpected

Example fix

// before
Feign.builder().decoder(new MultiDecoder.Builder().add(new JacksonDecoder()).build());
// after
Feign.builder().decoder(new MultiDecoder.Builder()
    .add(new JacksonDecoder())
    .add(new Decoder() { // fallback
      public Object decode(Response r, Type t) {
        return Util.toString(r.body().asReader(r.charset()));
      }
    }).build());
Defensive patterns

Strategy: try-catch

Validate before calling

// before building the client, verify decoder coverage for expected content types
List<String> expected = List.of("application/json", "text/plain");
boolean covered = expected.stream().anyMatch(ct ->
    registeredDecoders.stream().anyMatch(d -> d.canDecode(
        Response.builder().status(200).reason("OK")
            .request(Request.create(Request.HttpMethod.GET, "/", Collections.emptyMap(), null, Util.UTF_8))
            .headers(Collections.singletonMap("Content-Type", List.of(ct)))
            .build(), MyType.class)));

Try / catch

try {
  return api.call();
} catch (DecodeException e) {
  if (e.getMessage().startsWith("Unable to decode")) {
    // inspect e.status()/content type, add matching decoder or fall back to raw body
    throw new ClientConfigurationException("No decoder for response: " + e.getMessage(), e);
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling a Feign client built with Feign.builder().decoder(MultiDecoder) (or the default pipeline) where the response's Content-Type (e.g. application/xml, text/plain, application/hal+json) matches no registered decoder's predicate for the method's return type.

Common situations: Server returns a Content-Type the decoder list does not cover (e.g. text/plain error body or application/problem+json); a custom decoder's canDecode predicate is too narrow; an encoder/decoder was registered for JSON but the endpoint returns XML; after upgrading, Feign's default decoder no longer covers a type the old default handled.

Understand the failure class

Related errors


AI-assisted analysis of OpenFeign/feign@e2a1e27560 (2026-09-10). Data as JSON: /api/errors/e6dd8a31a55407b4. Report an issue: GitHub.

Appendix: source

Thrown at core/src/main/java/feign/codec/MultiDecoder.java:112

  /**
   * Decodes using the first decoder that accepts the response.
   *
   * @param response {@inheritDoc}
   * @param type {@inheritDoc}
   * @return {@inheritDoc}
   * @throws IOException {@inheritDoc}
   * @throws DecodeException when no decoder accepts the response, or the chosen one fails
   * @throws FeignException {@inheritDoc}
   */
  @Override
  public Object decode(Response response, Type type)
      throws IOException, DecodeException, FeignException {
    for (PredicatedDecoder decoder : decoders) {
      if (decoder.canDecode(response, type)) {
        return decoder.decode(response, type);
      }
    }
    throw new DecodeException(
        response.status(), unableToDecode(response, type), response.request());
  }

  private String unableToDecode(Response response, Type type) {
    StringBuilder message =
        new StringBuilder("Unable to decode ")
            .append(response.status())
            .append(" response (")
            .append(headers(response))
            .append(") as ")
            .append(type == null ? "the expected type" : type.getTypeName())
            .append(". Decoders tried, in order:");
    appendTo(message, "\n  ");
    return message
        .append("\nRegister a decoder that accepts it, or add a catch-all")
        .append(" (DecoderPredicate.any()) last.")
        .toString();
  }

View on GitHub (pinned to e2a1e27560)