diff --git a/CHANGELOG.md b/CHANGELOG.md
index b6b745f14..ada9b2fd8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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
diff --git a/MIGRATION-v14.md b/MIGRATION-v14.md
index e91bfbe27..013a3dcdc 100644
--- a/MIGRATION-v14.md
+++ b/MIGRATION-v14.md
@@ -355,6 +355,172 @@ VertxFeign.builder()
---
+### 14. `Encoder.encode()` now returns `boolean` (https://github.com/OpenFeign/feign/pull/3476)
+
+`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. JSON and XML encoders require `Content-Type` header (https://github.com/OpenFeign/feign/pull/3476)
+
+Built-in JSON and XML encoders now gate on the `Content-Type` header and return
+`false` from `encode()` when it does not match:
+
+- JSON encoders (Jackson, Gson, Moshi, Fastjson2, Jackson-Jr, Jackson-Jaxb) check
+ `Util.isJsonContentType(template)` and return `false` when it does not match
+ (e.g., `application/json`, `application/ld+json`).
+- XML encoders (JAXB, SOAP) check `Util.isXmlContentType(template)` and return
+ `false` when it does not match (e.g., `text/xml`, `application/xml`).
+
+When using `Encoder.of()` (see section 18), a JSON or XML encoder will only claim
+the request if a matching `Content-Type` header is present.
+
+**You must ensure a `Content-Type` header is set** before the encoder runs. If
+your interface does not set the `Content-Type` header via annotations (e.g.,
+`@Headers("Content-Type: application/json")`), add it in one of the following ways:
+
+```java
+// 1. Add the header via @Headers on the interface or method:
+@Headers("Content-Type: application/json")
+public interface MyApi {
+ @RequestLine("POST /data")
+ void post(Data data);
+}
+
+// 2. Add the header globally via a RequestInterceptor:
+Feign.builder()
+ .requestInterceptor(template ->
+ template.header("Content-Type", "application/json"))
+ .target(MyApi.class, "https://api.example.com");
+```
+
+> [!NOTE]
+> Spring Cloud OpenFeign automatically sets `Content-Type: application/json` for `@RequestBody`-annotated parameters, so
+> Spring Feign users are not affected by this change.
+
+---
+
+### 16. `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.
+
+---
+
+### 17. `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()
+```
+
+---
+
+### 18. Composing multiple encoders with `Encoder.of()` (https://github.com/OpenFeign/feign/pull/3476)
+
+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`.
+
+---
+
+### 19. Multi-encoder support — `DelegatingEncoder` (https://github.com/OpenFeign/feign/pull/3476)
+
+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
diff --git a/api/pom.xml b/api/pom.xml
index 48dce106d..7eb0cfd0c 100644
--- a/api/pom.xml
+++ b/api/pom.xml
@@ -38,7 +38,7 @@
org.mockito
- mockito-core
+ mockito-junit-jupiter
${mockito.version}
test
diff --git a/api/src/main/java/feign/RequestTemplateFactoryResolver.java b/api/src/main/java/feign/RequestTemplateFactoryResolver.java
index dc78b51e7..8f4413757 100644
--- a/api/src/main/java/feign/RequestTemplateFactoryResolver.java
+++ b/api/src/main/java/feign/RequestTemplateFactoryResolver.java
@@ -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;
@@ -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) {
@@ -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;
@@ -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);
+ }
+ }
}
}
diff --git a/api/src/main/java/feign/Util.java b/api/src/main/java/feign/Util.java
index 639bb021a..5c9e35a1b 100644
--- a/api/src/main/java/feign/Util.java
+++ b/api/src/main/java/feign/Util.java
@@ -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.*"));
+ }
}
diff --git a/api/src/main/java/feign/codec/DelegatingEncoder.java b/api/src/main/java/feign/codec/DelegatingEncoder.java
new file mode 100644
index 000000000..9e0142bac
--- /dev/null
+++ b/api/src/main/java/feign/codec/DelegatingEncoder.java
@@ -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 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 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 + '}';
+ }
+}
diff --git a/api/src/main/java/feign/codec/Encoder.java b/api/src/main/java/feign/codec/Encoder.java
index 1979580a7..f980a4566 100644
--- a/api/src/main/java/feign/codec/Encoder.java
+++ b/api/src/main/java/feign/codec/Encoder.java
@@ -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
@@ -70,6 +72,30 @@ public interface Encoder {
/** Type literal for {@code Map}, 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 encoders) {
+ return new DelegatingEncoder(encoders);
+ }
+
/**
* Converts objects to an appropriate representation in the template.
*
@@ -77,7 +103,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;
}
diff --git a/api/src/test/java/feign/RequestTemplateFactoryResolverTest.java b/api/src/test/java/feign/RequestTemplateFactoryResolverTest.java
new file mode 100644
index 000000000..ba1ee8747
--- /dev/null
+++ b/api/src/test/java/feign/RequestTemplateFactoryResolverTest.java
@@ -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.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> 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()));
+ }
+ }
+}
diff --git a/api/src/test/java/feign/UtilTest.java b/api/src/test/java/feign/UtilTest.java
index d0a232c19..613aaeb4e 100644
--- a/api/src/test/java/feign/UtilTest.java
+++ b/api/src/test/java/feign/UtilTest.java
@@ -21,6 +21,7 @@
import static feign.Util.resolveLastTypeParameter;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.junit.jupiter.api.Assertions.assertEquals;
import feign.codec.Decoder;
import java.io.Reader;
@@ -35,6 +36,8 @@
import java.util.Optional;
import java.util.Set;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
class UtilTest {
@@ -313,6 +316,48 @@ void nullMap() {
assertThat(actualMap).isEmpty();
}
+ @ParameterizedTest
+ @CsvSource(
+ textBlock =
+ """
+ application/json, true
+ application/json;charset=UTF-8, true
+ application/ld+json, true
+ application/json-patch+json, true
+ application/vnd.api+json, true
+ application/manifest+json, true
+ text/json, true
+ text/xml, false
+ application/xml, false
+ """)
+ void testIsJsonContentType(String contentType, boolean expected) {
+ var template = new RequestTemplate().header("Content-Type", contentType);
+ var actual = Util.isJsonContentType(template);
+
+ assertEquals(expected, actual);
+ }
+
+ @ParameterizedTest
+ @CsvSource(
+ textBlock =
+ """
+ application/xml, true
+ application/xml;charset=UTF-8, true
+ application/atom+xml, true
+ application/rss+xml, true
+ application/soap+xml, true
+ text/xml, true
+ text/xml;charset=UTF-8, true
+ application/json, false
+ text/json, false
+ """)
+ void testIsXmlContentType(String contentType, boolean expected) {
+ var template = new RequestTemplate().header("Content-Type", contentType);
+ var actual = Util.isXmlContentType(template);
+
+ assertEquals(expected, actual);
+ }
+
interface LastTypeParameter {
List LIST_STRING = null;
Parameterized> PARAMETERIZED_LIST_STRING = null;
diff --git a/api/src/test/java/feign/codec/DelegatingEncoderTest.java b/api/src/test/java/feign/codec/DelegatingEncoderTest.java
new file mode 100644
index 000000000..99c3ca555
--- /dev/null
+++ b/api/src/test/java/feign/codec/DelegatingEncoderTest.java
@@ -0,0 +1,62 @@
+/*
+ * 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 static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import feign.RequestTemplate;
+import java.util.List;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+class DelegatingEncoderTest {
+ @Mock private Encoder delegate;
+ private Encoder encoder;
+
+ @BeforeEach
+ void setUp() {
+ encoder = new DelegatingEncoder(List.of(delegate));
+ }
+
+ @Test
+ void shouldEncode() {
+ var object = "Hello, World!";
+ var bodyType = String.class;
+ var requestTemplate = mock(RequestTemplate.class);
+
+ when(delegate.encode(object, bodyType, requestTemplate)).thenReturn(true);
+
+ encoder.encode(object, bodyType, requestTemplate);
+
+ verify(delegate).encode(object, bodyType, requestTemplate);
+ }
+
+ @Test
+ void shouldThrowEncodeExceptionWhenNoSuitableEncoderFound() {
+ var object = "Hello, World!";
+ var bodyType = String.class;
+ var requestTemplate = mock(RequestTemplate.class);
+
+ assertThrows(EncodeException.class, () -> encoder.encode(object, bodyType, requestTemplate));
+ }
+}
diff --git a/core/src/main/java/feign/core/codec/DefaultEncoder.java b/core/src/main/java/feign/core/codec/DefaultEncoder.java
index fe35256c1..5b021c456 100644
--- a/core/src/main/java/feign/core/codec/DefaultEncoder.java
+++ b/core/src/main/java/feign/core/codec/DefaultEncoder.java
@@ -15,11 +15,8 @@
*/
package feign.core.codec;
-import static java.lang.String.format;
-
import feign.Request;
import feign.RequestTemplate;
-import feign.codec.EncodeException;
import feign.codec.Encoder;
import java.io.File;
import java.io.InputStream;
@@ -29,7 +26,7 @@
public class DefaultEncoder implements Encoder {
@Override
- public void encode(Object object, Type bodyType, RequestTemplate template) {
+ public boolean encode(Object object, Type bodyType, RequestTemplate template) {
if (bodyType == String.class) {
template.body(Request.Body.of(object.toString()));
} else if (bodyType == byte[].class) {
@@ -42,9 +39,9 @@ public void encode(Object object, Type bodyType, RequestTemplate template) {
template.body(new Request.InputStreamBody((InputStream) object));
} else if (object instanceof Request.Body) {
template.body((Request.Body) object);
- } else if (object != null) {
- throw new EncodeException(
- format("%s is not a type supported by this encoder.", object.getClass()));
+ } else {
+ return object == null;
}
+ return true;
}
}
diff --git a/core/src/test/java/feign/AlwaysEncodeBodyContractTest.java b/core/src/test/java/feign/AlwaysEncodeBodyContractTest.java
index 1ea64c597..afcc79e57 100644
--- a/core/src/test/java/feign/AlwaysEncodeBodyContractTest.java
+++ b/core/src/test/java/feign/AlwaysEncodeBodyContractTest.java
@@ -61,20 +61,22 @@ private interface SampleTargetOneParameter {
private static class AllParametersSampleEncoder implements Encoder {
@Override
- public void encode(Object object, Type bodyType, RequestTemplate template)
+ public boolean encode(Object object, Type bodyType, RequestTemplate template)
throws EncodeException {
Object[] methodParameters = (Object[]) object;
String body =
Arrays.stream(methodParameters).map(String::valueOf).collect(Collectors.joining());
template.body(Request.Body.of(body));
+ return true;
}
}
private static class BodyParameterSampleEncoder implements Encoder {
@Override
- public void encode(Object object, Type bodyType, RequestTemplate template)
+ public boolean encode(Object object, Type bodyType, RequestTemplate template)
throws EncodeException {
template.body(Request.Body.of(String.valueOf(object)));
+ return true;
}
}
diff --git a/core/src/test/java/feign/AsyncFeignTest.java b/core/src/test/java/feign/AsyncFeignTest.java
index 0d81302db..c40ff45bb 100644
--- a/core/src/test/java/feign/AsyncFeignTest.java
+++ b/core/src/test/java/feign/AsyncFeignTest.java
@@ -149,8 +149,9 @@ void bodyTypeCorrespondsWithParameterType() throws Exception {
.encoder(
new DefaultEncoder() {
@Override
- public void encode(Object object, Type bodyType, RequestTemplate template) {
+ public boolean encode(Object object, Type bodyType, RequestTemplate template) {
encodedType.set(bodyType);
+ return true;
}
})
.target("http://localhost:" + server.getPort());
@@ -1242,6 +1243,7 @@ static final class TestInterfaceAsyncBuilder {
} else {
template.body(Request.Body.of(object.toString()));
}
+ return true;
});
TestInterfaceAsyncBuilder requestInterceptor(RequestInterceptor requestInterceptor) {
diff --git a/core/src/test/java/feign/BaseApiTest.java b/core/src/test/java/feign/BaseApiTest.java
index a59e521c7..4b48edd21 100644
--- a/core/src/test/java/feign/BaseApiTest.java
+++ b/core/src/test/java/feign/BaseApiTest.java
@@ -84,8 +84,10 @@ void resolvesBodyParameter() throws Exception {
Feign.builder()
.encoder(
- (_, bodyType, _) ->
- assertThat(bodyType).isEqualTo(new TypeToken>() {}.getType()))
+ (_, bodyType, _) -> {
+ assertThat(bodyType).isEqualTo(new TypeToken>() {}.getType());
+ return true;
+ })
.decoder(
(_, type) -> {
assertThat(type).isEqualTo(new TypeToken>() {}.getType());
diff --git a/core/src/test/java/feign/FeignBuilderTest.java b/core/src/test/java/feign/FeignBuilderTest.java
index 0a1ae468c..e20c18cc9 100644
--- a/core/src/test/java/feign/FeignBuilderTest.java
+++ b/core/src/test/java/feign/FeignBuilderTest.java
@@ -213,7 +213,11 @@ void overrideEncoder() throws Exception {
server.enqueue(new MockResponse.Builder().body("response data").build());
String url = "http://localhost:" + server.getPort();
- Encoder encoder = (object, _, template) -> template.body(Request.Body.of(object.toString()));
+ Encoder encoder =
+ (object, _, template) -> {
+ template.body(Request.Body.of(object.toString()));
+ return true;
+ };
TestInterface api = Feign.builder().encoder(encoder).target(TestInterface.class, url);
api.encodedPost(Arrays.asList("This", "is", "my", "request"));
diff --git a/core/src/test/java/feign/FeignTest.java b/core/src/test/java/feign/FeignTest.java
index 37ef0cdb2..d9e877d65 100755
--- a/core/src/test/java/feign/FeignTest.java
+++ b/core/src/test/java/feign/FeignTest.java
@@ -170,8 +170,9 @@ void bodyTypeCorrespondsWithParameterType() throws Exception {
.encoder(
new DefaultEncoder() {
@Override
- public void encode(Object object, Type bodyType, RequestTemplate template) {
+ public boolean encode(Object object, Type bodyType, RequestTemplate template) {
encodedType.set(bodyType);
+ return true;
}
})
.target("http://localhost:" + server.getPort());
@@ -1510,6 +1511,7 @@ static final class TestInterfaceBuilder {
} else {
template.body(Request.Body.of(object.toString()));
}
+ return true;
});
TestInterfaceBuilder requestInterceptor(RequestInterceptor requestInterceptor) {
diff --git a/core/src/test/java/feign/FeignUnderAsyncTest.java b/core/src/test/java/feign/FeignUnderAsyncTest.java
index a3f333bb5..5002e6c4c 100644
--- a/core/src/test/java/feign/FeignUnderAsyncTest.java
+++ b/core/src/test/java/feign/FeignUnderAsyncTest.java
@@ -133,8 +133,9 @@ void bodyTypeCorrespondsWithParameterType() throws Exception {
.encoder(
new DefaultEncoder() {
@Override
- public void encode(Object object, Type bodyType, RequestTemplate template) {
+ public boolean encode(Object object, Type bodyType, RequestTemplate template) {
encodedType.set(bodyType);
+ return true;
}
})
.target("http://localhost:" + server.getPort());
@@ -972,6 +973,7 @@ static final class TestInterfaceBuilder {
} else {
template.body(Request.Body.of(object.toString()));
}
+ return true;
});
TestInterfaceBuilder requestInterceptor(RequestInterceptor requestInterceptor) {
diff --git a/core/src/test/java/feign/MethodMetadataPresenceTest.java b/core/src/test/java/feign/MethodMetadataPresenceTest.java
index 1df9eb376..89ddcb587 100644
--- a/core/src/test/java/feign/MethodMetadataPresenceTest.java
+++ b/core/src/test/java/feign/MethodMetadataPresenceTest.java
@@ -67,7 +67,7 @@ void encoder() throws Exception {
assertThat(template).isNotNull();
assertThat(template.methodMetadata()).isNotNull();
assertThat(template.feignTarget()).isNotNull();
- new DefaultEncoder().encode(object, bodyType, template);
+ return new DefaultEncoder().encode(object, bodyType, template);
})
.target(TestInterface.class, url);
diff --git a/core/src/test/java/feign/core/codec/DefaultEncoderTest.java b/core/src/test/java/feign/core/codec/DefaultEncoderTest.java
index 3186c8580..4279262de 100644
--- a/core/src/test/java/feign/core/codec/DefaultEncoderTest.java
+++ b/core/src/test/java/feign/core/codec/DefaultEncoderTest.java
@@ -16,13 +16,11 @@
package feign.core.codec;
import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import feign.Request;
import feign.RequestTemplate;
-import feign.codec.EncodeException;
import feign.codec.Encoder;
import java.io.ByteArrayInputStream;
import java.io.File;
@@ -74,11 +72,7 @@ void encodesByteArray() throws Exception {
@Test
void refusesToEncodeOtherTypes() throws Exception {
- Throwable exception =
- assertThatExceptionOfType(EncodeException.class)
- .isThrownBy(() -> encoder.encode(Clock.systemUTC(), Clock.class, new RequestTemplate()))
- .actual();
- assertThat(exception.getMessage()).contains("is not a type supported by this encoder.");
+ assertThat(encoder.encode(Clock.systemUTC(), Clock.class, new RequestTemplate())).isFalse();
}
@Test
diff --git a/dropwizard-metrics4/src/main/java/feign/metrics4/MeteredEncoder.java b/dropwizard-metrics4/src/main/java/feign/metrics4/MeteredEncoder.java
index 4263d14d5..1f60c6fdd 100644
--- a/dropwizard-metrics4/src/main/java/feign/metrics4/MeteredEncoder.java
+++ b/dropwizard-metrics4/src/main/java/feign/metrics4/MeteredEncoder.java
@@ -39,15 +39,16 @@ public MeteredEncoder(
}
@Override
- public void encode(Object object, Type bodyType, RequestTemplate template)
+ public boolean encode(Object object, Type bodyType, RequestTemplate template)
throws EncodeException {
+ boolean isEncoded;
try (final Timer.Context classTimer =
metricRegistry
.timer(
metricName.metricName(template.methodMetadata(), template.feignTarget()),
metricSuppliers.timers())
.time()) {
- encoder.encode(object, bodyType, template);
+ isEncoded = encoder.encode(object, bodyType, template);
}
template
@@ -60,5 +61,6 @@ public void encode(Object object, Type bodyType, RequestTemplate template)
template.methodMetadata(), template.feignTarget(), "request_size"),
metricSuppliers.histograms())
.update(body.contentLength()));
+ return isEncoded;
}
}
diff --git a/dropwizard-metrics5/src/main/java/feign/metrics5/MeteredEncoder.java b/dropwizard-metrics5/src/main/java/feign/metrics5/MeteredEncoder.java
index 09f3058be..d48eb87dc 100644
--- a/dropwizard-metrics5/src/main/java/feign/metrics5/MeteredEncoder.java
+++ b/dropwizard-metrics5/src/main/java/feign/metrics5/MeteredEncoder.java
@@ -51,15 +51,16 @@ public MeteredEncoder(
}
@Override
- public void encode(Object object, Type bodyType, RequestTemplate template)
+ public boolean encode(Object object, Type bodyType, RequestTemplate template)
throws EncodeException {
+ boolean isEncoded;
try (final Context classTimer =
metricRegistry
.timer(
metricName.metricName(template.methodMetadata(), template.feignTarget()),
metricSuppliers.timers())
.time()) {
- encoder.encode(object, bodyType, template);
+ isEncoded = encoder.encode(object, bodyType, template);
}
template
@@ -72,5 +73,6 @@ public void encode(Object object, Type bodyType, RequestTemplate template)
template.methodMetadata(), template.feignTarget(), "request_size"),
metricSuppliers.histograms())
.update(body.contentLength()));
+ return isEncoded;
}
}
diff --git a/fastjson2/src/main/java/feign/fastjson2/Fastjson2Encoder.java b/fastjson2/src/main/java/feign/fastjson2/Fastjson2Encoder.java
index 7e79af331..ce477aaca 100644
--- a/fastjson2/src/main/java/feign/fastjson2/Fastjson2Encoder.java
+++ b/fastjson2/src/main/java/feign/fastjson2/Fastjson2Encoder.java
@@ -19,6 +19,7 @@
import com.alibaba.fastjson2.JSONWriter;
import feign.Request;
import feign.RequestTemplate;
+import feign.Util;
import feign.codec.EncodeException;
import feign.codec.Encoder;
import feign.codec.JsonEncoder;
@@ -40,8 +41,12 @@ public Fastjson2Encoder(JSONWriter.Feature[] features) {
}
@Override
- public void encode(Object object, Type bodyType, RequestTemplate template)
+ public boolean encode(Object object, Type bodyType, RequestTemplate template)
throws EncodeException {
+ if (!Util.isJsonContentType(template)) {
+ return false;
+ }
template.body(Request.Body.of(JSON.toJSONBytes(object, features)));
+ return true;
}
}
diff --git a/fastjson2/src/test/java/feign/fastjson2/FastJsonCodecTest.java b/fastjson2/src/test/java/feign/fastjson2/FastJsonCodecTest.java
index b6b897a56..d1d6cfd67 100644
--- a/fastjson2/src/test/java/feign/fastjson2/FastJsonCodecTest.java
+++ b/fastjson2/src/test/java/feign/fastjson2/FastJsonCodecTest.java
@@ -66,6 +66,7 @@ void encodesMapObjectNumericalValuesAsInteger() {
map.put("foo", 1);
RequestTemplate template = new RequestTemplate();
+ template.header("Content-Type", "application/json");
new Fastjson2Encoder().encode(map, map.getClass(), template);
assertThat(template)
@@ -83,6 +84,7 @@ void encodesFormParams() {
form.put("bar", Arrays.asList(2, 3));
RequestTemplate template = new RequestTemplate();
+ template.header("Content-Type", "application/json");
new Fastjson2Encoder().encode(form, new TypeReference