Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
73 commits
Select commit Hold shift + click to select a range
634dfbf
build(deps): Bump com.gradle:common-custom-user-data-maven-extension
dependabot[bot] Aug 4, 2026
98169f5
Merge pull request #3504 from OpenFeign/dependabot/maven/com.gradle-c…
github-actions[bot] Aug 4, 2026
088c5e0
build(deps): Bump com.alibaba.fastjson2:fastjson2
dependabot[bot] Aug 6, 2026
661b398
Merge pull request #3506 from OpenFeign/dependabot/maven/com.alibaba.…
github-actions[bot] Aug 6, 2026
f8f1f92
Avoid default Content-Type on POST/PUT/PATCH with empty body
seonwooj0810 Aug 9, 2026
44f688e
Flatten feign-bom so imports do not override consumer dependency mana…
kalayciburak Aug 9, 2026
17f9732
build(deps): Bump org.apache.httpcomponents.client5:httpclient5
dependabot[bot] Aug 10, 2026
675eddc
build(deps): Bump netty.version from 4.2.16.Final to 4.2.17.Final
dependabot[bot] Aug 10, 2026
5271968
build(deps-dev): Bump vertx.version in /vertx/feign-vertx5-test
dependabot[bot] Aug 10, 2026
c17aa52
Merge pull request #3510 from OpenFeign/dependabot/maven/netty.versio…
github-actions[bot] Aug 10, 2026
cb5eb3b
Merge pull request #3511 from OpenFeign/dependabot/maven/vertx/feign-…
github-actions[bot] Aug 10, 2026
4226185
Merge pull request #3509 from OpenFeign/dependabot/maven/org.apache.h…
github-actions[bot] Aug 10, 2026
0852535
Add GraphQL subscription support over graphql-transport-ws
velo Aug 10, 2026
afc0764
Bound GraphQL subscription reads, threads and send failures
velo Aug 10, 2026
7e07edb
Add concurrency test for GraphQL subscriptions
velo Aug 10, 2026
603e8cc
build(deps): Bump org.junit:junit-bom from 6.1.2 to 6.1.3
dependabot[bot] Aug 11, 2026
3846112
build(deps-dev): Bump vertx.version in /vertx/feign-vertx4-test
dependabot[bot] Aug 11, 2026
a23bec7
Merge pull request #3514 from OpenFeign/dependabot/maven/vertx/feign-…
github-actions[bot] Aug 11, 2026
27e0152
Merge pull request #3513 from OpenFeign/dependabot/maven/org.junit-ju…
github-actions[bot] Aug 11, 2026
3577c00
Fix subscription worker starvation under a small thread pool
velo Aug 11, 2026
77f9f64
Merge pull request #3512 from OpenFeign/graphql-subscriptions
velo Aug 11, 2026
4fb8e61
Merge pull request #3507 from seonwooj0810/fix/issue-2068-empty-body-…
velo Aug 11, 2026
80a8ac7
Use a version property for flatten-maven-plugin and keep sortpom on t…
velo Aug 11, 2026
250038b
Merge pull request #3508 from kalayciburak/fix/feign-bom-flatten-import
velo Aug 11, 2026
60e75ad
build(deps-dev): Bump org.codehaus.mojo:flatten-maven-plugin
dependabot[bot] Aug 12, 2026
ca61da9
Merge pull request #3515 from OpenFeign/dependabot/maven/org.codehaus…
github-actions[bot] Aug 12, 2026
75b7fd3
Skip synthetic and bridge methods in contract parsing
AzazelSensei Aug 12, 2026
e0fd29f
Map bridge methods to bridged handlers in ReflectiveFeign
AzazelSensei Aug 12, 2026
846f378
Format BridgeMethodTest for git-code-format
AzazelSensei Aug 12, 2026
fcac4e7
build(deps): Bump org.openrewrite.recipe:rewrite-testing-frameworks
dependabot[bot] Aug 13, 2026
7d3813f
Merge pull request #3518 from OpenFeign/dependabot/maven/org.openrewr…
github-actions[bot] Aug 13, 2026
982fc33
match Content-Length header case-insensitively in DefaultClient
alhudz Aug 13, 2026
20ef5f6
Apply JAXB factory properties when creating unmarshallers
arimu1 Aug 16, 2026
a35e41b
build(deps-dev): Bump org.openrewrite.maven:rewrite-maven-plugin
dependabot[bot] Aug 17, 2026
67df9e3
build(deps): Bump org.openrewrite.recipe:rewrite-migrate-java
dependabot[bot] Aug 17, 2026
71b7dc7
Merge pull request #3520 from OpenFeign/dependabot/maven/org.openrewr…
github-actions[bot] Aug 17, 2026
84941c7
Merge pull request #3521 from OpenFeign/dependabot/maven/org.openrewr…
github-actions[bot] Aug 17, 2026
a090a5a
build(deps): Bump org.json:json from 20260719 to 20260814
dependabot[bot] Aug 18, 2026
5d532e9
build(deps): Bump tools.jackson:jackson-bom from 3.2.1 to 3.2.2
dependabot[bot] Aug 18, 2026
e90bdd5
Merge pull request #3522 from OpenFeign/dependabot/maven/org.json-jso…
github-actions[bot] Aug 18, 2026
a06b1a0
Merge pull request #3523 from OpenFeign/dependabot/maven/tools.jackso…
github-actions[bot] Aug 18, 2026
e114354
build(deps): Bump com.google.guava:guava from 33.6.0-jre to 33.7.1-jre
dependabot[bot] Aug 19, 2026
182624e
Merge pull request #3524 from OpenFeign/dependabot/maven/com.google.g…
github-actions[bot] Aug 19, 2026
51d1692
Decode every root field of a GraphQL operation
velo Aug 19, 2026
02b8549
Merge pull request #3526 from OpenFeign/fix/3525-multi-root-fields
velo Aug 19, 2026
fcb4a97
Merge pull request #3451 from alhudz/content-length-duplicate
velo Aug 19, 2026
a2676b8
Merge pull request #3519 from arimu1/fix/3056-jaxb-unmarshaller-prope…
velo Aug 19, 2026
f9ab405
Merge pull request #3517 from AzazelSensei/fix/skip-synthetic-bridge-…
velo Aug 19, 2026
a291578
Add Util helpers for detecting JSON and XML content types
yvasyliev Aug 19, 2026
a8f11c2
Add PredicatedEncoder and EncoderPredicate for conditional encoding
trumpetinc Aug 19, 2026
df9ae65
Add MultiEncoder to select an encoder per request
yvasyliev Aug 19, 2026
0ece284
Expose and document multi-encoder configuration
velo Aug 19, 2026
78eab43
Mark the multi-encoder API as experimental
velo Aug 19, 2026
fecafdb
Rework multi-encoder around encoders that declare their own canEncode
velo Aug 19, 2026
d2fa88a
Add Util helpers for detecting JSON and XML response content types
velo Aug 19, 2026
42b1573
Add PredicatedDecoder and DecoderPredicate for conditional decoding
velo Aug 19, 2026
4411979
Add MultiDecoder to select a decoder per response
velo Aug 19, 2026
02078d3
Expose and document multi-decoder configuration
velo Aug 19, 2026
93d52d6
Declare applicability on the first-party decoders
velo Aug 19, 2026
372ebee
prepare release 13.14
velo Aug 19, 2026
f6a02de
[ci skip] updating versions to next development iteration 13.15-SNAPSHOT
velo Aug 19, 2026
3426cb3
build(deps): Bump jackson.version from 2.22.1 to 2.22.2
dependabot[bot] Aug 20, 2026
9433cab
build(deps): Bump com.squareup.okhttp3:okhttp-bom from 5.4.0 to 5.5.0
dependabot[bot] Aug 20, 2026
7ac6f3e
Merge pull request #3529 from OpenFeign/dependabot/maven/jackson.vers…
github-actions[bot] Aug 20, 2026
30170cc
Merge pull request #3530 from OpenFeign/dependabot/maven/com.squareup…
github-actions[bot] Aug 20, 2026
e012d67
Drop the multi-encoder default encoder in favour of an explicit any()…
velo Aug 20, 2026
cfdd193
Drop the multi-decoder default decoder in favour of an explicit any()…
velo Aug 20, 2026
cd77b42
Generate the flattened feign-bom POM under target instead of the sour…
velo Aug 20, 2026
582aaf9
Merge pull request #3527 from OpenFeign/feat/multi-encoder
velo Aug 20, 2026
9fdec74
Merge branch 'master' into feat/multi-decoder
velo Aug 20, 2026
4ec2608
Merge pull request #3531 from OpenFeign/fix/flatten-pom-in-target
velo Aug 20, 2026
4b7c1eb
Merge pull request #3528 from OpenFeign/feat/multi-decoder
velo Aug 20, 2026
a9cc0a1
Merge branch 'master' into 14.x
velo Aug 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .mvn/extensions.xml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,6 @@
<extension>
<groupId>com.gradle</groupId>
<artifactId>common-custom-user-data-maven-extension</artifactId>
<version>2.3.0</version>
<version>2.4.0</version>
</extension>
</extensions>
25 changes: 25 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,34 @@

### Version 13.14

* Add `@Experimental` `MultiEncoder`, `PredicatedEncoder` and `EncoderPredicate`, letting a single
client route each request to the right encoder. Encoders declare what they can handle by
implementing `PredicatedEncoder`; anything else is paired with a predicate via
`PredicatedEncoder.of(predicate, encoder)` or `MultiEncoder.builder()`. Encoders are consulted in
the order given and a request nothing accepts fails with an `EncodeException` naming what was
tried, so a default is an encoder guarded by `EncoderPredicate.any()` listed last. `FormEncoder`
and `SpringFormEncoder` gain `createPredicatedFormEncoder()`, a delegate-free flavour that can
take part. The first-party JSON encoders (Gson, Jackson, Jackson 3, Jackson Jr, Jackson JAXB,
Moshi, Fastjson2, JSON-java) and XML encoders (JAXB, JAXB Jakarta, SOAP, SOAP Jakarta) now declare
themselves, and the metrics modules' `MeteredEncoder` forwards `canEncode` to the encoder it
wraps. The `Encoder` interface is unchanged, so existing encoders keep working (#3485).
* Add `@Experimental` `MultiDecoder`, `PredicatedDecoder` and `DecoderPredicate`, the decode-side
counterpart, letting a single client route each response to the right decoder. Decoders declare
what they can handle by implementing `PredicatedDecoder`; anything else is paired with a predicate
via `PredicatedDecoder.of(predicate, decoder)` or `MultiDecoder.builder()`. Decoders are consulted
in the order given and a response nothing accepts fails with a `DecodeException` naming what was
tried, so a default is a decoder guarded by `DecoderPredicate.any()` listed last. The first-party
JSON decoders (Gson, Jackson, Jackson 3, Jackson Jr, Jackson JAXB, Moshi, Fastjson2, JSON-java)
and XML decoders (JAXB, JAXB Jakarta, SAX, SOAP, SOAP Jakarta) now declare themselves, and
`OptionalDecoder` and the metrics modules' `MeteredDecoder` forward `canDecode` to the decoder
they wrap. The `Decoder` interface is unchanged, so existing decoders keep working.
* `JAXBContextFactory.withProperty` is now applied when creating Unmarshallers, not only
Marshallers. Marshaller-only properties are skipped on unmarshal (#3056).
* Add support for the HTTP QUERY method (RFC 10008) — safe, idempotent, and cacheable with a
request body. `HttpCacheInterceptor` includes QUERY in its default cacheable set and
incorporates a body hash into the cache key to reduce cross-body collisions.
* Flatten `feign-bom` on install/deploy so importing the BOM does not pull `feign-parent`
dependency management (for example Jackson) into consumer projects such as Spring Boot.

### Version 13.12

Expand Down
235 changes: 235 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -663,6 +663,116 @@ public class Example {
}
```

#### Multiple decoders

> This API is `@Experimental` and may change incompatibly, or be removed, in a future release.

A single client sometimes has to read more than one format &mdash; JSON for most endpoints, XML for
a legacy one, plain text for a health check. `MultiDecoder` hands each response to the first decoder
that accepts it.

Most first-party decoders already declare what they can handle, so they can simply be listed, in the
order they should be consulted:

```java
interface MixedClient {
@RequestLine("GET /orders/{id}")
Order order(@Param("id") String id);

@RequestLine("GET /legacy/orders/{id}")
Order legacyOrder(@Param("id") String id);
}

public class Example {
public static void main(String[] args) {
MixedClient client = Feign.builder()
.decoders(new GsonDecoder(), new JAXBDecoder())
.target(MixedClient.class, "https://foo.com");
}
}
```

Routing is driven by what the server actually sent back, so a client that talks to endpoints
answering `application/json` and `application/xml` no longer needs one Feign instance per format.

There is no implicit fallback. A response that no decoder accepts fails with a `DecodeException`
naming the decoders that were tried and what each one wants:

```
Unable to decode 200 response (Content-Type: text/plain) as com.example.Order. Decoders tried, in order:
- GsonDecoder
- JAXBDecoder
Add a decoder guarded by DecoderPredicate.any() last to act as a default.
```

To get a default, pair a decoder with the predicate that accepts everything and list it **last**:

```java
Feign.builder()
.decoders(
new GsonDecoder(),
new JAXBDecoder(),
PredicatedDecoder.of(DecoderPredicate.any(), new DefaultDecoder()));
```

The same pairing works for any decoder that does not declare itself, including one you do not
control. `MultiDecoder.builder()` spells it out when a lambda reads better than a wrapper:

```java
Decoder decoder =
MultiDecoder.builder()
.add(new GsonDecoder()) // declares itself
.add(DecoderPredicate.xmlContentType(), someXmlDecoder) // paired
.add((response, type) -> type == byte[].class, binaryDecoder)
.add(DecoderPredicate.any(), new DefaultDecoder()) // the default, last
.build();
```

Decoders are consulted in the order they were added, so put the narrowest one first.

##### Declaring your own decoder

Implement `PredicatedDecoder` and say what you handle. `canDecode` has no default: a decoder that
declares nothing would claim every response, which is rarely what its author meant.

```java
public class MyDecoder implements PredicatedDecoder {

@Override
public boolean canDecode(Response response, Type type) {
return Util.isJsonContentType(response);
}

@Override
public Object decode(Response response, Type type) throws IOException {
// ...
}
}
```

`DecoderPredicate` is the `@FunctionalInterface` here, so predicates can be lambdas. It ships with
`any()`, `jsonContentType()`, `xmlContentType()`, `contentType(mediaType)`, `emptyBody()`,
`status(codes...)` and `returnType(type)`, plus `and`/`or`/`negate` to combine them. Each one
describes itself, which is what shows up in the error message above; wrap your own lambdas in
`DecoderPredicate.describedAs("it is Tuesday", ...)` to read as well.

`PredicatedDecoder.of(predicate, decoder)` replaces whatever the decoder says about itself, so it
can widen a decoder as well as narrow it. To keep the decoder's own declaration and add to it, use
`narrowing`:

```java
// JSON responses as usual, but only when the call actually succeeded
PredicatedDecoder.narrowing(DecoderPredicate.status(200, 201), new GsonDecoder());
```

**Predicates must not read the response body.** For most clients it is a single-pass stream, so
consuming it in `canDecode` would leave nothing for the decoder that is eventually chosen. Decide
on the status, the headers and the expected type instead.

**If you wrap a decoder, forward `canDecode` to your delegate**, otherwise wrapping silently changes
what the decoder handles. `OptionalDecoder` and the metrics modules' `MeteredDecoder` forward for
exactly this reason.

### Encoders
The simplest way to send a request body to a server is to define a `POST` method that has a `String` or `byte[]` parameter without any annotations on it. You will likely need to add a `Content-Type` header.

Expand Down Expand Up @@ -709,6 +819,131 @@ public class Example {
}
```

#### Multiple encoders

> This API is `@Experimental` and may change incompatibly, or be removed, in a future release.

A single client sometimes has to speak more than one format &mdash; JSON for most endpoints, XML for
a legacy one, plain bytes for an upload. `MultiEncoder` hands each request to the first encoder that
accepts it.

Most first-party encoders already declare what they can handle, so they can simply be listed, in the
order they should be consulted:

```java
interface MixedClient {
@RequestLine("POST /orders")
@Headers("Content-Type: application/json")
void createOrder(Order order);

@RequestLine("POST /legacy/orders")
@Headers("Content-Type: application/xml")
void createLegacyOrder(Order order);
}

public class Example {
public static void main(String[] args) {
MixedClient client = Feign.builder()
.encoders(new GsonEncoder(), new JAXBEncoder())
.target(MixedClient.class, "https://foo.com");
}
}
```

There is no implicit fallback. A request that no encoder accepts fails with an `EncodeException`
naming the encoders that were tried and what each one wants:

```
Unable to encode java.lang.String (Content-Type: text/plain) for POST /orders. Encoders tried, in order:
- GsonEncoder
- JAXBEncoder
Add an encoder guarded by EncoderPredicate.any() last to act as a default.
```

To get a default, pair an encoder with the predicate that accepts everything and list it **last**:

```java
Feign.builder()
.encoders(
new GsonEncoder(),
new JAXBEncoder(),
PredicatedEncoder.of(EncoderPredicate.any(), new DefaultEncoder()));
```

The same pairing works for any encoder that does not declare itself, including one you do not
control. `MultiEncoder.builder()` spells it out when a lambda reads better than a wrapper:

```java
Encoder encoder =
MultiEncoder.builder()
.add(new GsonEncoder()) // declares itself
.add(EncoderPredicate.xmlContentType(), someXmlEncoder) // paired
.add((object, bodyType, template) -> bodyType == byte[].class, binaryEncoder)
.add(EncoderPredicate.any(), new DefaultEncoder()) // the default, last
.build();
```

Encoders are consulted in the order they were added, so put the narrowest one first. Note that
`Content-Type: application/json` with a null body is claimed by a JSON encoder before
`EncoderPredicate.emptyBody()` gets a chance &mdash; order accordingly.

##### Declaring your own encoder

Implement `PredicatedEncoder` and say what you handle. `canEncode` has no default: an encoder that
declares nothing would claim every request, which is rarely what its author meant.

```java
public class MyEncoder implements PredicatedEncoder {

@Override
public boolean canEncode(Object object, Type bodyType, RequestTemplate template) {
return Util.isJsonContentType(template);
}

@Override
public void encode(Object object, Type bodyType, RequestTemplate template) {
// ...
}
}
```

`EncoderPredicate` is the `@FunctionalInterface` here, so predicates can be lambdas. It ships with
`any()`, `jsonContentType()`, `xmlContentType()`, `contentType(mediaType)`, `emptyBody()`,
`bodyType(type)` and `formEncoded()`, plus `and`/`or`/`negate` to combine them. Each one describes
itself, which is what shows up in the error message above; wrap your own lambdas in
`EncoderPredicate.describedAs("it is Tuesday", ...)` to read as well.

`PredicatedEncoder.of(predicate, encoder)` replaces whatever the encoder says about itself, so it
can widen an encoder as well as narrow it. To keep the encoder's own declaration and add to it, use
`narrowing`:

```java
// only this vendor content type, and only what Gson would have taken anyway
PredicatedEncoder.narrowing(
EncoderPredicate.contentType("application/vnd.acme+json"), new GsonEncoder());
```

**If you wrap an encoder, forward `canEncode` to your delegate**, otherwise wrapping silently
changes what the encoder handles. The metrics modules' `MeteredEncoder` forwards for exactly this
reason.

##### Form encoders

`FormEncoder` and `SpringFormEncoder` wrap a delegate encoder, so they cannot honestly declare what
they handle &mdash; the delegate's applicability is unknown to them. Instead, each offers a
delegate-free flavour that does:

```java
Feign.builder()
.encoders(
FormEncoder.createPredicatedFormEncoder(), // form and multipart requests only
new JacksonEncoder());
```

It accepts form and multipart requests carrying a map or a user pojo, and leaves everything else to
the encoders registered alongside it. Constructing one directly with a `null` delegate does the same
thing: anything it cannot encode itself fails with an `EncodeException` instead of being passed on.

### @Body templates
The `@Body` annotation indicates a template to expand using parameters annotated with `@Param`. You will likely need to add a `Content-Type` header.

Expand Down
68 changes: 68 additions & 0 deletions api/src/main/java/feign/BaseBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,14 @@
import feign.Request.Options;
import feign.codec.Codec;
import feign.codec.Decoder;
import feign.codec.DecoderPredicate;
import feign.codec.Encoder;
import feign.codec.EncoderPredicate;
import feign.codec.ErrorDecoder;
import feign.codec.MultiDecoder;
import feign.codec.MultiEncoder;
import feign.codec.PredicatedDecoder;
import feign.codec.PredicatedEncoder;
import feign.interceptor.MethodInterceptor;
import feign.interceptor.MethodInterceptors;
import java.lang.reflect.Field;
Expand Down Expand Up @@ -98,11 +104,73 @@ public B encoder(Encoder encoder) {
return thisB();
}

/**
* Configures a {@link MultiEncoder} built from encoders that declare their own applicability.
*
* <p>Encoders are consulted in the order given, and the first one that accepts the request
* encodes it. There is no implicit fallback: pair an encoder with {@link EncoderPredicate#any()}
* and list it last to act as a default, otherwise a request nothing accepts fails with an {@link
* feign.codec.EncodeException}.
*
* <pre>
* Feign.builder()
* .encoders(
* new JacksonEncoder(),
* new JAXBEncoder(),
* PredicatedEncoder.of(EncoderPredicate.any(), new DefaultEncoder()))
* </pre>
*
* <p>To pair a predicate with an encoder that does not implement {@link PredicatedEncoder}, use
* {@link PredicatedEncoder#of(EncoderPredicate, Encoder)} as above, or {@link
* MultiEncoder#builder()} for the same thing spelled out.
*
* @param encoders the predicated encoders, consulted in the order given
*/
@Experimental
public B encoders(PredicatedEncoder... encoders) {
MultiEncoder.Builder builder = MultiEncoder.builder();
for (PredicatedEncoder encoder : encoders) {
builder.add(encoder);
}
return encoder(builder.build());
}

public B decoder(Decoder decoder) {
this.decoder = decoder;
return thisB();
}

/**
* Configures a {@link MultiDecoder} built from decoders that declare their own applicability.
*
* <p>Decoders are consulted in the order given, and the first one that accepts the response
* decodes it. There is no implicit fallback: pair a decoder with {@link DecoderPredicate#any()}
* and list it last to act as a default, otherwise a response nothing accepts fails with a {@link
* feign.codec.DecodeException}.
*
* <pre>
* Feign.builder()
* .decoders(
* new JacksonDecoder(),
* new JAXBDecoder(),
* PredicatedDecoder.of(DecoderPredicate.any(), new DefaultDecoder()))
* </pre>
*
* <p>To pair a predicate with a decoder that does not implement {@link PredicatedDecoder}, use
* {@link PredicatedDecoder#of(DecoderPredicate, Decoder)} as above, or {@link
* MultiDecoder#builder()} for the same thing spelled out.
*
* @param decoders the predicated decoders, consulted in the order given
*/
@Experimental
public B decoders(PredicatedDecoder... decoders) {
MultiDecoder.Builder builder = MultiDecoder.builder();
for (PredicatedDecoder decoder : decoders) {
builder.add(decoder);
}
return decoder(builder.build());
}

public B codec(Codec codec) {
this.encoder = codec.encoder();
this.decoder = codec.decoder();
Expand Down
4 changes: 3 additions & 1 deletion api/src/main/java/feign/Contract.java
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,9 @@ public List<MethodMetadata> parseAndValidateMetadata(Class<?> targetType) {
if (method.getDeclaringClass() == Object.class
|| (method.getModifiers() & Modifier.STATIC) != 0
|| Util.isDefault(method)
|| method.isAnnotationPresent(FeignIgnore.class)) {
|| method.isAnnotationPresent(FeignIgnore.class)
|| method.isSynthetic()
|| method.isBridge()) {
continue;
}
final MethodMetadata metadata = parseAndValidateMetadata(targetType, method);
Expand Down
Loading