Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
60 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
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>
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@
* `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 `Encoder.of()` factory method composes multiple encoders
into a `DelegatingEncoder` that tries each delegate's `encode()` in order. Built-in JSON encoders (Jackson, Gson,
Moshi, Fastjson2, Jackson-Jr) and XML encoders (JAXB, SOAP) now check the request's
`Content-Type` header via `Util.isJsonContentType()` / `Util.isXmlContentType()` and return
`false` when it does not match. Existing custom `Encoder` implementations
must update the `encode()` return type from `void` to `boolean`.
(https://github.com/OpenFeign/feign/pull/3476)

### Version 13.12

Expand Down
127 changes: 127 additions & 0 deletions MIGRATION-v14.md
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,133 @@ 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 `DelegatingEncoder` (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 `Encoder.of()` (https://github.com/OpenFeign/feign/pull/3485)

Use `Encoder.of(...)` to compose multiple encoders into a single `DelegatingEncoder`, 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(Encoder.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 `Encoder.of()` factory wraps the supplied encoders in a `DelegatingEncoder`, which tries
each encoder's `encode()` and uses the first one that returns `true`.

---

### 18. Multi-encoder support — `DelegatingEncoder` (https://github.com/OpenFeign/feign/pull/3485)

You can now compose multiple encoders via `Encoder.of()`, and Feign will pick the right one at
request time based on the return value of `encode()`. This is especially useful for APIs that mix
JSON, XML, and other content types:

**Before:**

```java
// Only one encoder — had to manually choose or wrap
Feign.builder()
.encoder(new FormEncoder(new JacksonEncoder()))
.target(MyApi.class, "https://api.example.com");
```

**After:**

```java
Feign.builder()
.encoder(Encoder.of(
new FormEncoder(), // handles multipart/form-urlencoded by Content-Type
new JacksonEncoder(), // implements JsonEncoder marker interface
new JAXBEncoder(factory) // handles XML
))
.target(MyApi.class, "https://api.example.com");
```

---

## 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
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.*"));
}
}
76 changes: 76 additions & 0 deletions api/src/main/java/feign/codec/DelegatingEncoder.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* 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;
import java.util.List;
import java.util.Objects;

/**
* An encoder that delegates to a list of encoders, using the first one that can encode the given
* object.
*
* @since 14
*/
class DelegatingEncoder implements Encoder {
private final List<Encoder> delegates;

/**
* Creates a new {@link DelegatingEncoder} with the given list of delegates.
*
* @param delegates the list of delegates to use for encoding. Both list and its elements must not
* be {@code null}.
*/
DelegatingEncoder(List<Encoder> delegates) {
this.delegates = Objects.requireNonNull(delegates, "delegates cannot be null");
}

/**
* Encodes the given object using the first delegate that can encode it. If no delegate can encode
* the object, an {@link EncodeException} is thrown.
*
* @param object {@inheritDoc}
* @param bodyType {@inheritDoc}
* @param template {@inheritDoc}
* @throws EncodeException {@inheritDoc}
*/
@Override
public boolean encode(Object object, Type bodyType, RequestTemplate template)
throws EncodeException {
return delegates.stream()
.map(encoder -> encoder.encode(object, bodyType, template))
.filter(Boolean::booleanValue)
.findFirst()
.orElseThrow(
() ->
new EncodeException(
"No suitable encoder found for object encoding: "
+ object
+ ", encoders: "
+ delegates));
}

/**
* {@inheritDoc}
*
* @return {@inheritDoc}
*/
@Override
public String toString() {
return "DelegatingEncoder{" + "delegates=" + delegates + '}';
}
}
32 changes: 30 additions & 2 deletions api/src/main/java/feign/codec/Encoder.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
import feign.RequestTemplate;
import feign.Util;
import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.List;

/**
* Encodes an object into an HTTP request body. Like {@code javax.websocket.Encoder}. {@code
Expand Down Expand Up @@ -70,14 +72,40 @@ public interface Encoder {
/** Type literal for {@code Map<String, ?>}, indicating the object to encode is a form. */
Type MAP_STRING_WILDCARD = Util.MAP_STRING_WILDCARD;

/**
* Creates a delegating encoder that will try each of the provided encoders in order until one
* returns {@code true} from {@link #encode(Object, Type, RequestTemplate)}.
*
* @param encoders the encoders to delegate to
* @return a delegating encoder
* @since 14
*/
static Encoder of(Encoder... encoders) {
return of(Arrays.asList(encoders));
}

/**
* Creates a delegating encoder that will try each of the provided encoders in order until one
* returns {@code true} from {@link #encode(Object, Type, RequestTemplate)}.
*
* @param encoders the encoders to delegate to
* @return a delegating encoder
* @since 14
*/
static Encoder of(List<Encoder> encoders) {
return new DelegatingEncoder(encoders);
}

/**
* Converts objects to an appropriate representation in the template.
*
* @param object what to encode as the request body.
* @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;
}
Loading