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>() {}.getType(), template); assertThat(template) diff --git a/form-spring/src/main/java/feign/form/spring/SpringFormEncoder.java b/form-spring/src/main/java/feign/form/spring/SpringFormEncoder.java index e177767c1..60a678900 100644 --- a/form-spring/src/main/java/feign/form/spring/SpringFormEncoder.java +++ b/form-spring/src/main/java/feign/form/spring/SpringFormEncoder.java @@ -55,7 +55,7 @@ public SpringFormEncoder(Encoder delegate) { } @Override - public void encode(Object object, Type bodyType, RequestTemplate template) + public boolean encode(Object object, Type bodyType, RequestTemplate template) throws EncodeException { if (bodyType.equals(MultipartFile[].class)) { final var files = (MultipartFile[]) object; @@ -63,11 +63,11 @@ public void encode(Object object, Type bodyType, RequestTemplate template) for (var file : files) { data.put(file.getName(), file); } - super.encode(data, MAP_STRING_WILDCARD, template); + return super.encode(data, MAP_STRING_WILDCARD, template); } else if (bodyType.equals(MultipartFile.class)) { final var file = (MultipartFile) object; final var data = singletonMap(file.getName(), object); - super.encode(data, MAP_STRING_WILDCARD, template); + return super.encode(data, MAP_STRING_WILDCARD, template); } else if (isMultipartFileCollection(object)) { final var iterable = (Iterable) object; final var data = new HashMap(); @@ -75,9 +75,9 @@ public void encode(Object object, Type bodyType, RequestTemplate template) final var file = (MultipartFile) item; data.put(file.getName(), file); } - super.encode(data, MAP_STRING_WILDCARD, template); + return super.encode(data, MAP_STRING_WILDCARD, template); } else { - super.encode(object, bodyType, template); + return super.encode(object, bodyType, template); } } diff --git a/form-spring/src/test/java/feign/form/feign/spring/SpringFormEncoderTest.java b/form-spring/src/test/java/feign/form/feign/spring/SpringFormEncoderTest.java index 309397c0e..0c605e9b8 100644 --- a/form-spring/src/test/java/feign/form/feign/spring/SpringFormEncoderTest.java +++ b/form-spring/src/test/java/feign/form/feign/spring/SpringFormEncoderTest.java @@ -23,6 +23,7 @@ import feign.Response; import java.util.HashMap; import java.util.List; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @@ -76,6 +77,8 @@ void uploadFileNameAndContentTypeTest() throws Exception { .isEqualTo(file.getOriginalFilename() + ':' + file.getContentType() + ':' + folder); } + // TODO: enable when Spring Cloud OpenFeign migrates to Feign 14 + @Disabled("Disabled until Spring Cloud OpenFeign migrates to Feign 14") @Test void upload4Test() throws Exception { var map = new HashMap(); diff --git a/form/src/main/java/feign/form/FormEncoder.java b/form/src/main/java/feign/form/FormEncoder.java index deb465673..5d2aeaf91 100644 --- a/form/src/main/java/feign/form/FormEncoder.java +++ b/form/src/main/java/feign/form/FormEncoder.java @@ -80,13 +80,12 @@ public FormEncoder(Encoder delegate) { @Override @SuppressWarnings("unchecked") - public void encode(Object object, Type bodyType, RequestTemplate template) + public boolean encode(Object object, Type bodyType, RequestTemplate template) throws EncodeException { String contentTypeValue = getContentTypeValue(template.headers()); final var contentType = ContentType.of(contentTypeValue); if (processors.containsKey(contentType) == false) { - delegate.encode(object, bodyType, template); - return; + return delegate.encode(object, bodyType, template); } Map data; @@ -95,12 +94,12 @@ public void encode(Object object, Type bodyType, RequestTemplate template) } else if (isUserPojo(bodyType)) { data = toMap(object); } else { - delegate.encode(object, bodyType, template); - return; + return delegate.encode(object, bodyType, template); } final var charset = getCharset(contentTypeValue); processors.get(contentType).process(template, charset, data); + return true; } /** diff --git a/form/src/test/java/feign/form/MultipartBoundaryTest.java b/form/src/test/java/feign/form/MultipartBoundaryTest.java index 26399fcdc..dd2fc9783 100644 --- a/form/src/test/java/feign/form/MultipartBoundaryTest.java +++ b/form/src/test/java/feign/form/MultipartBoundaryTest.java @@ -26,7 +26,7 @@ class MultipartBoundaryTest { - private static final Encoder NOOP_DELEGATE = (object, bodyType, template) -> {}; + private static final Encoder NOOP_DELEGATE = (object, bodyType, template) -> true; @Test void boundaryIsNotDerivedFromTheClock() { diff --git a/form/src/test/java/feign/form/multipart/DelegateWriterTest.java b/form/src/test/java/feign/form/multipart/DelegateWriterTest.java index 3a3f7a248..f2515cedc 100644 --- a/form/src/test/java/feign/form/multipart/DelegateWriterTest.java +++ b/form/src/test/java/feign/form/multipart/DelegateWriterTest.java @@ -34,6 +34,7 @@ void usesContentTypeFromDelegate() throws Exception { (object, bodyType, template) -> { template.header("Content-Type", "application/json"); template.body(Request.Body.of("{\"hash\":\"somehash\"}")); + return true; }; assertThat(write(delegate)) @@ -43,7 +44,11 @@ void usesContentTypeFromDelegate() throws Exception { @Test void fallsBackToTextPlainWhenDelegateSetsNoContentType() throws Exception { - Encoder delegate = (object, bodyType, template) -> template.body(Request.Body.of("plain")); + Encoder delegate = + (object, bodyType, template) -> { + template.body(Request.Body.of("plain")); + return true; + }; assertThat(write(delegate)).contains("Content-Type: text/plain; charset=UTF-8"); } diff --git a/graphql/src/main/java/feign/graphql/GraphqlEncoder.java b/graphql/src/main/java/feign/graphql/GraphqlEncoder.java index 4370a2622..c911685c0 100644 --- a/graphql/src/main/java/feign/graphql/GraphqlEncoder.java +++ b/graphql/src/main/java/feign/graphql/GraphqlEncoder.java @@ -34,12 +34,11 @@ public GraphqlEncoder(Encoder delegate, GraphqlContract contract) { } @Override - public void encode(Object object, Type bodyType, RequestTemplate template) + public boolean encode(Object object, Type bodyType, RequestTemplate template) throws EncodeException { var meta = contract.lookupMetadata(template); if (meta == null) { - delegate.encode(object, bodyType, template); - return; + return delegate.encode(object, bodyType, template); } var graphqlBody = new LinkedHashMap(); @@ -52,5 +51,6 @@ public void encode(Object object, Type bodyType, RequestTemplate template) } delegate.encode(graphqlBody, MAP_STRING_WILDCARD, template); + return true; } } diff --git a/graphql/src/test/java/feign/graphql/GraphqlEncoderTest.java b/graphql/src/test/java/feign/graphql/GraphqlEncoderTest.java index d25f21bce..91053c2fd 100644 --- a/graphql/src/test/java/feign/graphql/GraphqlEncoderTest.java +++ b/graphql/src/test/java/feign/graphql/GraphqlEncoderTest.java @@ -73,6 +73,7 @@ private String requestBodyString(RequestTemplate template) { @Test void encodesBodyWithVariables() throws Exception { var template = templateFor(MutationApi.class); + template.header("Content-Type", "application/json"); var body = Map.of("name", "John", "email", "john@example.com"); encoder.encode(body, Map.class, template); @@ -87,6 +88,7 @@ void encodesBodyWithVariables() throws Exception { @Test void delegatesToWrappedEncoderForNonGraphql() { var template = new RequestTemplate(); + template.header("Content-Type", "application/json"); encoder.encode("plain body", String.class, template); assertThat(template.requestBody()).isPresent(); } @@ -94,6 +96,7 @@ void delegatesToWrappedEncoderForNonGraphql() { @Test void interceptorSetsBodyForNoVariableQuery() throws Exception { var template = templateFor(NoVariableApi.class); + template.header("Content-Type", "application/json"); interceptor.apply(template); var result = mapper.readTree(requestBodyBytes(template)); diff --git a/gson/src/main/java/feign/gson/GsonEncoder.java b/gson/src/main/java/feign/gson/GsonEncoder.java index c0c3d0eb2..105a0fea1 100644 --- a/gson/src/main/java/feign/gson/GsonEncoder.java +++ b/gson/src/main/java/feign/gson/GsonEncoder.java @@ -19,6 +19,7 @@ import com.google.gson.TypeAdapter; import feign.Request; import feign.RequestTemplate; +import feign.Util; import feign.codec.Encoder; import feign.codec.JsonEncoder; import java.lang.reflect.Type; @@ -41,7 +42,11 @@ public GsonEncoder(Gson gson) { } @Override - public void encode(Object object, Type bodyType, RequestTemplate template) { + public boolean encode(Object object, Type bodyType, RequestTemplate template) { + if (!Util.isJsonContentType(template)) { + return false; + } template.body(Request.Body.of(gson.toJson(object, bodyType))); + return true; } } diff --git a/gson/src/test/java/feign/gson/GsonCodecTest.java b/gson/src/test/java/feign/gson/GsonCodecTest.java index f1a99670b..13a06ce36 100644 --- a/gson/src/test/java/feign/gson/GsonCodecTest.java +++ b/gson/src/test/java/feign/gson/GsonCodecTest.java @@ -45,6 +45,7 @@ void encodesMapObjectNumericalValuesAsInteger() { map.put("foo", 1); RequestTemplate template = new RequestTemplate(); + template.header("Content-Type", "application/json"); new GsonEncoder().encode(map, map.getClass(), template); assertThat(template) @@ -82,6 +83,7 @@ void encodesFormParams() { form.put("bar", Arrays.asList(2, 3)); RequestTemplate template = new RequestTemplate(); + template.header("Content-Type", "application/json"); new GsonEncoder().encode(form, new TypeToken>() {}.getType(), template); assertThat(template) @@ -226,6 +228,7 @@ void customEncoder() { zones.add(new Zone("denominator.io.", "abcd")); RequestTemplate template = new RequestTemplate(); + template.header("Content-Type", "application/json"); encoder.encode(zones, new TypeToken>() {}.getType(), template); assertThat(template) diff --git a/hc5/src/test/java/feign/hc5/AsyncApacheHttp5ClientTest.java b/hc5/src/test/java/feign/hc5/AsyncApacheHttp5ClientTest.java index f8396dcd9..1b0bcbf5e 100644 --- a/hc5/src/test/java/feign/hc5/AsyncApacheHttp5ClientTest.java +++ b/hc5/src/test/java/feign/hc5/AsyncApacheHttp5ClientTest.java @@ -162,8 +162,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()); @@ -1093,6 +1094,7 @@ static final class TestInterfaceAsyncBuilder { } else { template.body(Request.Body.of(object.toString())); } + return true; }); TestInterfaceAsyncBuilder requestInterceptor(RequestInterceptor requestInterceptor) { diff --git a/jackson-jaxb/src/main/java/feign/jackson/jaxb/JacksonJaxbJsonEncoder.java b/jackson-jaxb/src/main/java/feign/jackson/jaxb/JacksonJaxbJsonEncoder.java index 6e4caeaed..7f5770169 100644 --- a/jackson-jaxb/src/main/java/feign/jackson/jaxb/JacksonJaxbJsonEncoder.java +++ b/jackson-jaxb/src/main/java/feign/jackson/jaxb/JacksonJaxbJsonEncoder.java @@ -22,6 +22,7 @@ import com.fasterxml.jackson.jaxrs.json.JacksonJaxbJsonProvider; import feign.Request; import feign.RequestTemplate; +import feign.Util; import feign.codec.EncodeException; import feign.codec.Encoder; import java.io.ByteArrayOutputStream; @@ -40,13 +41,17 @@ public JacksonJaxbJsonEncoder(ObjectMapper objectMapper) { } @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; + } ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); try { jacksonJaxbJsonProvider.writeTo( object, bodyType.getClass(), null, null, APPLICATION_JSON_TYPE, null, outputStream); template.body(Request.Body.of(outputStream.toByteArray())); + return true; } catch (IOException e) { throw new EncodeException(e.getMessage(), e); } diff --git a/jackson-jaxb/src/test/java/feign/jackson/jaxb/JacksonJaxbCodecTest.java b/jackson-jaxb/src/test/java/feign/jackson/jaxb/JacksonJaxbCodecTest.java index 29670826e..b3996f1a3 100644 --- a/jackson-jaxb/src/test/java/feign/jackson/jaxb/JacksonJaxbCodecTest.java +++ b/jackson-jaxb/src/test/java/feign/jackson/jaxb/JacksonJaxbCodecTest.java @@ -37,6 +37,7 @@ class JacksonJaxbCodecTest { void encodeTest() { JacksonJaxbJsonEncoder encoder = new JacksonJaxbJsonEncoder(); RequestTemplate template = new RequestTemplate(); + template.header("Content-Type", "application/json"); encoder.encode(new MockObject("Test"), MockObject.class, template); diff --git a/jackson-jr/src/main/java/feign/jackson/jr/JacksonJrEncoder.java b/jackson-jr/src/main/java/feign/jackson/jr/JacksonJrEncoder.java index 26fe78179..93ec95abb 100644 --- a/jackson-jr/src/main/java/feign/jackson/jr/JacksonJrEncoder.java +++ b/jackson-jr/src/main/java/feign/jackson/jr/JacksonJrEncoder.java @@ -19,6 +19,7 @@ import com.fasterxml.jackson.jr.ob.JacksonJrExtension; import feign.Request; import feign.RequestTemplate; +import feign.Util; import feign.codec.EncodeException; import feign.codec.Encoder; import java.io.IOException; @@ -51,13 +52,17 @@ public JacksonJrEncoder(Iterable iterable) { } @Override - public void encode(Object object, Type bodyType, RequestTemplate template) { + public boolean encode(Object object, Type bodyType, RequestTemplate template) { + if (!Util.isJsonContentType(template)) { + return false; + } try { if (bodyType == byte[].class) { template.body(Request.Body.of(mapper.asBytes(object))); } else { template.body(Request.Body.of(mapper.asString(object))); } + return true; } catch (IOException e) { throw new EncodeException(e.getMessage(), e); } diff --git a/jackson-jr/src/test/java/feign/jackson/jr/JacksonCodecTest.java b/jackson-jr/src/test/java/feign/jackson/jr/JacksonCodecTest.java index 85759e43e..156c9222d 100644 --- a/jackson-jr/src/test/java/feign/jackson/jr/JacksonCodecTest.java +++ b/jackson-jr/src/test/java/feign/jackson/jr/JacksonCodecTest.java @@ -53,6 +53,7 @@ void encodesMapObjectNumericalValuesAsInteger() { map.put("foo", 1); RequestTemplate template = new RequestTemplate(); + template.header("Content-Type", "application/json"); new JacksonJrEncoder().encode(map, map.getClass(), template); assertThat(template).hasBody("{\"foo\":1}"); @@ -64,6 +65,7 @@ void encodesMapObjectNumericalValuesToByteArray() { map.put("foo", 1); RequestTemplate template = new RequestTemplate(); + template.header("Content-Type", "application/json"); new JacksonJrEncoder().encode(map, byte[].class, template); assertThat(template).hasBody("{\"foo\":1}"); @@ -76,6 +78,7 @@ void encodesFormParams() { form.put("bar", Arrays.asList(2, 3)); RequestTemplate template = new RequestTemplate(); + template.header("Content-Type", "application/json"); new JacksonJrEncoder().encode(form, new TypeReference>() {}.getType(), template); assertThat(template).hasBody("{\"foo\":1,\"bar\":[2,3]}"); @@ -178,6 +181,7 @@ void customEncoder() { dates.add(LocalDate.of(2021, 2, 3)); RequestTemplate template = new RequestTemplate(); + template.header("Content-Type", "application/json"); encoder.encode(dates, new TypeReference>() {}.getType(), template); assertThat(template).hasBody(DATES_JSON); diff --git a/jackson/src/main/java/feign/jackson/JacksonEncoder.java b/jackson/src/main/java/feign/jackson/JacksonEncoder.java index 280cd3bb5..1ad7b4284 100644 --- a/jackson/src/main/java/feign/jackson/JacksonEncoder.java +++ b/jackson/src/main/java/feign/jackson/JacksonEncoder.java @@ -23,6 +23,7 @@ import com.fasterxml.jackson.databind.SerializationFeature; import feign.Request; import feign.RequestTemplate; +import feign.Util; import feign.codec.EncodeException; import feign.codec.Encoder; import feign.codec.JsonEncoder; @@ -50,10 +51,14 @@ public JacksonEncoder(ObjectMapper mapper) { } @Override - public void encode(Object object, Type bodyType, RequestTemplate template) { + public boolean encode(Object object, Type bodyType, RequestTemplate template) { + if (!Util.isJsonContentType(template)) { + return false; + } try { JavaType javaType = mapper.getTypeFactory().constructType(bodyType); template.body(Request.Body.of(mapper.writerFor(javaType).writeValueAsBytes(object))); + return true; } catch (JsonProcessingException e) { throw new EncodeException(e.getMessage(), e); } diff --git a/jackson/src/test/java/feign/jackson/JacksonCodecTest.java b/jackson/src/test/java/feign/jackson/JacksonCodecTest.java index f6ae68174..8833942ff 100644 --- a/jackson/src/test/java/feign/jackson/JacksonCodecTest.java +++ b/jackson/src/test/java/feign/jackson/JacksonCodecTest.java @@ -77,6 +77,7 @@ void encodesMapObjectNumericalValuesAsInteger() { map.put("foo", 1); RequestTemplate template = new RequestTemplate(); + template.header("Content-Type", "application/json"); new JacksonEncoder().encode(map, map.getClass(), template); assertThat(template) @@ -96,6 +97,7 @@ void encodesFormParams() { form.put("bar", Arrays.asList(2, 3)); RequestTemplate template = new RequestTemplate(); + template.header("Content-Type", "application/json"); new JacksonEncoder().encode(form, new TypeReference>() {}.getType(), template); assertThat(template) @@ -186,6 +188,7 @@ void customEncoder() { zones.add(new Zone("denominator.io.", "abcd")); RequestTemplate template = new RequestTemplate(); + template.header("Content-Type", "application/json"); encoder.encode(zones, new TypeReference>() {}.getType(), template); assertThat(template) diff --git a/jackson3/src/main/java/feign/jackson3/Jackson3Encoder.java b/jackson3/src/main/java/feign/jackson3/Jackson3Encoder.java index 273e6d8f7..72277ea96 100644 --- a/jackson3/src/main/java/feign/jackson3/Jackson3Encoder.java +++ b/jackson3/src/main/java/feign/jackson3/Jackson3Encoder.java @@ -18,6 +18,7 @@ import com.fasterxml.jackson.annotation.JsonInclude; import feign.Request; import feign.RequestTemplate; +import feign.Util; import feign.codec.EncodeException; import feign.codec.Encoder; import feign.codec.JsonEncoder; @@ -52,10 +53,14 @@ public Jackson3Encoder(JsonMapper mapper) { } @Override - public void encode(Object object, Type bodyType, RequestTemplate template) { + public boolean encode(Object object, Type bodyType, RequestTemplate template) { + if (!Util.isJsonContentType(template)) { + return false; + } try { JavaType javaType = mapper.getTypeFactory().constructType(bodyType); template.body(Request.Body.of(mapper.writerFor(javaType).writeValueAsBytes(object))); + return true; } catch (JacksonException e) { throw new EncodeException(e.getMessage(), e); } diff --git a/jackson3/src/test/java/feign/jackson3/Jackson3CodecTest.java b/jackson3/src/test/java/feign/jackson3/Jackson3CodecTest.java index 6f1e9df89..0f9fa2331 100644 --- a/jackson3/src/test/java/feign/jackson3/Jackson3CodecTest.java +++ b/jackson3/src/test/java/feign/jackson3/Jackson3CodecTest.java @@ -77,6 +77,7 @@ void encodesMapObjectNumericalValuesAsInteger() { map.put("foo", 1); RequestTemplate template = new RequestTemplate(); + template.header("Content-Type", "application/json"); new Jackson3Encoder().encode(map, map.getClass(), template); assertThat(template) @@ -96,6 +97,7 @@ void encodesFormParams() { form.put("bar", Arrays.asList(2, 3)); RequestTemplate template = new RequestTemplate(); + template.header("Content-Type", "application/json"); new Jackson3Encoder().encode(form, new TypeReference>() {}.getType(), template); assertThat(template) @@ -186,6 +188,7 @@ void customEncoder() { zones.add(new Zone("denominator.io.", "abcd")); RequestTemplate template = new RequestTemplate(); + template.header("Content-Type", "application/json"); encoder.encode(zones, new TypeReference>() {}.getType(), template); assertThat(template) diff --git a/java11/src/test/java/feign/http2client/test/Http2ClientAsyncTest.java b/java11/src/test/java/feign/http2client/test/Http2ClientAsyncTest.java index d8eb6c122..d386814df 100644 --- a/java11/src/test/java/feign/http2client/test/Http2ClientAsyncTest.java +++ b/java11/src/test/java/feign/http2client/test/Http2ClientAsyncTest.java @@ -169,8 +169,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()); @@ -1036,6 +1037,7 @@ static final class TestInterfaceAsyncBuilder { } else { template.body(Request.Body.of(object.toString())); } + return true; }); TestInterfaceAsyncBuilder requestInterceptor(RequestInterceptor requestInterceptor) { diff --git a/jaxb-jakarta/src/main/java/feign/jaxb/JAXBEncoder.java b/jaxb-jakarta/src/main/java/feign/jaxb/JAXBEncoder.java index a25845800..472bc8638 100644 --- a/jaxb-jakarta/src/main/java/feign/jaxb/JAXBEncoder.java +++ b/jaxb-jakarta/src/main/java/feign/jaxb/JAXBEncoder.java @@ -17,6 +17,7 @@ import feign.Request; import feign.RequestTemplate; +import feign.Util; import feign.codec.EncodeException; import feign.codec.Encoder; import jakarta.xml.bind.JAXBException; @@ -52,7 +53,10 @@ public JAXBEncoder(JAXBContextFactory jaxbContextFactory) { } @Override - public void encode(Object object, Type bodyType, RequestTemplate template) { + public boolean encode(Object object, Type bodyType, RequestTemplate template) { + if (!Util.isXmlContentType(template)) { + return false; + } if (!(bodyType instanceof Class)) { throw new UnsupportedOperationException( "JAXB only supports encoding raw types. Found " + bodyType); @@ -62,6 +66,7 @@ public void encode(Object object, Type bodyType, RequestTemplate template) { StringWriter stringWriter = new StringWriter(); marshaller.marshal(object, stringWriter); template.body(Request.Body.of(stringWriter.toString())); + return true; } catch (JAXBException e) { throw new EncodeException(e.toString(), e); } diff --git a/jaxb-jakarta/src/test/java/feign/jaxb/JAXBCodecTest.java b/jaxb-jakarta/src/test/java/feign/jaxb/JAXBCodecTest.java index 741069670..676328ebf 100644 --- a/jaxb-jakarta/src/test/java/feign/jaxb/JAXBCodecTest.java +++ b/jaxb-jakarta/src/test/java/feign/jaxb/JAXBCodecTest.java @@ -54,6 +54,7 @@ void encodesXml() throws Exception { mock.value = "Test"; RequestTemplate template = new RequestTemplate(); + template.header("Content-Type", "text/xml"); new JAXBEncoder(new JAXBContextFactory.Builder().build()) .encode(mock, MockObject.class, template); @@ -73,6 +74,7 @@ class ParameterizedHolder { Type parameterized = ParameterizedHolder.class.getDeclaredField("field").getGenericType(); RequestTemplate template = new RequestTemplate(); + template.header("Content-Type", "text/xml"); Throwable exception = assertThatExceptionOfType(UnsupportedOperationException.class) .isThrownBy( @@ -96,6 +98,7 @@ void encodesXmlWithCustomJAXBEncoding() throws Exception { mock.value = "Test"; RequestTemplate template = new RequestTemplate(); + template.header("Content-Type", "text/xml"); encoder.encode(mock, MockObject.class, template); assertThat(template) @@ -119,6 +122,7 @@ void encodesXmlWithCustomJAXBSchemaLocation() throws Exception { mock.value = "Test"; RequestTemplate template = new RequestTemplate(); + template.header("Content-Type", "text/xml"); encoder.encode(mock, MockObject.class, template); assertThat(template) @@ -144,6 +148,7 @@ void encodesXmlWithCustomJAXBNoNamespaceSchemaLocation() throws Exception { mock.value = "Test"; RequestTemplate template = new RequestTemplate(); + template.header("Content-Type", "text/xml"); encoder.encode(mock, MockObject.class, template); assertThat(template) @@ -167,6 +172,7 @@ void encodesXmlWithCustomJAXBFormattedOutput() { mock.value = "Test"; RequestTemplate template = new RequestTemplate(); + template.header("Content-Type", "text/xml"); encoder.encode(mock, MockObject.class, template); // RequestTemplate always expects a UNIX style newline. @@ -359,6 +365,7 @@ void encodeThrowsExceptionWhenMarshallingFailsWithSetSchema() throws Exception { Encoder encoder = new JAXBEncoder(jaxbContextFactory); RequestTemplate template = new RequestTemplate(); + template.header("Content-Type", "text/xml"); EncodeException exception = assertThatExceptionOfType(EncodeException.class) .isThrownBy(() -> encoder.encode(new MockIntObject(), MockIntObject.class, template)) @@ -379,6 +386,7 @@ void encodesIgnoringErrorsWithEventHandler() throws Exception { Encoder encoder = new JAXBEncoder(jaxbContextFactory); RequestTemplate template = new RequestTemplate(); + template.header("Content-Type", "text/xml"); encoder.encode(new MockIntObject(), MockIntObject.class, template); assertThat(template) .hasBody( diff --git a/jaxb/src/main/java/feign/jaxb/JAXBEncoder.java b/jaxb/src/main/java/feign/jaxb/JAXBEncoder.java index 4c05d82e7..10fbdcffc 100644 --- a/jaxb/src/main/java/feign/jaxb/JAXBEncoder.java +++ b/jaxb/src/main/java/feign/jaxb/JAXBEncoder.java @@ -17,6 +17,7 @@ import feign.Request; import feign.RequestTemplate; +import feign.Util; import feign.codec.EncodeException; import feign.codec.Encoder; import java.io.StringWriter; @@ -52,7 +53,10 @@ public JAXBEncoder(JAXBContextFactory jaxbContextFactory) { } @Override - public void encode(Object object, Type bodyType, RequestTemplate template) { + public boolean encode(Object object, Type bodyType, RequestTemplate template) { + if (!Util.isXmlContentType(template)) { + return false; + } if (!(bodyType instanceof Class)) { throw new UnsupportedOperationException( "JAXB only supports encoding raw types. Found " + bodyType); @@ -62,6 +66,7 @@ public void encode(Object object, Type bodyType, RequestTemplate template) { StringWriter stringWriter = new StringWriter(); marshaller.marshal(object, stringWriter); template.body(Request.Body.of(stringWriter.toString())); + return true; } catch (JAXBException e) { throw new EncodeException(e.toString(), e); } diff --git a/jaxb/src/test/java/feign/jaxb/JAXBCodecTest.java b/jaxb/src/test/java/feign/jaxb/JAXBCodecTest.java index 97bc514d0..9319a71df 100644 --- a/jaxb/src/test/java/feign/jaxb/JAXBCodecTest.java +++ b/jaxb/src/test/java/feign/jaxb/JAXBCodecTest.java @@ -54,6 +54,7 @@ void encodesXml() throws Exception { mock.value = "Test"; RequestTemplate template = new RequestTemplate(); + template.header("Content-Type", "text/xml"); new JAXBEncoder(new JAXBContextFactory.Builder().build()) .encode(mock, MockObject.class, template); @@ -73,6 +74,7 @@ class ParameterizedHolder { Type parameterized = ParameterizedHolder.class.getDeclaredField("field").getGenericType(); RequestTemplate template = new RequestTemplate(); + template.header("Content-Type", "text/xml"); Throwable exception = assertThatExceptionOfType(UnsupportedOperationException.class) .isThrownBy( @@ -96,6 +98,7 @@ void encodesXmlWithCustomJAXBEncoding() throws Exception { mock.value = "Test"; RequestTemplate template = new RequestTemplate(); + template.header("Content-Type", "text/xml"); encoder.encode(mock, MockObject.class, template); assertThat(template) @@ -119,6 +122,7 @@ void encodesXmlWithCustomJAXBSchemaLocation() throws Exception { mock.value = "Test"; RequestTemplate template = new RequestTemplate(); + template.header("Content-Type", "text/xml"); encoder.encode(mock, MockObject.class, template); assertThat(template) @@ -144,6 +148,7 @@ void encodesXmlWithCustomJAXBNoNamespaceSchemaLocation() throws Exception { mock.value = "Test"; RequestTemplate template = new RequestTemplate(); + template.header("Content-Type", "text/xml"); encoder.encode(mock, MockObject.class, template); assertThat(template) @@ -167,6 +172,7 @@ void encodesXmlWithCustomJAXBFormattedOutput() { mock.value = "Test"; RequestTemplate template = new RequestTemplate(); + template.header("Content-Type", "text/xml"); encoder.encode(mock, MockObject.class, template); // RequestTemplate always expects a UNIX style newline. @@ -360,6 +366,7 @@ void encodeThrowsExceptionWhenMarshallingFailsWithSetSchema() throws Exception { Encoder encoder = new JAXBEncoder(jaxbContextFactory); RequestTemplate template = new RequestTemplate(); + template.header("Content-Type", "text/xml"); EncodeException exception = assertThatExceptionOfType(EncodeException.class) .isThrownBy(() -> encoder.encode(new MockIntObject(), MockIntObject.class, template)) @@ -380,6 +387,7 @@ void encodesIgnoringErrorsWithEventHandler() throws Exception { Encoder encoder = new JAXBEncoder(jaxbContextFactory); RequestTemplate template = new RequestTemplate(); + template.header("Content-Type", "text/xml"); encoder.encode(new MockIntObject(), MockIntObject.class, template); assertThat(template) .hasBody( diff --git a/json/src/main/java/feign/json/JsonEncoder.java b/json/src/main/java/feign/json/JsonEncoder.java index 1ef3cd6fb..a841132aa 100644 --- a/json/src/main/java/feign/json/JsonEncoder.java +++ b/json/src/main/java/feign/json/JsonEncoder.java @@ -55,11 +55,12 @@ public class JsonEncoder implements Encoder { @Override - public void encode(Object object, Type bodyType, RequestTemplate template) + public boolean encode(Object object, Type bodyType, RequestTemplate template) throws EncodeException { - if (object == null) return; + if (object == null) return true; if (object instanceof JSONArray || object instanceof JSONObject) { template.body(Request.Body.of(object.toString())); + return true; } else { throw new EncodeException(format("%s is not a type supported by this encoder.", bodyType)); } diff --git a/kotlin/src/test/kotlin/feign/kotlin/CoroutineFeignTest.kt b/kotlin/src/test/kotlin/feign/kotlin/CoroutineFeignTest.kt index a25e3d5a8..e9309a745 100644 --- a/kotlin/src/test/kotlin/feign/kotlin/CoroutineFeignTest.kt +++ b/kotlin/src/test/kotlin/feign/kotlin/CoroutineFeignTest.kt @@ -190,11 +190,12 @@ class CoroutineFeignTest { internal class TestInterfaceAsyncBuilder { private val delegate = CoroutineFeign.builder() .decoder(DefaultDecoder()).encoder { `object`, bodyType, template -> - if (`object` is Map<*, *>) { - template.body(Request.Body.of(Gson().toJson(`object`))) - } else { - template.body(Request.Body.of(`object`.toString())) - } + if (`object` is Map<*, *>) { + template.body(Request.Body.of(Gson().toJson(`object`))); + } else { + template.body(Request.Body.of(`object`.toString())); + } + return@encoder true; } fun requestInterceptor(requestInterceptor: RequestInterceptor?): TestInterfaceAsyncBuilder { diff --git a/micrometer/src/main/java/feign/micrometer/MeteredEncoder.java b/micrometer/src/main/java/feign/micrometer/MeteredEncoder.java index 784aa86c3..49442aa42 100644 --- a/micrometer/src/main/java/feign/micrometer/MeteredEncoder.java +++ b/micrometer/src/main/java/feign/micrometer/MeteredEncoder.java @@ -53,16 +53,18 @@ public MeteredEncoder( } @Override - public void encode(Object object, Type bodyType, RequestTemplate template) + public boolean encode(Object object, Type bodyType, RequestTemplate template) throws EncodeException { - createTimer(object, bodyType, template) - .record(() -> encoder.encode(object, bodyType, template)); + boolean isEncoded = + createTimer(object, bodyType, template) + .record(() -> encoder.encode(object, bodyType, template)); template.headers().getOrDefault(CONTENT_LENGTH, Collections.emptySet()).stream() .findFirst() .ifPresent( contentLength -> createSummary(object, bodyType, template).record(Long.parseLong(contentLength))); + return isEncoded; } protected Timer createTimer(Object object, Type bodyType, RequestTemplate template) { diff --git a/moshi/src/main/java/feign/moshi/MoshiEncoder.java b/moshi/src/main/java/feign/moshi/MoshiEncoder.java index fdd750774..bbf672de6 100644 --- a/moshi/src/main/java/feign/moshi/MoshiEncoder.java +++ b/moshi/src/main/java/feign/moshi/MoshiEncoder.java @@ -19,6 +19,7 @@ import com.squareup.moshi.Moshi; import feign.Request; import feign.RequestTemplate; +import feign.Util; import feign.codec.Encoder; import feign.codec.JsonEncoder; import java.lang.reflect.Type; @@ -40,8 +41,12 @@ public MoshiEncoder(Iterable> adapters) { } @Override - public void encode(Object object, Type bodyType, RequestTemplate template) { + public boolean encode(Object object, Type bodyType, RequestTemplate template) { + if (!Util.isJsonContentType(template)) { + return false; + } JsonAdapter jsonAdapter = moshi.adapter(bodyType).indent(" "); template.body(Request.Body.of(jsonAdapter.toJson(object))); + return true; } } diff --git a/moshi/src/test/java/feign/moshi/MoshiEncoderTest.java b/moshi/src/test/java/feign/moshi/MoshiEncoderTest.java index 2a76d7597..84ce9de83 100644 --- a/moshi/src/test/java/feign/moshi/MoshiEncoderTest.java +++ b/moshi/src/test/java/feign/moshi/MoshiEncoderTest.java @@ -36,6 +36,7 @@ void encodesMapObjectNumericalValuesAsInteger() { map.put("foo", 1); RequestTemplate template = new RequestTemplate(); + template.header("Content-Type", "application/json"); new MoshiEncoder().encode(map, Map.class, template); assertThat(template) @@ -55,6 +56,7 @@ void encodesFormParams() { form.put("bar", Arrays.asList(2, 3)); RequestTemplate template = new RequestTemplate(); + template.header("Content-Type", "application/json"); new MoshiEncoder().encode(form, Map.class, template); @@ -82,6 +84,7 @@ void customEncoder() { zones.add(new Zone("denominator.io.", "abcd")); RequestTemplate template = new RequestTemplate(); + template.header("Content-Type", "application/json"); encoder.encode(zones, UpperZoneJSONAdapter.class, template); assertThat(template) @@ -107,6 +110,7 @@ void customObjectEncoder() { VideoGame videoGame = new VideoGame("Super Mario", "Luigi", "Bowser"); RequestTemplate template = new RequestTemplate(); + template.header("Content-Type", "application/json"); encoder.encode(videoGame, videoGameJsonAdapter.getClass(), template); assertThat(template) diff --git a/okhttp/src/test/java/feign/okhttp/OkHttpClientAsyncTest.java b/okhttp/src/test/java/feign/okhttp/OkHttpClientAsyncTest.java index d923c3f10..e237c766d 100644 --- a/okhttp/src/test/java/feign/okhttp/OkHttpClientAsyncTest.java +++ b/okhttp/src/test/java/feign/okhttp/OkHttpClientAsyncTest.java @@ -168,8 +168,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()); @@ -1035,6 +1036,7 @@ static final class TestInterfaceAsyncBuilder { } else { template.body(Request.Body.of(object.toString())); } + return true; }); TestInterfaceAsyncBuilder requestInterceptor(RequestInterceptor requestInterceptor) { diff --git a/pom.xml b/pom.xml index f85a2977d..78c1b4046 100644 --- a/pom.xml +++ b/pom.xml @@ -1005,6 +1005,36 @@ feign.mock.RequestKey feign.vertx.VertxHttpClient + + feign.moshi.MoshiEncoder + + feign.gson.GsonEncoder + + feign.jackson.jaxb.JacksonJaxbJsonEncoder + + feign.jackson.jr.JacksonJrEncoder + + feign.jackson3.Jackson3Encoder + + feign.jackson.JacksonEncoder + + feign.jaxb.JAXBEncoder + + feign.fastjson2.Fastjson2Encoder + + feign.soap.SOAPEncoder + + feign.form.FormEncoder + + feign.json.JsonEncoder + + feign.micrometer.MeteredEncoder + + feign.metrics4.MeteredEncoder + + feign.metrics5.MeteredEncoder + + feign.form.spring.SpringFormEncoder public diff --git a/soap-jakarta/src/main/java/feign/soap/SOAPEncoder.java b/soap-jakarta/src/main/java/feign/soap/SOAPEncoder.java index 31bc76830..97ab5a666 100644 --- a/soap-jakarta/src/main/java/feign/soap/SOAPEncoder.java +++ b/soap-jakarta/src/main/java/feign/soap/SOAPEncoder.java @@ -17,6 +17,7 @@ import feign.Request; import feign.RequestTemplate; +import feign.Util; import feign.codec.EncodeException; import feign.codec.Encoder; import feign.jaxb.JAXBContextFactory; @@ -110,7 +111,10 @@ public SOAPEncoder(JAXBContextFactory jaxbContextFactory) { } @Override - public void encode(Object object, Type bodyType, RequestTemplate template) { + public boolean encode(Object object, Type bodyType, RequestTemplate template) { + if (!Util.isXmlContentType(template)) { + return false; + } if (!(bodyType instanceof Class)) { throw new UnsupportedOperationException( "SOAP only supports encoding raw types. Found " + bodyType); @@ -137,6 +141,7 @@ public void encode(Object object, Type bodyType, RequestTemplate template) { soapMessage.writeTo(bos); } template.body(Request.Body.of(bos.toByteArray())); + return true; } catch (SOAPException | JAXBException | ParserConfigurationException diff --git a/soap-jakarta/src/test/java/feign/soap/SOAPCodecTest.java b/soap-jakarta/src/test/java/feign/soap/SOAPCodecTest.java index f267625c5..b23d9ae9d 100644 --- a/soap-jakarta/src/test/java/feign/soap/SOAPCodecTest.java +++ b/soap-jakarta/src/test/java/feign/soap/SOAPCodecTest.java @@ -56,6 +56,7 @@ void encodesSoap() { mock.item.value = "Apples"; RequestTemplate template = new RequestTemplate(); + template.header("Content-Type", "text/xml"); encoder.encode(mock, GetPrice.class, template); String soapEnvelop = @@ -84,6 +85,7 @@ class ParameterizedHolder { Type parameterized = ParameterizedHolder.class.getDeclaredField("field").getGenericType(); RequestTemplate template = new RequestTemplate(); + template.header("Content-Type", "text/xml"); Throwable exception = assertThatExceptionOfType(UnsupportedOperationException.class) .isThrownBy( @@ -113,6 +115,7 @@ void encodesSoapWithCustomJAXBMarshallerEncoding() { mock.item.value = "Apples"; RequestTemplate template = new RequestTemplate(); + template.header("Content-Type", "text/xml"); encoder.encode(mock, GetPrice.class, template); String soapEnvelop = @@ -145,6 +148,7 @@ void encodesSoapWithCustomJAXBSchemaLocation() { mock.item.value = "Apples"; RequestTemplate template = new RequestTemplate(); + template.header("Content-Type", "text/xml"); encoder.encode(mock, GetPrice.class, template); assertThat(template) @@ -176,6 +180,7 @@ void encodesSoapWithCustomJAXBNoSchemaLocation() { mock.item.value = "Apples"; RequestTemplate template = new RequestTemplate(); + template.header("Content-Type", "text/xml"); encoder.encode(mock, GetPrice.class, template); assertThat(template) @@ -206,6 +211,7 @@ void encodesSoapWithCustomJAXBFormattedOuput() { mock.item.value = "Apples"; RequestTemplate template = new RequestTemplate(); + template.header("Content-Type", "text/xml"); encoder.encode(mock, GetPrice.class, template); assertThat(template) @@ -395,6 +401,7 @@ void decodeAnnotatedParameterizedTypes() throws Exception { Box> boxBoxStr = new Box<>(); boxBoxStr.set(boxStr); RequestTemplate template = new RequestTemplate(); + template.header("Content-Type", "text/xml"); encoder.encode(boxBoxStr, Box.class, template); Response response = @@ -436,6 +443,7 @@ void changeSoapProtocolAndSetHeader() { mock.item.value = "Apples"; RequestTemplate template = new RequestTemplate(); + template.header("Content-Type", "text/xml"); encoder.encode(mock, GetPrice.class, template); String soapEnvelop = diff --git a/soap/src/main/java/feign/soap/SOAPEncoder.java b/soap/src/main/java/feign/soap/SOAPEncoder.java index 711c44171..9f4017071 100644 --- a/soap/src/main/java/feign/soap/SOAPEncoder.java +++ b/soap/src/main/java/feign/soap/SOAPEncoder.java @@ -17,6 +17,7 @@ import feign.Request; import feign.RequestTemplate; +import feign.Util; import feign.codec.EncodeException; import feign.codec.Encoder; import feign.jaxb.JAXBContextFactory; @@ -110,7 +111,10 @@ public SOAPEncoder(JAXBContextFactory jaxbContextFactory) { } @Override - public void encode(Object object, Type bodyType, RequestTemplate template) { + public boolean encode(Object object, Type bodyType, RequestTemplate template) { + if (!Util.isXmlContentType(template)) { + return false; + } if (!(bodyType instanceof Class)) { throw new UnsupportedOperationException( "SOAP only supports encoding raw types. Found " + bodyType); @@ -137,6 +141,7 @@ public void encode(Object object, Type bodyType, RequestTemplate template) { soapMessage.writeTo(bos); } template.body(Request.Body.of(bos.toByteArray())); + return true; } catch (SOAPException | JAXBException | ParserConfigurationException diff --git a/soap/src/test/java/feign/soap/SOAPCodecTest.java b/soap/src/test/java/feign/soap/SOAPCodecTest.java index 34274583e..68a5b240f 100644 --- a/soap/src/test/java/feign/soap/SOAPCodecTest.java +++ b/soap/src/test/java/feign/soap/SOAPCodecTest.java @@ -56,6 +56,7 @@ void encodesSoap() { mock.item.value = "Apples"; RequestTemplate template = new RequestTemplate(); + template.header("Content-Type", "text/xml"); encoder.encode(mock, GetPrice.class, template); String soapEnvelop = @@ -84,6 +85,7 @@ class ParameterizedHolder { Type parameterized = ParameterizedHolder.class.getDeclaredField("field").getGenericType(); RequestTemplate template = new RequestTemplate(); + template.header("Content-Type", "text/xml"); Throwable exception = assertThatExceptionOfType(UnsupportedOperationException.class) .isThrownBy( @@ -113,6 +115,7 @@ void encodesSoapWithCustomJAXBMarshallerEncoding() { mock.item.value = "Apples"; RequestTemplate template = new RequestTemplate(); + template.header("Content-Type", "text/xml"); encoder.encode(mock, GetPrice.class, template); String soapEnvelop = @@ -145,6 +148,7 @@ void encodesSoapWithCustomJAXBSchemaLocation() { mock.item.value = "Apples"; RequestTemplate template = new RequestTemplate(); + template.header("Content-Type", "text/xml"); encoder.encode(mock, GetPrice.class, template); assertThat(template) @@ -176,6 +180,7 @@ void encodesSoapWithCustomJAXBNoSchemaLocation() { mock.item.value = "Apples"; RequestTemplate template = new RequestTemplate(); + template.header("Content-Type", "text/xml"); encoder.encode(mock, GetPrice.class, template); assertThat(template) @@ -206,6 +211,7 @@ void encodesSoapWithCustomJAXBFormattedOuput() { mock.item.value = "Apples"; RequestTemplate template = new RequestTemplate(); + template.header("Content-Type", "text/xml"); encoder.encode(mock, GetPrice.class, template); assertThat(template) @@ -405,6 +411,7 @@ void decodeAnnotatedParameterizedTypes() throws Exception { Box> boxBoxStr = new Box<>(); boxBoxStr.set(boxStr); RequestTemplate template = new RequestTemplate(); + template.header("Content-Type", "text/xml"); encoder.encode(boxBoxStr, Box.class, template); Response response = @@ -446,6 +453,7 @@ void changeSoapProtocolAndSetHeader() { mock.item.value = "Apples"; RequestTemplate template = new RequestTemplate(); + template.header("Content-Type", "text/xml"); encoder.encode(mock, GetPrice.class, template); String soapEnvelop = diff --git a/spring/src/test/java/feign/spring/SpringContractTest.java b/spring/src/test/java/feign/spring/SpringContractTest.java index b105ce25d..0aba410bb 100755 --- a/spring/src/test/java/feign/spring/SpringContractTest.java +++ b/spring/src/test/java/feign/spring/SpringContractTest.java @@ -348,7 +348,10 @@ void checkWithName( @RequestMapping(value = "/withRequiredRequestBody", method = RequestMethod.POST) void checkWithRequiredRequestBody(@RequestBody() UserObject obj); - @RequestMapping(value = "/part/{id}", method = RequestMethod.POST) + @RequestMapping( + value = "/part/{id}", + method = RequestMethod.POST, + consumes = MediaType.APPLICATION_JSON_VALUE) void checkRequestPart( @PathVariable(name = "id") String campaignId, @RequestPart(name = "name1") String name, diff --git a/validation-jakarta/src/test/java/feign/validation/BeanValidationMethodInterceptorTest.java b/validation-jakarta/src/test/java/feign/validation/BeanValidationMethodInterceptorTest.java index 970748ae7..b01243058 100644 --- a/validation-jakarta/src/test/java/feign/validation/BeanValidationMethodInterceptorTest.java +++ b/validation-jakarta/src/test/java/feign/validation/BeanValidationMethodInterceptorTest.java @@ -84,7 +84,11 @@ interface Api { private Api api() { return Feign.builder() - .encoder((object, _, template) -> template.body(Request.Body.of(String.valueOf(object)))) + .encoder( + (object, _, template) -> { + template.body(Request.Body.of(String.valueOf(object))); + return true; + }) .methodInterceptor(BeanValidationMethodInterceptor.usingDefaultFactory()) .target(Api.class, "http://localhost:" + server.getPort()); } diff --git a/validation/src/test/java/feign/validation/BeanValidationMethodInterceptorTest.java b/validation/src/test/java/feign/validation/BeanValidationMethodInterceptorTest.java index c9155611c..1fe04d256 100644 --- a/validation/src/test/java/feign/validation/BeanValidationMethodInterceptorTest.java +++ b/validation/src/test/java/feign/validation/BeanValidationMethodInterceptorTest.java @@ -84,7 +84,11 @@ interface Api { private Api api() { return Feign.builder() - .encoder((object, _, template) -> template.body(Request.Body.of(String.valueOf(object)))) + .encoder( + (object, _, template) -> { + template.body(Request.Body.of(String.valueOf(object))); + return true; + }) .methodInterceptor(BeanValidationMethodInterceptor.usingDefaultFactory()) .target(Api.class, "http://localhost:" + server.getPort()); }