Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
84 commits
Select commit Hold shift + click to select a range
cf28371
build(deps-dev): Bump io.undertow:undertow-core
dependabot[bot] Jun 26, 2026
7a5d815
Merge pull request #3452 from OpenFeign/dependabot/maven/io.undertow-…
github-actions[bot] Jun 26, 2026
46d604f
build(deps): Bump org.junit:junit-bom from 6.1.0 to 6.1.1
dependabot[bot] Jun 29, 2026
6bda942
Merge pull request #3455 from OpenFeign/dependabot/maven/org.junit-ju…
github-actions[bot] Jun 29, 2026
68815c8
build(deps): Bump org.apache.httpcomponents.client5:httpclient5
dependabot[bot] Jun 30, 2026
e2ce674
build(deps): Bump com.gradle:common-custom-user-data-maven-extension
dependabot[bot] Jun 30, 2026
0181b69
build(deps): Bump com.google.http-client:google-http-client
dependabot[bot] Jun 30, 2026
9d573b4
Merge pull request #3457 from OpenFeign/dependabot/maven/com.gradle-c…
github-actions[bot] Jun 30, 2026
0fd7247
Merge pull request #3456 from OpenFeign/dependabot/maven/org.apache.h…
github-actions[bot] Jun 30, 2026
2ea2dec
Merge pull request #3458 from OpenFeign/dependabot/maven/com.google.h…
github-actions[bot] Jun 30, 2026
b6474fa
build(deps): Bump com.github.jknack:handlebars from 4.5.2 to 4.5.3
dependabot[bot] Jul 1, 2026
7446c1f
build(deps-dev): Bump com.gradle:develocity-maven-extension
dependabot[bot] Jul 1, 2026
d9da80c
Merge pull request #3461 from OpenFeign/dependabot/maven/com.gradle-d…
github-actions[bot] Jul 1, 2026
d7ea97b
Merge pull request #3460 from OpenFeign/dependabot/maven/com.github.j…
github-actions[bot] Jul 1, 2026
0056795
build(deps): Bump org.openrewrite.recipe:rewrite-migrate-java
dependabot[bot] Jul 2, 2026
f10e817
build(deps-dev): Bump org.openrewrite.maven:rewrite-maven-plugin
dependabot[bot] Jul 2, 2026
a52a378
Merge pull request #3465 from OpenFeign/dependabot/maven/org.openrewr…
github-actions[bot] Jul 2, 2026
5b81580
Merge pull request #3467 from OpenFeign/dependabot/maven/org.openrewr…
github-actions[bot] Jul 2, 2026
935954a
build(deps): Bump org.openrewrite.recipe:rewrite-testing-frameworks
dependabot[bot] Jul 2, 2026
8896c30
Merge pull request #3466 from OpenFeign/dependabot/maven/org.openrewr…
github-actions[bot] Jul 2, 2026
a527c35
build(deps): Bump netty.version from 4.2.15.Final to 4.2.16.Final
dependabot[bot] Jul 7, 2026
7510344
Merge pull request #3470 from OpenFeign/dependabot/maven/netty.versio…
github-actions[bot] Jul 7, 2026
6b0a283
build(deps): Bump jackson.version from 2.22.0 to 2.22.1
dependabot[bot] Jul 8, 2026
5f749db
build(deps-dev): Bump vertx.version in /vertx/feign-vertx5-test
dependabot[bot] Jul 8, 2026
2a8209a
Merge pull request #3472 from OpenFeign/dependabot/maven/vertx/feign-…
github-actions[bot] Jul 8, 2026
14e1842
Merge pull request #3471 from OpenFeign/dependabot/maven/jackson.vers…
github-actions[bot] Jul 8, 2026
cbb53a6
build(deps-dev): Bump vertx.version in /vertx/feign-vertx4-test
dependabot[bot] Jul 10, 2026
9e2aad2
Merge pull request #3473 from OpenFeign/dependabot/maven/vertx/feign-…
github-actions[bot] Jul 10, 2026
7965183
build(deps): Bump tools.jackson:jackson-bom from 3.2.0 to 3.2.1
dependabot[bot] Jul 13, 2026
710dab7
build(deps): Bump org.junit:junit-bom from 6.1.1 to 6.1.2
dependabot[bot] Jul 13, 2026
9c51c3b
Merge pull request #3474 from OpenFeign/dependabot/maven/tools.jackso…
github-actions[bot] Jul 13, 2026
33bf63b
Merge pull request #3475 from OpenFeign/dependabot/maven/org.junit-ju…
github-actions[bot] Jul 13, 2026
2f548d5
feat: allow registering multiple encoders in Feign
yvasyliev Jul 13, 2026
cd9b364
build(deps-dev): Bump vertx.version in /vertx/feign-vertx4-test
dependabot[bot] Jul 14, 2026
a3403f0
build(deps-dev): Bump vertx.version in /vertx/feign-vertx5-test
dependabot[bot] Jul 14, 2026
0629332
Merge pull request #3478 from OpenFeign/dependabot/maven/vertx/feign-…
github-actions[bot] Jul 14, 2026
ed41e7a
Merge pull request #3477 from OpenFeign/dependabot/maven/vertx/feign-…
github-actions[bot] Jul 14, 2026
e9b0f35
build(deps): Bump kotlin.version from 2.4.0 to 2.4.10
dependabot[bot] Jul 15, 2026
855c1f6
build(deps-dev): Bump org.openrewrite.maven:rewrite-maven-plugin
dependabot[bot] Jul 15, 2026
b5ab18f
build(deps): Bump org.openrewrite.recipe:rewrite-migrate-java
dependabot[bot] Jul 15, 2026
26a3c4a
Merge pull request #3479 from OpenFeign/dependabot/maven/kotlin.versi…
github-actions[bot] Jul 15, 2026
9009483
Merge pull request #3480 from OpenFeign/dependabot/maven/org.openrewr…
github-actions[bot] Jul 15, 2026
5370c8e
Merge pull request #3481 from OpenFeign/dependabot/maven/org.openrewr…
github-actions[bot] Jul 15, 2026
e461dc4
build(deps): Bump org.openrewrite.recipe:rewrite-testing-frameworks
dependabot[bot] Jul 15, 2026
661d121
Merge pull request #3482 from OpenFeign/dependabot/maven/org.openrewr…
github-actions[bot] Jul 15, 2026
df2c15b
feat: make `feign.json.JsonEncoder` implement `feign.codec.JsonEncoder`
yvasyliev Jul 15, 2026
f130cce
docs: document new multi-encoder features
yvasyliev Jul 15, 2026
10c9265
docs: fix typos in docs
yvasyliev Jul 15, 2026
a696a4a
feat: make `Encode#encode` method return boolean
yvasyliev Jul 17, 2026
4621dd7
docs: fix typos and add clarity to the docs
yvasyliev Jul 19, 2026
7451e9d
build(deps): Bump org.json:json from 20260522 to 20260719
dependabot[bot] Jul 20, 2026
c39b706
Merge pull request #3484 from OpenFeign/dependabot/maven/org.json-jso…
github-actions[bot] Jul 20, 2026
2db791b
Merge remote-tracking branch 'yvasyliev/feature/multi-encoder' into m…
trumpetinc Jul 20, 2026
c5c19f2
Remove json content-type checks
trumpetinc Jul 20, 2026
4d7b063
Remove XML content-type checks
trumpetinc Jul 20, 2026
0a767ad
Remove migration note about json and xml content-type headers
trumpetinc Jul 20, 2026
0386c8d
Merge branch 'master' into multi-encoder
trumpetinc Jul 20, 2026
fb503f7
Updating PR hyperlink
trumpetinc Jul 20, 2026
4ed95e6
Put main.java.version back to 11 (sorry - shouldn't have changed it)
trumpetinc Jul 22, 2026
db95df3
Clean up unused imports
trumpetinc Jul 22, 2026
3a93590
Merge remote-tracking branch 'origin/multiloggertest_fix_windows_file…
trumpetinc Jul 27, 2026
6e91211
trying to clean up code formatting for git-code-format-maven-plugin
trumpetinc Jul 27, 2026
ef33b17
Merge remote-tracking branch 'origin/multiloggertest_fix_windows_file…
trumpetinc Jul 27, 2026
5b11c91
Rename DelegateEncoder to MultiEncoder
trumpetinc Jul 27, 2026
36761fc
MultiEncoder can't be referenced from interfaces in api module
trumpetinc Jul 27, 2026
ffbf4c3
MultiEncoder cleanup
trumpetinc Jul 27, 2026
d00312c
Add PredicateEncoder and supporting classes
trumpetinc Jul 27, 2026
ea970f6
Remove unnused import
trumpetinc Jul 27, 2026
7af1863
Formatting
trumpetinc Jul 27, 2026
d00ad86
Formatting
trumpetinc Jul 27, 2026
095acdb
Add license
trumpetinc Jul 27, 2026
b484275
Remove lombok from core
trumpetinc Jul 27, 2026
6710ffc
Fix formatting
trumpetinc Jul 27, 2026
5922082
Update changelog for changes in MultiEncoder, link to new pull request
trumpetinc Jul 27, 2026
f6e3743
Merge remote-tracking branch 'Feign/14.x' into multi-encoder
trumpetinc Jul 28, 2026
5440c15
Remove library version changes
trumpetinc Jul 28, 2026
c830176
private constructor
trumpetinc Jul 28, 2026
b3e884d
Add javadoc
trumpetinc Jul 28, 2026
a6f05c4
Use Mockito more
trumpetinc Jul 28, 2026
da348f5
Make class public
trumpetinc Jul 28, 2026
49b7d91
Fix toString
trumpetinc Jul 29, 2026
ee828e7
Swap constructor argument order (predicte first, then delegate)
trumpetinc Jul 29, 2026
9106c0b
Clean up migration document
trumpetinc Jul 29, 2026
983a244
Fix formatting
trumpetinc Jul 29, 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
4 changes: 2 additions & 2 deletions .mvn/extensions.xml
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@
<extension>
<groupId>com.gradle</groupId>
<artifactId>develocity-maven-extension</artifactId>
<version>2.4.2</version>
<version>2.5.0</version>
</extension>
<extension>
<groupId>com.gradle</groupId>
<artifactId>common-custom-user-data-maven-extension</artifactId>
<version>2.2.0</version>
<version>2.3.0</version>
</extension>
</extensions>
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
* `DefaultEncoder` now supports streaming request bodies for `File`, `Path`, `InputStream`, and `Request.Body` types,
avoiding in-memory buffering. New `Request.PathBody` and `Request.InputStreamBody` implementations are provided for
these cases. (https://github.com/OpenFeign/feign/pull/3396)
* `Encoder.encode()` now returns `boolean` — return `true` when encoding succeeds, `false` if
the encoder does not handle the type. The `MultiEncoder.of()` factory method composes multiple encoders
into a `MultiEncoder` that tries each delegate's `encode()` in order. Existing custom `Encoder` implementations
must update the `encode()` return type from `void` to `boolean` and throw EncodeException for true encoding failures.
(https://github.com/OpenFeign/feign/pull/3485)

### Version 13.12

Expand Down
118 changes: 118 additions & 0 deletions MIGRATION-v14.md
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,104 @@ VertxFeign.builder()

---

### 14. `Encoder.encode()` now returns `boolean` (https://github.com/OpenFeign/feign/pull/3485)

`Encoder.encode()` now returns `boolean` instead of `void`. Return `true` when the encoder
handles the object, `false` otherwise. This replaces the separate `canEncode()` method.

**Before:**

```java
public class MyEncoder implements Encoder {
@Override
public void encode(Object object, Type bodyType, RequestTemplate template) {
template.body(Request.Body.of(serialize(object)));
}
}
```

**After:**

```java
public class MyEncoder implements Encoder {
@Override
public boolean encode(Object object, Type bodyType, RequestTemplate template) {
template.body(Request.Body.of(serialize(object)));
return true; // or return false if the encoder does not handle this type
}
}
```

Built-in encoders (`DefaultEncoder`, `FormEncoder`, `MeteredEncoder`, `GraphqlEncoder`, etc.) already return `boolean`
from `encode()`. If your encoder returns `false`, the `MultiEncoder` (see section 19) will try the next encoder. If
no encoder returns `true`, an `EncodeException` is thrown.

---

### 15. `Encoder` moved from `core` to `api` module

`feign.codec.Encoder` has been relocated from the `feign-core` module to the new `feign-api`
module. The package name (`feign.codec`) is unchanged. If you have a direct dependency on
`feign-core` without `feign-api`, you need to add `feign-api` to your classpath.

---

### 16. `Encoder.Default` removed

The deprecated inner class `Encoder.Default` (which extended `DefaultEncoder`) has been removed.

**Before:**

```java
new Encoder.Default()
```

**After:**

```java
new feign.core.codec.DefaultEncoder()
```

---

### 17. Composing multiple encoders with `MultiEncoder.of()` (https://github.com/OpenFeign/feign/pull/3485)

Use `MultiEncoder.of(...)` to compose multiple encoders into a single `MultiEncoder`, which
delegates to the first encoder whose `encode()` returns `true`.

**Before:**

```java
Feign.builder()
.encoder(new JacksonEncoder())
.target(MyApi.class, "https://api.example.com");
```

**After (multiple encoders):**

```java
Feign.builder()
.encoder(MultiEncoder.of(
new FormEncoder(),
new JacksonEncoder(),
new JAXBEncoder(factory)
))
.target(MyApi.class, "https://api.example.com");
```

**After (single encoder is unchanged):**

```java
Feign.builder()
.encoder(new JacksonEncoder())
.target(MyApi.class, "https://api.example.com");
```

The `MultiEncoder.of()` factory returns a `MultiEncoder`, which tries
each encoder's `encode()` and uses the first one that returns `true`.

---

## Implementing a Custom Streaming Body

If you want to stream a body (e.g., from a file or `InputStream`), implement `Request.Body` directly. Because
Expand Down Expand Up @@ -420,6 +518,26 @@ public class FileBody implements Request.Body {

---

## Selecting Encoder Based on a Predicate

TODO: Once we have specialized EncoderPredicate factory methods (i.e. for xml content types), update this example to use those factory methods

The new `PredicateEncoder` and `EncoderPredicate` classes can be used in conjunction with `MultiEncoder` to fine tune which Encoder
handles different types of encode requests.

```java
Feign.builder()
.encoder(MultiEncoder.of(
new DefaultEncoder(),
new PredicateEncoder((obj, type, templ) -> templ.headers().get("Content-Type").contains("application/xml"), new JAXBEncoder(factory), // handle xml requests
new JacksonEncoder() // handle everything else
))
.target(MyApi.class, "https://api.example.com");

```

---

## Spring Cloud OpenFeign Compatibility

`RequestTemplate#body(byte[], Charset)` is kept `@Deprecated` for backward compatibility with
Expand Down
2 changes: 1 addition & 1 deletion api/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@

<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>${mockito.version}</version>
<scope>test</scope>
</dependency>
Expand Down
22 changes: 19 additions & 3 deletions api/src/main/java/feign/RequestTemplateFactoryResolver.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import feign.codec.EncodeException;
import feign.codec.Encoder;
import feign.template.UriUtils;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
Expand Down Expand Up @@ -232,7 +233,9 @@ protected RequestTemplate resolve(
}
}
try {
encoder.encode(formVariables, Encoder.MAP_STRING_WILDCARD, mutable);
if (!encoder.encode(formVariables, Encoder.MAP_STRING_WILDCARD, mutable)) {
throw new EncodeException("This encoder does not support form encoding: " + encoder);
}
} catch (EncodeException e) {
throw e;
} catch (RuntimeException e) {
Expand Down Expand Up @@ -269,9 +272,9 @@ protected RequestTemplate resolve(
try {
if (alwaysEncodeBody) {
body = argv == null ? new Object[0] : argv;
encoder.encode(body, Object[].class, mutable);
encode(body, Object[].class, mutable);
} else {
encoder.encode(body, metadata.bodyType(), mutable);
encode(body, metadata.bodyType(), mutable);
}
} catch (EncodeException e) {
throw e;
Expand All @@ -280,5 +283,18 @@ protected RequestTemplate resolve(
}
return super.resolve(argv, mutable, variables);
}

private void encode(Object object, Type bodyType, RequestTemplate mutable)
throws EncodeException {
if (!encoder.encode(object, bodyType, mutable)) {
throw new EncodeException(
"This encoder does not support encoding of type: "
+ bodyType
+ " with object: "
+ object
+ ", encoder: "
+ encoder);
}
}
}
}
30 changes: 30 additions & 0 deletions api/src/main/java/feign/Util.java
Original file line number Diff line number Diff line change
Expand Up @@ -371,4 +371,34 @@ public static String getThreadIdentifier() {
+ "_"
+ currentThread.getId();
}

/**
* Checks if the request template has a content type header that is JSON.
*
* @param template the request template to check
* @return {@code true} if the content type is JSON, {@code false} otherwise
* @since 14
*/
public static boolean isJsonContentType(RequestTemplate template) {
return template.headers().getOrDefault("Content-Type", List.of()).stream()
.anyMatch(
contentType ->
contentType != null
&& contentType.trim().matches("(?i)\\w+/(?:[\\w._-]+\\+)?json.*"));
}

/**
* Checks if the request template has a content type header that is XML.
*
* @param template the request template to check
* @return {@code true} if the content type is XML, {@code false} otherwise
* @since 14
*/
public static boolean isXmlContentType(RequestTemplate template) {
return template.headers().getOrDefault("Content-Type", List.of()).stream()
.anyMatch(
contentType ->
contentType != null
&& contentType.trim().matches("(?i)\\w+/(?:[\\w._-]+\\+)?xml.*"));
}
}
6 changes: 4 additions & 2 deletions api/src/main/java/feign/codec/Encoder.java
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,9 @@ public interface Encoder {
* @param bodyType the type the object should be encoded as. {@link #MAP_STRING_WILDCARD}
* indicates form encoding.
* @param template the request template to populate.
* @throws EncodeException when encoding failed due to a checked exception.
* @return {@code true} if the encoder is able to handle the encode request, {@code false} if the
* encoder cannot encode the request
* @throws EncodeException if this encoder should be able to encode the request but encoding fails
*/
void encode(Object object, Type bodyType, RequestTemplate template) throws EncodeException;
boolean encode(Object object, Type bodyType, RequestTemplate template) throws EncodeException;
}
34 changes: 34 additions & 0 deletions api/src/main/java/feign/codec/EncoderPredicate.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright © 2012 The Feign Authors (feign@commonhaus.dev)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package feign.codec;

import feign.RequestTemplate;
import java.lang.reflect.Type;

/** A predicate that determines whether a given object can be encoded by an encoder. */
@FunctionalInterface
public interface EncoderPredicate {

/**
* Tests whether the given object can be encoded by an encoder.
*
* @param object the object to be encoded
* @param bodyType the type of the object to be encoded
* @param template the request template that will be used to encode the object
* @return {@code true} if the object can be encoded, {@code false} otherwise
*/
boolean test(Object object, Type bodyType, RequestTemplate template);
}
79 changes: 79 additions & 0 deletions api/src/test/java/feign/RequestTemplateFactoryResolverTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* Copyright © 2012 The Feign Authors (feign@commonhaus.dev)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package feign;

import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.params.provider.Arguments.arguments;
import static org.mockito.Mockito.mock;

import feign.codec.EncodeException;
import java.util.Map;
import java.util.function.Supplier;
import java.util.stream.Stream;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.FieldSource;

class RequestTemplateFactoryResolverTest {
@Nested
class BuildFormEncodedTemplateFromArgsTest {
@Test
void shouldThrowEncodeException() {
var methodMetadata = new MethodMetadata();
var variables = Map.<String, Object>of("data", "Hello, World!");
var factory =
new RequestTemplateFactoryResolver.BuildFormEncodedTemplateFromArgs(
methodMetadata, mock(), mock(), mock());

methodMetadata.formParams().add("data");

assertThrows(
EncodeException.class,
() -> factory.resolve(new Object[0], new RequestTemplate(), variables));
}
}

@Nested
class BuildEncodedTemplateFromArgsTest {
private static final Supplier<Stream<Arguments>> shouldThrowEncodeException =
() -> {
var methodMetadata1 = new MethodMetadata();
methodMetadata1.alwaysEncodeBody(true);

var methodMetadata2 = new MethodMetadata();
methodMetadata2.bodyIndex(0);

return Stream.of(
arguments(methodMetadata1, new Object[0]),
arguments(methodMetadata2, new Object[] {"Hello, World!"}));
};

@ParameterizedTest
@FieldSource
void shouldThrowEncodeException(MethodMetadata methodMetadata, Object[] argv) {
var factory =
new RequestTemplateFactoryResolver.BuildEncodedTemplateFromArgs(
methodMetadata, mock(), mock(), mock());
var mutable = new RequestTemplate();

mutable.methodMetadata(methodMetadata);

assertThrows(EncodeException.class, () -> factory.resolve(argv, mutable, Map.of()));
}
}
}
2 changes: 1 addition & 1 deletion benchmark/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
<jmh.version>1.37</jmh.version>
<rx.netty.version>0.5.3</rx.netty.version>
<rx.java.version>1.3.8</rx.java.version>
<netty.version>4.2.15.Final</netty.version>
<netty.version>4.2.16.Final</netty.version>
<moditect.skip>true</moditect.skip>
</properties>

Expand Down
Loading