From cf283717787ecc57a3ca4ca0a28a1d3ad1e5bb31 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 26 Jun 2026 05:15:35 +0000 Subject: [PATCH 01/63] build(deps-dev): Bump io.undertow:undertow-core Bumps [io.undertow:undertow-core](https://github.com/undertow-io/undertow) from 2.4.1.Final to 2.4.2.Final. - [Release notes](https://github.com/undertow-io/undertow/releases) - [Commits](https://github.com/undertow-io/undertow/compare/2.4.1.Final...2.4.2.Final) --- updated-dependencies: - dependency-name: io.undertow:undertow-core dependency-version: 2.4.2.Final dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 279862715..84a6a4846 100644 --- a/pom.xml +++ b/pom.xml @@ -254,7 +254,7 @@ 2025.1.2 5.0.2 7.0.8 - 2.4.1.Final + 2.4.2.Final 1.18.0 From 46d604fad649b40c9593c2796fe4853dd44e7cfb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Jun 2026 05:14:22 +0000 Subject: [PATCH 02/63] build(deps): Bump org.junit:junit-bom from 6.1.0 to 6.1.1 Bumps [org.junit:junit-bom](https://github.com/junit-team/junit-framework) from 6.1.0 to 6.1.1. - [Release notes](https://github.com/junit-team/junit-framework/releases) - [Commits](https://github.com/junit-team/junit-framework/compare/r6.1.0...r6.1.1) --- updated-dependencies: - dependency-name: org.junit:junit-bom dependency-version: 6.1.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 84a6a4846..3783c908f 100644 --- a/pom.xml +++ b/pom.xml @@ -173,7 +173,7 @@ 20260522 4.1.0 - 6.1.0 + 6.1.1 2.22.0 3.2.0 3.27.7 From 68815c8dbda0597deece78639cdefe16a2b58ea1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 30 Jun 2026 05:12:23 +0000 Subject: [PATCH 03/63] build(deps): Bump org.apache.httpcomponents.client5:httpclient5 Bumps [org.apache.httpcomponents.client5:httpclient5](https://github.com/apache/httpcomponents-client) from 5.6.1 to 5.6.2. - [Changelog](https://github.com/apache/httpcomponents-client/blob/rel/v5.6.2/RELEASE_NOTES.txt) - [Commits](https://github.com/apache/httpcomponents-client/compare/rel/v5.6.1...rel/v5.6.2) --- updated-dependencies: - dependency-name: org.apache.httpcomponents.client5:httpclient5 dependency-version: 5.6.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 3783c908f..66651c98e 100644 --- a/pom.xml +++ b/pom.xml @@ -222,7 +222,7 @@ 26.0 4.5.2 4.5.14 - 5.6.1 + 5.6.2 1.13.0 1.5.18 3.1.0 From e2ce674637dee19b648dad1ee87a092c4d513dc6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 30 Jun 2026 05:13:06 +0000 Subject: [PATCH 04/63] build(deps): Bump com.gradle:common-custom-user-data-maven-extension Bumps [com.gradle:common-custom-user-data-maven-extension](https://github.com/gradle/common-custom-user-data-maven-extension) from 2.2.0 to 2.3.0. - [Release notes](https://github.com/gradle/common-custom-user-data-maven-extension/releases) - [Commits](https://github.com/gradle/common-custom-user-data-maven-extension/compare/v2.2.0...v2.3.0) --- updated-dependencies: - dependency-name: com.gradle:common-custom-user-data-maven-extension dependency-version: 2.3.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .mvn/extensions.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.mvn/extensions.xml b/.mvn/extensions.xml index bfbb33c26..827961c11 100644 --- a/.mvn/extensions.xml +++ b/.mvn/extensions.xml @@ -24,6 +24,6 @@ com.gradle common-custom-user-data-maven-extension - 2.2.0 + 2.3.0 From 0181b6934b2ea291809e7d2bcba826bbc8ee0d5e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 30 Jun 2026 05:13:31 +0000 Subject: [PATCH 05/63] build(deps): Bump com.google.http-client:google-http-client Bumps [com.google.http-client:google-http-client](https://github.com/googleapis/google-http-java-client) from 2.1.0 to 2.1.1. - [Release notes](https://github.com/googleapis/google-http-java-client/releases) - [Changelog](https://github.com/googleapis/google-http-java-client/blob/main/CHANGELOG.md) - [Commits](https://github.com/googleapis/google-http-java-client/compare/v2.1.0...v2.1.1) --- updated-dependencies: - dependency-name: com.google.http-client:google-http-client dependency-version: 2.1.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 3783c908f..684b96dff 100644 --- a/pom.xml +++ b/pom.xml @@ -166,7 +166,7 @@ 5.4.0 33.6.0-jre - 2.1.0 + 2.1.1 2.14.0 1.15.2 2.0.18 From b6474fa87fb24442a19cd92a1742107340aa01a5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Jul 2026 05:13:02 +0000 Subject: [PATCH 06/63] build(deps): Bump com.github.jknack:handlebars from 4.5.2 to 4.5.3 Bumps [com.github.jknack:handlebars](https://github.com/jknack/handlebars.java) from 4.5.2 to 4.5.3. - [Release notes](https://github.com/jknack/handlebars.java/releases) - [Commits](https://github.com/jknack/handlebars.java/compare/v4.5.2...v4.5.3) --- updated-dependencies: - dependency-name: com.github.jknack:handlebars dependency-version: 4.5.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 0f32bc294..bbd400637 100644 --- a/pom.xml +++ b/pom.xml @@ -220,7 +220,7 @@ 1.15.0 0.23.0 26.0 - 4.5.2 + 4.5.3 4.5.14 5.6.2 1.13.0 From 7446c1fe38eb5fc873635e07861ba152cabf10bd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Jul 2026 05:13:40 +0000 Subject: [PATCH 07/63] build(deps-dev): Bump com.gradle:develocity-maven-extension Bumps com.gradle:develocity-maven-extension from 2.4.2 to 2.5.0. --- updated-dependencies: - dependency-name: com.gradle:develocity-maven-extension dependency-version: 2.5.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .mvn/extensions.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.mvn/extensions.xml b/.mvn/extensions.xml index 827961c11..2ce7b7f8a 100644 --- a/.mvn/extensions.xml +++ b/.mvn/extensions.xml @@ -19,7 +19,7 @@ com.gradle develocity-maven-extension - 2.4.2 + 2.5.0 com.gradle From 00567950adf3e67ccf438c712f4d63572cc8833a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 2 Jul 2026 05:12:19 +0000 Subject: [PATCH 08/63] build(deps): Bump org.openrewrite.recipe:rewrite-migrate-java Bumps [org.openrewrite.recipe:rewrite-migrate-java](https://github.com/openrewrite/rewrite-migrate-java) from 3.38.0 to 3.39.0. - [Release notes](https://github.com/openrewrite/rewrite-migrate-java/releases) - [Commits](https://github.com/openrewrite/rewrite-migrate-java/compare/v3.38.0...v3.39.0) --- updated-dependencies: - dependency-name: org.openrewrite.recipe:rewrite-migrate-java dependency-version: 3.39.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index bbd400637..44a09f9bc 100644 --- a/pom.xml +++ b/pom.xml @@ -207,7 +207,7 @@ 4.0.0 6.42.0 3.40.0 - 3.38.0 + 3.39.0 0.26.1 1.0 From f10e817bcc038c2b47830e227fe2653ac256a60e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 2 Jul 2026 05:13:22 +0000 Subject: [PATCH 09/63] build(deps-dev): Bump org.openrewrite.maven:rewrite-maven-plugin Bumps [org.openrewrite.maven:rewrite-maven-plugin](https://github.com/openrewrite/rewrite-maven-plugin) from 6.42.0 to 6.43.0. - [Release notes](https://github.com/openrewrite/rewrite-maven-plugin/releases) - [Commits](https://github.com/openrewrite/rewrite-maven-plugin/compare/v6.42.0...v6.43.0) --- updated-dependencies: - dependency-name: org.openrewrite.maven:rewrite-maven-plugin dependency-version: 6.43.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index bbd400637..d98194b90 100644 --- a/pom.xml +++ b/pom.xml @@ -205,7 +205,7 @@ 3.2.0 1.2.8 4.0.0 - 6.42.0 + 6.43.0 3.40.0 3.38.0 0.26.1 From 935954a5fbf8e29507744fc9d2ca5c1fd6ad2534 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 2 Jul 2026 05:24:42 +0000 Subject: [PATCH 10/63] build(deps): Bump org.openrewrite.recipe:rewrite-testing-frameworks Bumps [org.openrewrite.recipe:rewrite-testing-frameworks](https://github.com/openrewrite/rewrite-testing-frameworks) from 3.40.0 to 3.41.0. - [Release notes](https://github.com/openrewrite/rewrite-testing-frameworks/releases) - [Commits](https://github.com/openrewrite/rewrite-testing-frameworks/compare/v3.40.0...v3.41.0) --- updated-dependencies: - dependency-name: org.openrewrite.recipe:rewrite-testing-frameworks dependency-version: 3.41.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 1091737f5..f10551e8f 100644 --- a/pom.xml +++ b/pom.xml @@ -206,7 +206,7 @@ 1.2.8 4.0.0 6.43.0 - 3.40.0 + 3.41.0 3.39.0 0.26.1 1.0 From a527c35567f5d1fdf145bdbf4caba956dae83dd9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 7 Jul 2026 05:13:05 +0000 Subject: [PATCH 11/63] build(deps): Bump netty.version from 4.2.15.Final to 4.2.16.Final Bumps `netty.version` from 4.2.15.Final to 4.2.16.Final. Updates `io.netty:netty-bom` from 4.2.15.Final to 4.2.16.Final - [Release notes](https://github.com/netty/netty/releases) - [Commits](https://github.com/netty/netty/compare/netty-4.2.15.Final...netty-4.2.16.Final) Updates `io.netty:netty-handler` from 4.2.15.Final to 4.2.16.Final - [Release notes](https://github.com/netty/netty/releases) - [Commits](https://github.com/netty/netty/compare/netty-4.2.15.Final...netty-4.2.16.Final) Updates `io.netty:netty-codec-http` from 4.2.15.Final to 4.2.16.Final - [Release notes](https://github.com/netty/netty/releases) - [Commits](https://github.com/netty/netty/compare/netty-4.2.15.Final...netty-4.2.16.Final) --- updated-dependencies: - dependency-name: io.netty:netty-bom dependency-version: 4.2.16.Final dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: io.netty:netty-handler dependency-version: 4.2.16.Final dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: io.netty:netty-codec-http dependency-version: 4.2.16.Final dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- benchmark/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/benchmark/pom.xml b/benchmark/pom.xml index 111421129..74bbbb22e 100644 --- a/benchmark/pom.xml +++ b/benchmark/pom.xml @@ -32,7 +32,7 @@ 1.37 0.5.3 1.3.8 - 4.2.15.Final + 4.2.16.Final true From 6b0a283d2bfd241753984b05387e47add7e34d68 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 8 Jul 2026 05:12:41 +0000 Subject: [PATCH 12/63] build(deps): Bump jackson.version from 2.22.0 to 2.22.1 Bumps `jackson.version` from 2.22.0 to 2.22.1. Updates `com.fasterxml.jackson:jackson-bom` from 2.22.0 to 2.22.1 - [Commits](https://github.com/FasterXML/jackson-bom/compare/jackson-bom-2.22.0...jackson-bom-2.22.1) Updates `com.fasterxml.jackson.jaxrs:jackson-jaxrs-json-provider` from 2.22.0 to 2.22.1 --- updated-dependencies: - dependency-name: com.fasterxml.jackson:jackson-bom dependency-version: 2.22.1 dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: com.fasterxml.jackson.jaxrs:jackson-jaxrs-json-provider dependency-version: 2.22.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- vertx/feign-vertx/pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index f10551e8f..7ed867753 100644 --- a/pom.xml +++ b/pom.xml @@ -174,7 +174,7 @@ 4.1.0 6.1.1 - 2.22.0 + 2.22.1 3.2.0 3.27.7 5.23.0 diff --git a/vertx/feign-vertx/pom.xml b/vertx/feign-vertx/pom.xml index 5dc883e66..9e4323e8e 100644 --- a/vertx/feign-vertx/pom.xml +++ b/vertx/feign-vertx/pom.xml @@ -31,7 +31,7 @@ 11 - 2.22.0 + 2.22.1 From 5f749db1f9ea9c5007ac456d92af0e0212834025 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 8 Jul 2026 05:13:27 +0000 Subject: [PATCH 13/63] build(deps-dev): Bump vertx.version in /vertx/feign-vertx5-test Bumps `vertx.version` from 5.1.3 to 5.1.4. Updates `io.vertx:vertx-junit5` from 5.1.3 to 5.1.4 - [Commits](https://github.com/eclipse-vertx/vertx-junit5/compare/5.1.3...5.1.4) Updates `io.vertx:vertx-web-client` from 5.1.3 to 5.1.4 - [Commits](https://github.com/vert-x3/vertx-web/compare/5.1.3...5.1.4) --- updated-dependencies: - dependency-name: io.vertx:vertx-junit5 dependency-version: 5.1.4 dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: io.vertx:vertx-web-client dependency-version: 5.1.4 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- vertx/feign-vertx5-test/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vertx/feign-vertx5-test/pom.xml b/vertx/feign-vertx5-test/pom.xml index 2798249ac..b18333fd8 100644 --- a/vertx/feign-vertx5-test/pom.xml +++ b/vertx/feign-vertx5-test/pom.xml @@ -30,7 +30,7 @@ Tests with Vertx 5.x. - 5.1.3 + 5.1.4 From cbb53a6b1172eb5023481a090ee08a645e5bbc1f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 10 Jul 2026 05:13:25 +0000 Subject: [PATCH 14/63] build(deps-dev): Bump vertx.version in /vertx/feign-vertx4-test Bumps `vertx.version` from 4.5.28 to 4.5.29. Updates `io.vertx:vertx-junit5` from 4.5.28 to 4.5.29 - [Commits](https://github.com/eclipse-vertx/vertx-junit5/compare/4.5.28...4.5.29) Updates `io.vertx:vertx-web-client` from 4.5.28 to 4.5.29 - [Commits](https://github.com/vert-x3/vertx-web/compare/4.5.28...4.5.29) --- updated-dependencies: - dependency-name: io.vertx:vertx-junit5 dependency-version: 4.5.29 dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: io.vertx:vertx-web-client dependency-version: 4.5.29 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- vertx/feign-vertx4-test/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vertx/feign-vertx4-test/pom.xml b/vertx/feign-vertx4-test/pom.xml index f3467662a..f2f3ce617 100644 --- a/vertx/feign-vertx4-test/pom.xml +++ b/vertx/feign-vertx4-test/pom.xml @@ -30,7 +30,7 @@ Tests with Vertx 4.x. - 4.5.28 + 4.5.29 From 7965183072e1d2d7d2b6918e64969edc60c21ea7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Jul 2026 05:14:34 +0000 Subject: [PATCH 15/63] build(deps): Bump tools.jackson:jackson-bom from 3.2.0 to 3.2.1 Bumps [tools.jackson:jackson-bom](https://github.com/FasterXML/jackson-bom) from 3.2.0 to 3.2.1. - [Commits](https://github.com/FasterXML/jackson-bom/compare/jackson-bom-3.2.0...jackson-bom-3.2.1) --- updated-dependencies: - dependency-name: tools.jackson:jackson-bom dependency-version: 3.2.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 7ed867753..53e3f039e 100644 --- a/pom.xml +++ b/pom.xml @@ -175,7 +175,7 @@ 6.1.1 2.22.1 - 3.2.0 + 3.2.1 3.27.7 5.23.0 2.0.61.android8 From 710dab7383620c9384e931eb82f5046590bba9e0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Jul 2026 05:15:21 +0000 Subject: [PATCH 16/63] build(deps): Bump org.junit:junit-bom from 6.1.1 to 6.1.2 Bumps [org.junit:junit-bom](https://github.com/junit-team/junit-framework) from 6.1.1 to 6.1.2. - [Release notes](https://github.com/junit-team/junit-framework/releases) - [Commits](https://github.com/junit-team/junit-framework/compare/r6.1.1...r6.1.2) --- updated-dependencies: - dependency-name: org.junit:junit-bom dependency-version: 6.1.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 7ed867753..5b496a507 100644 --- a/pom.xml +++ b/pom.xml @@ -173,7 +173,7 @@ 20260522 4.1.0 - 6.1.1 + 6.1.2 2.22.1 3.2.0 3.27.7 From 2f548d5662eff675345cb52aa24516f8487f038b Mon Sep 17 00:00:00 2001 From: Yevhen Vasyliev Date: Mon, 13 Jul 2026 18:28:08 +0300 Subject: [PATCH 17/63] feat: allow registering multiple encoders in Feign Co-authored-by: trumpetinc <6618744+trumpetinc@users.noreply.github.com> --- README.md | 18 ++-- api/pom.xml | 2 +- api/src/main/java/feign/AsyncFeign.java | 31 +++++++ api/src/main/java/feign/BaseBuilder.java | 34 ++++++- api/src/main/java/feign/Feign.java | 31 +++++++ .../feign/RequestTemplateFactoryResolver.java | 23 ++++- .../java/feign/codec/DelegatingEncoder.java | 89 +++++++++++++++++++ api/src/main/java/feign/codec/Encoder.java | 14 +++ .../main/java/feign/codec/JsonEncoder.java | 28 +++++- api/src/main/java/feign/codec/XmlEncoder.java | 47 ++++++++++ .../RequestTemplateFactoryResolverTest.java | 79 ++++++++++++++++ .../feign/codec/DelegatingEncoderTest.java | 79 ++++++++++++++++ .../example/github/GitHubExample.java | 2 +- .../java/feign/core/codec/DefaultEncoder.java | 19 ++++ .../feign/AlwaysEncodeBodyContractTest.java | 16 +++- core/src/test/java/feign/AsyncFeignTest.java | 41 +++++++-- core/src/test/java/feign/BaseApiTest.java | 20 ++++- .../src/test/java/feign/FeignBuilderTest.java | 20 ++++- core/src/test/java/feign/FeignTest.java | 41 +++++++-- .../test/java/feign/FeignUnderAsyncTest.java | 41 +++++++-- .../feign/MethodMetadataPresenceTest.java | 25 ++++-- .../feign/core/codec/DefaultEncoderTest.java | 27 ++++++ .../java/feign/metrics4/MeteredEncoder.java | 13 +++ .../java/feign/metrics5/MeteredEncoder.java | 13 +++ .../main/java/example/github/GitHubExample.kt | 2 +- .../java/example/github/GitHubExample.java | 2 +- fastjson2/README.md | 2 +- .../java/feign/form/feign/spring/Client.java | 4 +- .../feign/spring/SpringFormEncoderTest.java | 3 + .../src/main/java/feign/form/FormEncoder.java | 13 +++ .../test/java/feign/form/BasicClientTest.java | 2 +- .../java/feign/form/ByteArrayClientTest.java | 2 +- .../java/feign/form/CustomClientTest.java | 2 +- .../java/feign/form/FormPropertyTest.java | 2 +- .../feign/form/MultipartBoundaryTest.java | 14 ++- .../UrlencodedFormContentProcessorTest.java | 2 +- .../test/java/feign/form/WildCardMapTest.java | 2 +- .../java/feign/form/issues/Issue63Test.java | 2 +- .../form/multipart/DelegateWriterTest.java | 32 ++++++- .../java/feign/graphql/GraphqlEncoder.java | 13 +++ gson/README.md | 2 +- .../feign/hc5/AsyncApacheHttp5ClientTest.java | 41 +++++++-- .../main/java/feign/hystrix/HystrixFeign.java | 31 +++++++ jackson-jaxb/README.md | 4 +- .../jackson/jaxb/JacksonJaxbJsonEncoder.java | 4 +- jackson-jr/README.md | 6 +- .../feign/jackson/jr/JacksonJrEncoder.java | 3 +- jackson/README.md | 2 +- jackson3/README.md | 2 +- .../test/Http2ClientAsyncTest.java | 41 +++++++-- .../src/main/java/feign/jaxb/JAXBEncoder.java | 6 +- jaxb/README.md | 2 +- .../src/main/java/feign/jaxb/JAXBEncoder.java | 6 +- json/README.md | 2 +- .../src/main/java/feign/json/JsonEncoder.java | 15 +++- .../test/java/feign/json/JsonCodecTest.java | 2 +- .../java/feign/kotlin/CoroutineFeign.java | 2 +- .../kotlin/feign/kotlin/CoroutineFeignTest.kt | 21 +++-- .../java/feign/micrometer/MeteredEncoder.java | 13 +++ moshi/README.md | 2 +- .../feign/moshi/examples/GithubExample.java | 2 +- .../feign/okhttp/OkHttpClientAsyncTest.java | 41 +++++++-- .../ReactiveFeignIntegrationTest.java | 4 +- soap-jakarta/README.md | 2 +- .../src/main/java/feign/soap/SOAPEncoder.java | 6 +- soap/README.md | 2 +- .../src/main/java/feign/soap/SOAPEncoder.java | 6 +- .../java/feign/spring/SpringContractTest.java | 9 +- .../BeanValidationMethodInterceptorTest.java | 18 +++- .../BeanValidationMethodInterceptorTest.java | 18 +++- vertx/README.md | 2 +- .../src/main/java/feign/VertxFeign.java | 35 +++++++- .../feign/vertx/ConnectionsLeakTests.java | 4 +- .../vertx/Http11ClientReconnectTest.java | 2 +- .../feign/vertx/Http2ClientReconnectTest.java | 2 +- .../java/feign/vertx/RawContractTest.java | 2 +- .../java/feign/vertx/VertxHttpClientTest.java | 2 +- .../feign/vertx/ConnectionsLeakTests.java | 4 +- .../vertx/Http11ClientReconnectTest.java | 2 +- .../feign/vertx/Http2ClientReconnectTest.java | 2 +- .../java/feign/vertx/RawContractTest.java | 2 +- .../java/feign/vertx/VertxHttpClientTest.java | 2 +- 82 files changed, 1063 insertions(+), 165 deletions(-) create mode 100644 api/src/main/java/feign/codec/DelegatingEncoder.java create mode 100644 api/src/main/java/feign/codec/XmlEncoder.java create mode 100644 api/src/test/java/feign/RequestTemplateFactoryResolverTest.java create mode 100644 api/src/test/java/feign/codec/DelegatingEncoderTest.java diff --git a/README.md b/README.md index cbaae1076..ac1a83ca3 100644 --- a/README.md +++ b/README.md @@ -386,7 +386,7 @@ You can also configure the encoder and decoder separately: ```java GitHub github = Feign.builder() - .encoder(new GsonEncoder()) + .encoders(new GsonEncoder()) .decoder(new GsonDecoder()) .target(GitHub.class, "https://api.github.com"); ``` @@ -404,7 +404,7 @@ You can also configure the encoder and decoder separately: ```java GitHub github = Feign.builder() - .encoder(new JacksonEncoder()) + .encoders(new JacksonEncoder()) .decoder(new JacksonDecoder()) .target(GitHub.class, "https://api.github.com"); ``` @@ -425,7 +425,7 @@ You can also configure the encoder and decoder separately: ```java GitHub github = Feign.builder() - .encoder(new MoshiEncoder()) + .encoders(new MoshiEncoder()) .decoder(new MoshiDecoder()) .target(GitHub.class, "https://api.github.com"); ``` @@ -455,7 +455,7 @@ You can also configure the encoder and decoder separately: ```java Api api = Feign.builder() - .encoder(new JAXBEncoder(jaxbFactory)) + .encoders(new JAXBEncoder(jaxbFactory)) .decoder(new JAXBDecoder(jaxbFactory)) .target(Api.class, "https://apihost"); ``` @@ -476,7 +476,7 @@ You can also configure the encoder and decoder separately: ```java Api api = Feign.builder() - .encoder(new SOAPEncoder(jaxbFactory)) + .encoders(new SOAPEncoder(jaxbFactory)) .decoder(new SOAPDecoder(jaxbFactory)) .errorDecoder(new SOAPErrorDecoder()) .target(MyApi.class, "http://api"); @@ -498,7 +498,7 @@ You can also configure the encoder and decoder separately: ```java GitHub github = Feign.builder() - .encoder(new Fastjson2Encoder()) + .encoders(new Fastjson2Encoder()) .decoder(new Fastjson2Decoder()) .target(GitHub.class, "https://api.github.com"); ``` @@ -701,7 +701,7 @@ interface LoginClient { public class Example { public static void main(String[] args) { LoginClient client = Feign.builder() - .encoder(new GsonEncoder()) + .encoders(new GsonEncoder()) .target(LoginClient.class, "https://foo.com"); client.login(new Credentials("denominator", "secret")); @@ -1343,7 +1343,7 @@ Add `FormEncoder` to your `Feign.Builder` like so: ```java SomeApi github = Feign.builder() - .encoder(new FormEncoder()) + .encoders(new FormEncoder()) .target(SomeApi.class, "http://api.some.org"); ``` @@ -1351,7 +1351,7 @@ Moreover, you can decorate the existing encoder, for example JsonEncoder like th ```java SomeApi github = Feign.builder() - .encoder(new FormEncoder(new JacksonEncoder())) + .encoders(new FormEncoder(new JacksonEncoder())) .target(SomeApi.class, "http://api.some.org"); ``` 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/AsyncFeign.java b/api/src/main/java/feign/AsyncFeign.java index 14a6341b5..591abe771 100644 --- a/api/src/main/java/feign/AsyncFeign.java +++ b/api/src/main/java/feign/AsyncFeign.java @@ -23,6 +23,7 @@ import feign.codec.Encoder; import feign.codec.ErrorDecoder; import feign.interceptor.MethodInterceptor; +import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -163,11 +164,41 @@ public AsyncBuilder logger(Logger logger) { return super.logger(logger); } + /** + * {@inheritDoc} + * + * @deprecated use {@link #encoders(Encoder...)} or {@link #encoders(List)} instead + * @param encoder {@inheritDoc} + * @return {@inheritDoc} + */ @Override + @Deprecated(since = "14", forRemoval = true) public AsyncBuilder encoder(Encoder encoder) { return super.encoder(encoder); } + /** + * {@inheritDoc} + * + * @param encoders {@inheritDoc} + * @return {@inheritDoc} + */ + @Override + public AsyncBuilder encoders(Encoder... encoders) { + return super.encoders(encoders); + } + + /** + * {@inheritDoc} + * + * @param encoders {@inheritDoc} + * @return {@inheritDoc} + */ + @Override + public AsyncBuilder encoders(List encoders) { + return super.encoders(encoders); + } + @Override public AsyncBuilder queryMapEncoder(QueryMapEncoder queryMapEncoder) { return super.queryMapEncoder(queryMapEncoder); diff --git a/api/src/main/java/feign/BaseBuilder.java b/api/src/main/java/feign/BaseBuilder.java index 265e33022..168172087 100644 --- a/api/src/main/java/feign/BaseBuilder.java +++ b/api/src/main/java/feign/BaseBuilder.java @@ -21,8 +21,10 @@ import feign.Request.Options; import feign.codec.Codec; import feign.codec.Decoder; +import feign.codec.DelegatingEncoder; import feign.codec.Encoder; import feign.codec.ErrorDecoder; +import feign.interceptor.Invocation; import feign.interceptor.MethodInterceptor; import feign.interceptor.MethodInterceptors; import java.lang.reflect.Field; @@ -91,8 +93,38 @@ public B logger(Logger logger) { return thisB; } + /** + * Sets the encoder. + * + * @deprecated use {@link #encoders(Encoder...)} or {@link #encoders(List)} instead + * @param encoder the encoder to use for encoding request bodies. Must not be {@code null}. + * @return this builder + */ + @Deprecated(since = "14", forRemoval = true) public B encoder(Encoder encoder) { - this.encoder = encoder; + return encoders(encoder); + } + + /** + * Sets the encoders. + * + * @param encoders the encoders to use for encoding request bodies. Must not be {@code null}. + * @return this builder + * @since 14 + */ + public B encoders(Encoder... encoders) { + return encoders(Arrays.asList(encoders)); + } + + /** + * Sets the encoders. + * + * @param encoders the encoders to use for encoding request bodies. Must not be {@code null}. + * @return this builder + * @since 14 + */ + public B encoders(List encoders) { + this.encoder = new DelegatingEncoder(encoders); return thisB; } diff --git a/api/src/main/java/feign/Feign.java b/api/src/main/java/feign/Feign.java index 45c4d63fd..ad0fdc24d 100644 --- a/api/src/main/java/feign/Feign.java +++ b/api/src/main/java/feign/Feign.java @@ -25,6 +25,7 @@ import java.io.IOException; import java.lang.reflect.Method; import java.lang.reflect.Type; +import java.util.List; /** * Feign's purpose is to ease development against http apis that feign restfulness.
@@ -124,11 +125,41 @@ public Builder logger(Logger logger) { return super.logger(logger); } + /** + * {@inheritDoc} + * + * @deprecated use {@link #encoders(Encoder...)} or {@link #encoders(List)} instead + * @param encoder {@inheritDoc} + * @return {@inheritDoc} + */ @Override + @Deprecated(since = "14", forRemoval = true) public Builder encoder(Encoder encoder) { return super.encoder(encoder); } + /** + * {@inheritDoc} + * + * @param encoders {@inheritDoc} + * @return {@inheritDoc} + */ + @Override + public Builder encoders(Encoder... encoders) { + return super.encoders(encoders); + } + + /** + * {@inheritDoc} + * + * @param encoders {@inheritDoc} + * @return {@inheritDoc} + */ + @Override + public Builder encoders(List encoders) { + return super.encoders(encoders); + } + @Override public Builder decoder(Decoder decoder) { return super.decoder(decoder); diff --git a/api/src/main/java/feign/RequestTemplateFactoryResolver.java b/api/src/main/java/feign/RequestTemplateFactoryResolver.java index dc78b51e7..5340e6c5f 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; @@ -231,6 +232,9 @@ protected RequestTemplate resolve( formVariables.put(entry.getKey(), entry.getValue()); } } + if (!encoder.canEncode(formVariables, Encoder.MAP_STRING_WILDCARD, mutable)) { + throw new EncodeException("This encoder does not support form encoding: " + encoder); + } try { encoder.encode(formVariables, Encoder.MAP_STRING_WILDCARD, mutable); } catch (EncodeException e) { @@ -269,9 +273,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 +284,20 @@ protected RequestTemplate resolve( } return super.resolve(argv, mutable, variables); } + + private void encode(Object object, Type bodyType, RequestTemplate mutable) + throws EncodeException { + if (!encoder.canEncode(object, bodyType, mutable)) { + throw new EncodeException( + "This encoder does not support encoding of type: " + + bodyType + + " with object: " + + object + + ", encoder: " + + encoder); + } + + encoder.encode(object, bodyType, mutable); + } } } 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..724719fed --- /dev/null +++ b/api/src/main/java/feign/codec/DelegatingEncoder.java @@ -0,0 +1,89 @@ +/* + * 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 + */ +public 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}. + */ + public 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 void encode(Object object, Type bodyType, RequestTemplate template) + throws EncodeException { + delegates.stream() + .filter(encoder -> encoder.canEncode(object, bodyType, template)) + .findFirst() + .orElseThrow( + () -> + new EncodeException( + "No suitable encoder found for object encoding: " + + object + + ", encoders: " + + delegates)) + .encode(object, bodyType, template); + } + + /** + * Checks if any of the delegates can encode the given object. + * + * @param object {@inheritDoc} + * @param bodyType {@inheritDoc} + * @param template {@inheritDoc} + * @return {@inheritDoc} + */ + @Override + public boolean canEncode(Object object, Type bodyType, RequestTemplate template) { + return delegates.stream().anyMatch(delegate -> delegate.canEncode(object, bodyType, template)); + } + + /** + * {@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..03cbd9697 100644 --- a/api/src/main/java/feign/codec/Encoder.java +++ b/api/src/main/java/feign/codec/Encoder.java @@ -80,4 +80,18 @@ public interface Encoder { * @throws EncodeException when encoding failed due to a checked exception. */ void encode(Object object, Type bodyType, RequestTemplate template) throws EncodeException; + + /** + * Checks if the encoder can encode the given object. + * + * @param object what to encode as the request body + * @param bodyType the type the object should be encoded as. {@link #MAP_STRING_WILDCARD} + * indicates form encoding + * @param template the request template to populate + * @return {@code true} if the encoder can encode the object, {@code false} otherwise + * @since 14 + * @apiNote when this method returns {@code true}, the {@link EncodeException} may still be thrown + * by {@link #encode(Object, Type, RequestTemplate)} if the encoding fails. + */ + boolean canEncode(Object object, Type bodyType, RequestTemplate template); } diff --git a/api/src/main/java/feign/codec/JsonEncoder.java b/api/src/main/java/feign/codec/JsonEncoder.java index 47d800e74..6bce20b7f 100644 --- a/api/src/main/java/feign/codec/JsonEncoder.java +++ b/api/src/main/java/feign/codec/JsonEncoder.java @@ -15,7 +15,29 @@ */ package feign.codec; -import feign.Experimental; +import feign.RequestTemplate; +import java.lang.reflect.Type; +import java.util.List; -@Experimental -public interface JsonEncoder extends Encoder {} +/** An encoder that encodes objects to JSON. */ +@FunctionalInterface +public interface JsonEncoder extends Encoder { + /** + * Checks if the given object can be encoded to JSON based on the {@code Content-Type} header in + * the {@link RequestTemplate}. + * + * @param object {@inheritDoc} + * @param bodyType {@inheritDoc} + * @param template {@inheritDoc} + * @return {@code true} if the given {@code Content-Type} header is compatible with JSON, {@code + * false} otherwise + */ + @Override + default boolean canEncode(Object object, Type bodyType, RequestTemplate template) { + return template.headers().getOrDefault("Content-Type", List.of()).stream() + .anyMatch( + contentType -> + contentType != null + && contentType.trim().matches("(?i)\\w+/(?:[\\w._-]+\\+)?json.*")); + } +} diff --git a/api/src/main/java/feign/codec/XmlEncoder.java b/api/src/main/java/feign/codec/XmlEncoder.java new file mode 100644 index 000000000..53a54c4d1 --- /dev/null +++ b/api/src/main/java/feign/codec/XmlEncoder.java @@ -0,0 +1,47 @@ +/* + * 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; + +/** + * An encoder that encodes objects to XML. + * + * @since 14 + */ +@FunctionalInterface +public interface XmlEncoder extends Encoder { + /** + * Checks if the given object can be encoded to XML based on the {@code Content-Type} header in + * the {@link RequestTemplate}. + * + * @param object {@inheritDoc} + * @param bodyType {@inheritDoc} + * @param template {@inheritDoc} + * @return {@code true} if the given {@code Content-Type} header is compatible with XML, {@code + * false} otherwise + */ + @Override + default boolean canEncode(Object object, Type bodyType, 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/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/codec/DelegatingEncoderTest.java b/api/src/test/java/feign/codec/DelegatingEncoderTest.java new file mode 100644 index 000000000..9c14d6e8c --- /dev/null +++ b/api/src/test/java/feign/codec/DelegatingEncoderTest.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.codec; + +import static org.junit.jupiter.api.Assertions.assertEquals; +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.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +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)); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testCanEncode(boolean expected) { + var object = "Hello, World!"; + var bodyType = String.class; + var requestTemplate = mock(RequestTemplate.class); + + when(delegate.canEncode(object, bodyType, requestTemplate)).thenReturn(expected); + + var actual = encoder.canEncode(object, bodyType, requestTemplate); + + assertEquals(expected, actual); + } + + @Test + void shouldEncode() { + var object = "Hello, World!"; + var bodyType = String.class; + var requestTemplate = mock(RequestTemplate.class); + + when(delegate.canEncode(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/apt-test-generator/src/test/resources/example/github/GitHubExample.java b/apt-test-generator/src/test/resources/example/github/GitHubExample.java index 2539e5b02..dced22948 100644 --- a/apt-test-generator/src/test/resources/example/github/GitHubExample.java +++ b/apt-test-generator/src/test/resources/example/github/GitHubExample.java @@ -73,7 +73,7 @@ static GitHub connect() { final Decoder decoder = new GsonDecoder(); final Encoder encoder = new GsonEncoder(); return Feign.builder() - .encoder(encoder) + .encoders(encoder) .decoder(decoder) .errorDecoder(new GitHubErrorDecoder(decoder)) .logger(new Logger.ErrorLogger()) diff --git a/core/src/main/java/feign/core/codec/DefaultEncoder.java b/core/src/main/java/feign/core/codec/DefaultEncoder.java index fe35256c1..ecc99a663 100644 --- a/core/src/main/java/feign/core/codec/DefaultEncoder.java +++ b/core/src/main/java/feign/core/codec/DefaultEncoder.java @@ -47,4 +47,23 @@ public void encode(Object object, Type bodyType, RequestTemplate template) { format("%s is not a type supported by this encoder.", object.getClass())); } } + + /** + * {@inheritDoc} + * + * @param object {@inheritDoc} + * @param bodyType {@inheritDoc} + * @param template {@inheritDoc} + * @return {@inheritDoc} + */ + @Override + public boolean canEncode(Object object, Type bodyType, RequestTemplate template) { + return object == null + || bodyType == String.class + || bodyType == byte[].class + || object instanceof File + || object instanceof Path + || object instanceof InputStream + || object instanceof Request.Body; + } } diff --git a/core/src/test/java/feign/AlwaysEncodeBodyContractTest.java b/core/src/test/java/feign/AlwaysEncodeBodyContractTest.java index 1ea64c597..5fb24b5e4 100644 --- a/core/src/test/java/feign/AlwaysEncodeBodyContractTest.java +++ b/core/src/test/java/feign/AlwaysEncodeBodyContractTest.java @@ -68,6 +68,11 @@ public void encode(Object object, Type bodyType, RequestTemplate template) Arrays.stream(methodParameters).map(String::valueOf).collect(Collectors.joining()); template.body(Request.Body.of(body)); } + + @Override + public boolean canEncode(Object object, Type bodyType, RequestTemplate template) { + return true; + } } private static class BodyParameterSampleEncoder implements Encoder { @@ -76,6 +81,11 @@ public void encode(Object object, Type bodyType, RequestTemplate template) throws EncodeException { template.body(Request.Body.of(String.valueOf(object))); } + + @Override + public boolean canEncode(Object object, Type bodyType, RequestTemplate template) { + return true; + } } private static class SampleClient implements Client { @@ -102,7 +112,7 @@ void alwaysEncodeBodyTrueTest() { SampleTargetMultipleNonAnnotatedParameters sampleClient1 = Feign.builder() .contract(new SampleContract()) - .encoder(new AllParametersSampleEncoder()) + .encoders(new AllParametersSampleEncoder()) .client(new SampleClient()) .target(SampleTargetMultipleNonAnnotatedParameters.class, "http://localhost"); assertThat(sampleClient1.concatenate("foo", "bar", "char")).isEqualTo("foobarchar"); @@ -110,7 +120,7 @@ void alwaysEncodeBodyTrueTest() { SampleTargetNoParameters sampleClient2 = Feign.builder() .contract(new SampleContract()) - .encoder(new AllParametersSampleEncoder()) + .encoders(new AllParametersSampleEncoder()) .client(new SampleClient()) .target(SampleTargetNoParameters.class, "http://localhost"); assertThat(sampleClient2.concatenate()).isEmpty(); @@ -118,7 +128,7 @@ void alwaysEncodeBodyTrueTest() { SampleTargetOneParameter sampleClient3 = Feign.builder() .contract(new SampleContract()) - .encoder(new AllParametersSampleEncoder()) + .encoders(new AllParametersSampleEncoder()) .client(new SampleClient()) .target(SampleTargetOneParameter.class, "http://localhost"); assertThat(sampleClient3.concatenate("moo")).isEqualTo("moo"); diff --git a/core/src/test/java/feign/AsyncFeignTest.java b/core/src/test/java/feign/AsyncFeignTest.java index 0d81302db..572abfb7e 100644 --- a/core/src/test/java/feign/AsyncFeignTest.java +++ b/core/src/test/java/feign/AsyncFeignTest.java @@ -152,6 +152,11 @@ void bodyTypeCorrespondsWithParameterType() throws Exception { public void encode(Object object, Type bodyType, RequestTemplate template) { encodedType.set(bodyType); } + + @Override + public boolean canEncode(Object object, Type bodyType, RequestTemplate template) { + return true; + } }) .target("http://localhost:" + server.getPort()); @@ -863,8 +868,17 @@ void okIfEncodeRootCauseHasNoMessage() throws Throwable { TestInterfaceAsync api = new TestInterfaceAsyncBuilder() .encoder( - (_, _, _) -> { - throw new RuntimeException(); + new Encoder() { + @Override + public void encode(Object object, Type bodyType, RequestTemplate template) + throws EncodeException { + throw new RuntimeException(); + } + + @Override + public boolean canEncode(Object object, Type bodyType, RequestTemplate template) { + return true; + } }) .target("http://localhost:" + server.getPort()); @@ -1235,12 +1249,21 @@ static final class TestInterfaceAsyncBuilder { private final AsyncFeign.AsyncBuilder delegate = AsyncFeign.builder() .decoder(new DefaultDecoder()) - .encoder( - (object, _, template) -> { - if (object instanceof Map) { - template.body(Request.Body.of(new Gson().toJson(object))); - } else { - template.body(Request.Body.of(object.toString())); + .encoders( + new Encoder() { + @Override + public void encode(Object object, Type bodyType, RequestTemplate template) + throws EncodeException { + if (object instanceof Map) { + template.body(Request.Body.of(new Gson().toJson(object))); + } else { + template.body(Request.Body.of(object.toString())); + } + } + + @Override + public boolean canEncode(Object object, Type bodyType, RequestTemplate template) { + return true; } }); @@ -1250,7 +1273,7 @@ TestInterfaceAsyncBuilder requestInterceptor(RequestInterceptor requestIntercept } TestInterfaceAsyncBuilder encoder(Encoder encoder) { - delegate.encoder(encoder); + delegate.encoders(encoder); return this; } diff --git a/core/src/test/java/feign/BaseApiTest.java b/core/src/test/java/feign/BaseApiTest.java index a59e521c7..85e4e15e0 100644 --- a/core/src/test/java/feign/BaseApiTest.java +++ b/core/src/test/java/feign/BaseApiTest.java @@ -16,10 +16,12 @@ package feign; import static feign.assertj.MockWebServerAssertions.assertThat; -import static org.assertj.core.api.Assertions.assertThat; import com.google.gson.reflect.TypeToken; +import feign.codec.EncodeException; +import feign.codec.Encoder; import java.io.IOException; +import java.lang.reflect.Type; import java.util.List; import mockwebserver3.MockResponse; import mockwebserver3.MockWebServer; @@ -83,9 +85,19 @@ void resolvesBodyParameter() throws Exception { String baseUrl = server.url("/default").toString(); Feign.builder() - .encoder( - (_, bodyType, _) -> - assertThat(bodyType).isEqualTo(new TypeToken>() {}.getType())) + .encoders( + new Encoder() { + @Override + public void encode(Object object, Type bodyType, RequestTemplate template) + throws EncodeException { + assertThat(bodyType).isEqualTo(new TypeToken>() {}.getType()); + } + + @Override + public boolean canEncode(Object object, Type bodyType, RequestTemplate template) { + 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..81a30e2c8 100644 --- a/core/src/test/java/feign/FeignBuilderTest.java +++ b/core/src/test/java/feign/FeignBuilderTest.java @@ -21,6 +21,7 @@ import static org.assertj.core.api.Assertions.failBecauseExceptionWasNotThrown; import feign.codec.Decoder; +import feign.codec.EncodeException; import feign.codec.Encoder; import feign.core.DefaultClient; import java.io.IOException; @@ -28,6 +29,7 @@ import java.io.Reader; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; +import java.lang.reflect.Type; import java.nio.charset.Charset; import java.util.Arrays; import java.util.Collections; @@ -209,13 +211,25 @@ void urlPathConcatNoInitialSlashOnPathNoTrailingSlashOnUrl() throws Exception { } @Test - void overrideEncoder() throws Exception { + void overrideEncoders() 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 = + new Encoder() { + @Override + public void encode(Object object, Type bodyType, RequestTemplate template) + throws EncodeException { + template.body(Request.Body.of(object.toString())); + } + + @Override + public boolean canEncode(Object object, Type bodyType, RequestTemplate template) { + return true; + } + }; - TestInterface api = Feign.builder().encoder(encoder).target(TestInterface.class, url); + TestInterface api = Feign.builder().encoders(encoder).target(TestInterface.class, url); api.encodedPost(Arrays.asList("This", "is", "my", "request")); assertThat(server.takeRequest()).hasBody("[This, is, my, request]"); diff --git a/core/src/test/java/feign/FeignTest.java b/core/src/test/java/feign/FeignTest.java index 37ef0cdb2..0af6c4f27 100755 --- a/core/src/test/java/feign/FeignTest.java +++ b/core/src/test/java/feign/FeignTest.java @@ -173,6 +173,11 @@ void bodyTypeCorrespondsWithParameterType() throws Exception { public void encode(Object object, Type bodyType, RequestTemplate template) { encodedType.set(bodyType); } + + @Override + public boolean canEncode(Object object, Type bodyType, RequestTemplate template) { + return true; + } }) .target("http://localhost:" + server.getPort()); @@ -878,8 +883,17 @@ void okIfEncodeRootCauseHasNoMessage() throws Exception { TestInterface api = new TestInterfaceBuilder() .encoder( - (_, _, _) -> { - throw new RuntimeException(); + new Encoder() { + @Override + public void encode(Object object, Type bodyType, RequestTemplate template) + throws EncodeException { + throw new RuntimeException(); + } + + @Override + public boolean canEncode(Object object, Type bodyType, RequestTemplate template) { + return true; + } }) .target("http://localhost:" + server.getPort()); @@ -1503,12 +1517,21 @@ static final class TestInterfaceBuilder { private final Feign.Builder delegate = new Feign.Builder() .decoder(new DefaultDecoder()) - .encoder( - (object, _, template) -> { - if (object instanceof Map) { - template.body(Request.Body.of(new Gson().toJson(object))); - } else { - template.body(Request.Body.of(object.toString())); + .encoders( + new Encoder() { + @Override + public void encode(Object object, Type bodyType, RequestTemplate template) + throws EncodeException { + if (object instanceof Map) { + template.body(Request.Body.of(new Gson().toJson(object))); + } else { + template.body(Request.Body.of(object.toString())); + } + } + + @Override + public boolean canEncode(Object object, Type bodyType, RequestTemplate template) { + return true; } }); @@ -1528,7 +1551,7 @@ TestInterfaceBuilder responseInterceptors(Iterable response } TestInterfaceBuilder encoder(Encoder encoder) { - delegate.encoder(encoder); + delegate.encoders(encoder); return this; } diff --git a/core/src/test/java/feign/FeignUnderAsyncTest.java b/core/src/test/java/feign/FeignUnderAsyncTest.java index a3f333bb5..eea18e8a1 100644 --- a/core/src/test/java/feign/FeignUnderAsyncTest.java +++ b/core/src/test/java/feign/FeignUnderAsyncTest.java @@ -136,6 +136,11 @@ void bodyTypeCorrespondsWithParameterType() throws Exception { public void encode(Object object, Type bodyType, RequestTemplate template) { encodedType.set(bodyType); } + + @Override + public boolean canEncode(Object object, Type bodyType, RequestTemplate template) { + return true; + } }) .target("http://localhost:" + server.getPort()); @@ -628,8 +633,17 @@ void okIfEncodeRootCauseHasNoMessage() throws Exception { TestInterface api = new TestInterfaceBuilder() .encoder( - (_, _, _) -> { - throw new RuntimeException(); + new Encoder() { + @Override + public void encode(Object object, Type bodyType, RequestTemplate template) + throws EncodeException { + throw new RuntimeException(); + } + + @Override + public boolean canEncode(Object object, Type bodyType, RequestTemplate template) { + return true; + } }) .target("http://localhost:" + server.getPort()); @@ -965,12 +979,21 @@ static final class TestInterfaceBuilder { private final AsyncFeign.AsyncBuilder delegate = AsyncFeign.builder() .decoder(new DefaultDecoder()) - .encoder( - (object, _, template) -> { - if (object instanceof Map) { - template.body(Request.Body.of(new Gson().toJson(object))); - } else { - template.body(Request.Body.of(object.toString())); + .encoders( + new Encoder() { + @Override + public void encode(Object object, Type bodyType, RequestTemplate template) + throws EncodeException { + if (object instanceof Map) { + template.body(Request.Body.of(new Gson().toJson(object))); + } else { + template.body(Request.Body.of(object.toString())); + } + } + + @Override + public boolean canEncode(Object object, Type bodyType, RequestTemplate template) { + return true; } }); @@ -980,7 +1003,7 @@ TestInterfaceBuilder requestInterceptor(RequestInterceptor requestInterceptor) { } TestInterfaceBuilder encoder(Encoder encoder) { - delegate.encoder(encoder); + delegate.encoders(encoder); return this; } diff --git a/core/src/test/java/feign/MethodMetadataPresenceTest.java b/core/src/test/java/feign/MethodMetadataPresenceTest.java index 1df9eb376..a6d0da7d3 100644 --- a/core/src/test/java/feign/MethodMetadataPresenceTest.java +++ b/core/src/test/java/feign/MethodMetadataPresenceTest.java @@ -16,13 +16,15 @@ package feign; import static feign.assertj.MockWebServerAssertions.assertThat; -import static org.assertj.core.api.Assertions.assertThat; import feign.FeignBuilderTest.TestInterface; +import feign.codec.EncodeException; +import feign.codec.Encoder; import feign.core.DefaultClient; import feign.core.codec.DefaultDecoder; import feign.core.codec.DefaultEncoder; import java.io.IOException; +import java.lang.reflect.Type; import mockwebserver3.MockResponse; import mockwebserver3.MockWebServer; import org.junit.jupiter.api.AfterEach; @@ -62,12 +64,21 @@ void encoder() throws Exception { final String url = "http://localhost:" + server.getPort(); final TestInterface api = Feign.builder() - .encoder( - (object, bodyType, template) -> { - assertThat(template).isNotNull(); - assertThat(template.methodMetadata()).isNotNull(); - assertThat(template.feignTarget()).isNotNull(); - new DefaultEncoder().encode(object, bodyType, template); + .encoders( + new Encoder() { + @Override + public void encode(Object object, Type bodyType, RequestTemplate template) + throws EncodeException { + assertThat(template).isNotNull(); + assertThat(template.methodMetadata()).isNotNull(); + assertThat(template.feignTarget()).isNotNull(); + new DefaultEncoder().encode(object, bodyType, template); + } + + @Override + public boolean canEncode(Object object, Type bodyType, RequestTemplate template) { + return true; + } }) .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..06c399fb0 100644 --- a/core/src/test/java/feign/core/codec/DefaultEncoderTest.java +++ b/core/src/test/java/feign/core/codec/DefaultEncoderTest.java @@ -19,6 +19,7 @@ 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 static org.junit.jupiter.params.provider.Arguments.arguments; import feign.Request; import feign.RequestTemplate; @@ -28,15 +29,32 @@ import java.io.File; import java.io.IOException; import java.io.InputStream; +import java.lang.reflect.Type; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.time.Clock; import java.util.Optional; +import java.util.function.Supplier; +import java.util.stream.Stream; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.FieldSource; class DefaultEncoderTest { + private static final Supplier> testCanEncode = + () -> + Stream.of( + arguments(null, Object.class, true), + arguments("string", String.class, true), + arguments(new byte[] {1, 2, 3}, byte[].class, true), + arguments(new File("file"), File.class, true), + arguments(Path.of("path"), Path.class, true), + arguments(new ByteArrayInputStream(new byte[] {1, 2, 3}), InputStream.class, true), + arguments(Request.Body.of("body"), Request.Body.class, true), + arguments(new DefaultEncoderTest(), DefaultEncoderTest.class, false)); private final Encoder encoder = new DefaultEncoder(); @@ -129,6 +147,15 @@ void shouldEncodeRequestBody() { verifyBody(template, expected.length(), true, expected); } + @ParameterizedTest + @FieldSource + void testCanEncode(Object object, Type bodyType, boolean expected) { + var template = new RequestTemplate(); + var actual = encoder.canEncode(object, bodyType, template); + + assertEquals(expected, actual); + } + void verifyBody( RequestTemplate template, long contentLength, boolean repeatable, String expected) { var optionalBody = template.requestBody(); diff --git a/dropwizard-metrics4/src/main/java/feign/metrics4/MeteredEncoder.java b/dropwizard-metrics4/src/main/java/feign/metrics4/MeteredEncoder.java index 4263d14d5..f85fd5e64 100644 --- a/dropwizard-metrics4/src/main/java/feign/metrics4/MeteredEncoder.java +++ b/dropwizard-metrics4/src/main/java/feign/metrics4/MeteredEncoder.java @@ -61,4 +61,17 @@ public void encode(Object object, Type bodyType, RequestTemplate template) metricSuppliers.histograms()) .update(body.contentLength())); } + + /** + * {@inheritDoc} + * + * @param object {@inheritDoc} + * @param bodyType {@inheritDoc} + * @param template {@inheritDoc} + * @return {@inheritDoc} + */ + @Override + public boolean canEncode(Object object, Type bodyType, RequestTemplate template) { + return encoder.canEncode(object, bodyType, template); + } } diff --git a/dropwizard-metrics5/src/main/java/feign/metrics5/MeteredEncoder.java b/dropwizard-metrics5/src/main/java/feign/metrics5/MeteredEncoder.java index 09f3058be..0595a223a 100644 --- a/dropwizard-metrics5/src/main/java/feign/metrics5/MeteredEncoder.java +++ b/dropwizard-metrics5/src/main/java/feign/metrics5/MeteredEncoder.java @@ -73,4 +73,17 @@ public void encode(Object object, Type bodyType, RequestTemplate template) metricSuppliers.histograms()) .update(body.contentLength())); } + + /** + * {@inheritDoc} + * + * @param object {@inheritDoc} + * @param bodyType {@inheritDoc} + * @param template {@inheritDoc} + * @return {@inheritDoc} + */ + @Override + public boolean canEncode(Object object, Type bodyType, RequestTemplate template) { + return encoder.canEncode(object, bodyType, template); + } } diff --git a/example-github-with-coroutine/src/main/java/example/github/GitHubExample.kt b/example-github-with-coroutine/src/main/java/example/github/GitHubExample.kt index 3058435b1..a1e8e4375 100644 --- a/example-github-with-coroutine/src/main/java/example/github/GitHubExample.kt +++ b/example-github-with-coroutine/src/main/java/example/github/GitHubExample.kt @@ -90,7 +90,7 @@ interface GitHub { val decoder: Decoder = feign.gson.GsonDecoder() val encoder: Encoder = GsonEncoder() return CoroutineFeign.builder() - .encoder(encoder) + .encoders(encoder) .decoder(decoder) .errorDecoder(GitHubErrorDecoder(decoder)) .logger(ErrorLogger()) diff --git a/example-github/src/main/java/example/github/GitHubExample.java b/example-github/src/main/java/example/github/GitHubExample.java index 2539e5b02..dced22948 100644 --- a/example-github/src/main/java/example/github/GitHubExample.java +++ b/example-github/src/main/java/example/github/GitHubExample.java @@ -73,7 +73,7 @@ static GitHub connect() { final Decoder decoder = new GsonDecoder(); final Encoder encoder = new GsonEncoder(); return Feign.builder() - .encoder(encoder) + .encoders(encoder) .decoder(decoder) .errorDecoder(new GitHubErrorDecoder(decoder)) .logger(new Logger.ErrorLogger()) diff --git a/fastjson2/README.md b/fastjson2/README.md index 714995dba..6633c6300 100644 --- a/fastjson2/README.md +++ b/fastjson2/README.md @@ -15,7 +15,7 @@ You can also configure the encoder and decoder separately: ```java GitHub github = Feign.builder() - .encoder(new Fastjson2Encoder()) + .encoders(new Fastjson2Encoder()) .decoder(new Fastjson2Decoder()) .target(GitHub.class, "https://api.github.com"); ``` diff --git a/form-spring/src/test/java/feign/form/feign/spring/Client.java b/form-spring/src/test/java/feign/form/feign/spring/Client.java index dbd2040a9..f98b69b79 100644 --- a/form-spring/src/test/java/feign/form/feign/spring/Client.java +++ b/form-spring/src/test/java/feign/form/feign/spring/Client.java @@ -21,6 +21,7 @@ import feign.Logger; import feign.Response; +import feign.codec.DelegatingEncoder; import feign.codec.Encoder; import feign.form.spring.SpringFormEncoder; import java.util.List; @@ -92,7 +93,8 @@ class ClientConfiguration { @Bean Encoder feignEncoder(ObjectProvider messageConverters) { - return new SpringFormEncoder(new SpringEncoder(messageConverters)); + return new DelegatingEncoder( + List.of(new SpringEncoder(messageConverters), new SpringFormEncoder())); } @Bean 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..b26cb5f9c 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; @@ -31,6 +32,8 @@ import org.springframework.test.annotation.DirtiesContext.ClassMode; import org.springframework.web.multipart.MultipartFile; +// TODO: enable when Spring Cloud OpenFiegn migrates to Feign 14 +@Disabled("Disabled util Spring Cloud OpenFiegn migrates to Feign 14") @SpringBootTest( webEnvironment = DEFINED_PORT, classes = Server.class, diff --git a/form/src/main/java/feign/form/FormEncoder.java b/form/src/main/java/feign/form/FormEncoder.java index deb465673..cef546b37 100644 --- a/form/src/main/java/feign/form/FormEncoder.java +++ b/form/src/main/java/feign/form/FormEncoder.java @@ -103,6 +103,19 @@ public void encode(Object object, Type bodyType, RequestTemplate template) processors.get(contentType).process(template, charset, data); } + /** + * {@inheritDoc} + * + * @param object {@inheritDoc} + * @param bodyType {@inheritDoc} + * @param template {@inheritDoc} + * @return {@inheritDoc} + */ + @Override + public boolean canEncode(Object object, Type bodyType, RequestTemplate template) { + return processors.containsKey(ContentType.of(getContentTypeValue(template.headers()))); + } + /** * Returns {@link ContentProcessor} for specific {@link ContentType}. * diff --git a/form/src/test/java/feign/form/BasicClientTest.java b/form/src/test/java/feign/form/BasicClientTest.java index b88748c59..ed0fb5451 100644 --- a/form/src/test/java/feign/form/BasicClientTest.java +++ b/form/src/test/java/feign/form/BasicClientTest.java @@ -48,8 +48,8 @@ static void configureClient() { API = Feign.builder() - .encoder(new FormEncoder(new JacksonEncoder())) .logger(new JavaLogger(BasicClientTest.class).appendToFile(logFile)) + .encoders(new FormEncoder(), new JacksonEncoder()) .logLevel(FULL) .target(TestClient.class, "http://localhost:8080"); } diff --git a/form/src/test/java/feign/form/ByteArrayClientTest.java b/form/src/test/java/feign/form/ByteArrayClientTest.java index 476dfea1e..c622cb3ed 100644 --- a/form/src/test/java/feign/form/ByteArrayClientTest.java +++ b/form/src/test/java/feign/form/ByteArrayClientTest.java @@ -46,8 +46,8 @@ static void configureClient() { API = Feign.builder() - .encoder(encoder) .logger(new JavaLogger(ByteArrayClientTest.class).appendToFile(logFile)) + .encoders(encoder) .logLevel(FULL) .target(CustomClient.class, "http://localhost:8080"); } diff --git a/form/src/test/java/feign/form/CustomClientTest.java b/form/src/test/java/feign/form/CustomClientTest.java index 7fd85b6e5..dacb4d300 100644 --- a/form/src/test/java/feign/form/CustomClientTest.java +++ b/form/src/test/java/feign/form/CustomClientTest.java @@ -52,8 +52,8 @@ static void configureClient() { API = Feign.builder() - .encoder(encoder) .logger(new JavaLogger(CustomClientTest.class).appendToFile(logFile)) + .encoders(encoder) .logLevel(FULL) .target(CustomClient.class, "http://localhost:8080"); } diff --git a/form/src/test/java/feign/form/FormPropertyTest.java b/form/src/test/java/feign/form/FormPropertyTest.java index 745092882..f755374f5 100644 --- a/form/src/test/java/feign/form/FormPropertyTest.java +++ b/form/src/test/java/feign/form/FormPropertyTest.java @@ -43,8 +43,8 @@ static void configureClient() { API = Feign.builder() - .encoder(new FormEncoder(new JacksonEncoder())) .logger(new JavaLogger(FormPropertyTest.class).appendToFile(logFile)) + .encoders(new FormEncoder(new JacksonEncoder())) .logLevel(FULL) .target(FormClient.class, "http://localhost:8080"); } diff --git a/form/src/test/java/feign/form/MultipartBoundaryTest.java b/form/src/test/java/feign/form/MultipartBoundaryTest.java index 26399fcdc..c833d354f 100644 --- a/form/src/test/java/feign/form/MultipartBoundaryTest.java +++ b/form/src/test/java/feign/form/MultipartBoundaryTest.java @@ -19,14 +19,26 @@ import static org.assertj.core.api.Assertions.assertThat; import feign.RequestTemplate; +import feign.codec.EncodeException; import feign.codec.Encoder; +import java.lang.reflect.Type; import java.util.LinkedHashMap; import java.util.Map; import org.junit.jupiter.api.Test; class MultipartBoundaryTest { - private static final Encoder NOOP_DELEGATE = (object, bodyType, template) -> {}; + private static final Encoder NOOP_DELEGATE = + new Encoder() { + @Override + public void encode(Object object, Type bodyType, RequestTemplate template) + throws EncodeException {} + + @Override + public boolean canEncode(Object object, Type bodyType, RequestTemplate template) { + return true; + } + }; @Test void boundaryIsNotDerivedFromTheClock() { diff --git a/form/src/test/java/feign/form/UrlencodedFormContentProcessorTest.java b/form/src/test/java/feign/form/UrlencodedFormContentProcessorTest.java index be915fab9..dfa26a615 100644 --- a/form/src/test/java/feign/form/UrlencodedFormContentProcessorTest.java +++ b/form/src/test/java/feign/form/UrlencodedFormContentProcessorTest.java @@ -126,7 +126,7 @@ private void assertEncodedBody( .start()) { var client = Feign.builder() - .encoder(new FormEncoder(new JacksonEncoder())) + .encoders(new FormEncoder(new JacksonEncoder())) .target(Client.class, server.getConnectUrl()); var data = createRequestData(tags); diff --git a/form/src/test/java/feign/form/WildCardMapTest.java b/form/src/test/java/feign/form/WildCardMapTest.java index 04305c5cb..60d76317f 100644 --- a/form/src/test/java/feign/form/WildCardMapTest.java +++ b/form/src/test/java/feign/form/WildCardMapTest.java @@ -45,8 +45,8 @@ static void configureClient() { api = Feign.builder() - .encoder(new FormEncoder()) .logger(new JavaLogger(WildCardMapTest.class).appendToFile(logFile)) + .encoders(new FormEncoder()) .logLevel(FULL) .target(FormUrlEncodedApi.class, "http://localhost:8080"); } diff --git a/form/src/test/java/feign/form/issues/Issue63Test.java b/form/src/test/java/feign/form/issues/Issue63Test.java index e0a6dafd0..d8b354daa 100644 --- a/form/src/test/java/feign/form/issues/Issue63Test.java +++ b/form/src/test/java/feign/form/issues/Issue63Test.java @@ -36,7 +36,7 @@ void test() { try (var server = UndertowServer.builder().callback(this::handleRequest).start()) { var client = Feign.builder() - .encoder(new FormEncoder(new JacksonEncoder())) + .encoders(new FormEncoder(new JacksonEncoder())) .target(Client.class, server.getConnectUrl()); var data = new HashMap(); diff --git a/form/src/test/java/feign/form/multipart/DelegateWriterTest.java b/form/src/test/java/feign/form/multipart/DelegateWriterTest.java index 3a3f7a248..223a94032 100644 --- a/form/src/test/java/feign/form/multipart/DelegateWriterTest.java +++ b/form/src/test/java/feign/form/multipart/DelegateWriterTest.java @@ -18,7 +18,10 @@ import static org.assertj.core.api.Assertions.assertThat; import feign.Request; +import feign.RequestTemplate; +import feign.codec.EncodeException; import feign.codec.Encoder; +import java.lang.reflect.Type; import java.nio.charset.StandardCharsets; import org.junit.jupiter.api.Test; @@ -31,9 +34,18 @@ class DelegateWriterTest { @Test void usesContentTypeFromDelegate() throws Exception { Encoder delegate = - (object, bodyType, template) -> { - template.header("Content-Type", "application/json"); - template.body(Request.Body.of("{\"hash\":\"somehash\"}")); + new Encoder() { + @Override + public void encode(Object object, Type bodyType, RequestTemplate template) + throws EncodeException { + template.header("Content-Type", "application/json"); + template.body(Request.Body.of("{\"hash\":\"somehash\"}")); + } + + @Override + public boolean canEncode(Object object, Type bodyType, RequestTemplate template) { + return true; + } }; assertThat(write(delegate)) @@ -43,7 +55,19 @@ void usesContentTypeFromDelegate() throws Exception { @Test void fallsBackToTextPlainWhenDelegateSetsNoContentType() throws Exception { - Encoder delegate = (object, bodyType, template) -> template.body(Request.Body.of("plain")); + Encoder delegate = + new Encoder() { + @Override + public void encode(Object object, Type bodyType, RequestTemplate template) + throws EncodeException { + template.body(Request.Body.of("plain")); + } + + @Override + public boolean canEncode(Object object, Type bodyType, RequestTemplate template) { + 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..e45122042 100644 --- a/graphql/src/main/java/feign/graphql/GraphqlEncoder.java +++ b/graphql/src/main/java/feign/graphql/GraphqlEncoder.java @@ -53,4 +53,17 @@ public void encode(Object object, Type bodyType, RequestTemplate template) delegate.encode(graphqlBody, MAP_STRING_WILDCARD, template); } + + /** + * {@inheritDoc} + * + * @param object {@inheritDoc} + * @param bodyType {@inheritDoc} + * @param template {@inheritDoc} + * @return {@inheritDoc} + */ + @Override + public boolean canEncode(Object object, Type bodyType, RequestTemplate template) { + return contract.lookupMetadata(template) != null; + } } diff --git a/gson/README.md b/gson/README.md index c005699be..8e08f08f5 100644 --- a/gson/README.md +++ b/gson/README.md @@ -15,7 +15,7 @@ You can also configure the encoder and decoder separately: ```java GitHub github = Feign.builder() - .encoder(new GsonEncoder()) + .encoders(new GsonEncoder()) .decoder(new GsonDecoder()) .target(GitHub.class, "https://api.github.com"); ``` diff --git a/hc5/src/test/java/feign/hc5/AsyncApacheHttp5ClientTest.java b/hc5/src/test/java/feign/hc5/AsyncApacheHttp5ClientTest.java index f8396dcd9..52024212f 100644 --- a/hc5/src/test/java/feign/hc5/AsyncApacheHttp5ClientTest.java +++ b/hc5/src/test/java/feign/hc5/AsyncApacheHttp5ClientTest.java @@ -165,6 +165,11 @@ void bodyTypeCorrespondsWithParameterType() throws Exception { public void encode(Object object, Type bodyType, RequestTemplate template) { encodedType.set(bodyType); } + + @Override + public boolean canEncode(Object object, Type bodyType, RequestTemplate template) { + return true; + } }) .target("http://localhost:" + server.getPort()); @@ -660,8 +665,17 @@ void okIfEncodeRootCauseHasNoMessage() throws Throwable { final TestInterfaceAsync api = new TestInterfaceAsyncBuilder() .encoder( - (_, _, _) -> { - throw new RuntimeException(); + new Encoder() { + @Override + public void encode(Object object, Type bodyType, RequestTemplate template) + throws EncodeException { + throw new RuntimeException(); + } + + @Override + public boolean canEncode(Object object, Type bodyType, RequestTemplate template) { + return true; + } }) .target("http://localhost:" + server.getPort()); @@ -1086,12 +1100,21 @@ static final class TestInterfaceAsyncBuilder { AsyncFeign.builder() .client(new AsyncApacheHttp5Client()) .decoder(new DefaultDecoder()) - .encoder( - (object, _, template) -> { - if (object instanceof Map) { - template.body(Request.Body.of(new Gson().toJson(object))); - } else { - template.body(Request.Body.of(object.toString())); + .encoders( + new Encoder() { + @Override + public void encode(Object object, Type bodyType, RequestTemplate template) + throws EncodeException { + if (object instanceof Map) { + template.body(Request.Body.of(new Gson().toJson(object))); + } else { + template.body(Request.Body.of(object.toString())); + } + } + + @Override + public boolean canEncode(Object object, Type bodyType, RequestTemplate template) { + return true; } }); @@ -1101,7 +1124,7 @@ TestInterfaceAsyncBuilder requestInterceptor(RequestInterceptor requestIntercept } TestInterfaceAsyncBuilder encoder(Encoder encoder) { - delegate.encoder(encoder); + delegate.encoders(encoder); return this; } diff --git a/hystrix/src/main/java/feign/hystrix/HystrixFeign.java b/hystrix/src/main/java/feign/hystrix/HystrixFeign.java index 08ea3fed2..e1ddf2d2f 100644 --- a/hystrix/src/main/java/feign/hystrix/HystrixFeign.java +++ b/hystrix/src/main/java/feign/hystrix/HystrixFeign.java @@ -31,6 +31,7 @@ import feign.codec.ErrorDecoder; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; +import java.util.List; import java.util.Map; /** @@ -168,11 +169,41 @@ public Builder logger(Logger logger) { return (Builder) super.logger(logger); } + /** + * {@inheritDoc} + * + * @deprecated use {@link #encoders(Encoder...)} or {@link #encoders(List)} instead + * @param encoder {@inheritDoc} + * @return {@inheritDoc} + */ @Override + @Deprecated(since = "14", forRemoval = true) public Builder encoder(Encoder encoder) { return (Builder) super.encoder(encoder); } + /** + * {@inheritDoc} + * + * @param encoders {@inheritDoc} + * @return {@inheritDoc} + */ + @Override + public Builder encoders(Encoder... encoders) { + return (Builder) super.encoders(encoders); + } + + /** + * {@inheritDoc} + * + * @param encoders {@inheritDoc} + * @return {@inheritDoc} + */ + @Override + public Builder encoders(List encoders) { + return (Builder) super.encoders(encoders); + } + @Override public Builder decoder(Decoder decoder) { return (Builder) super.decoder(decoder); diff --git a/jackson-jaxb/README.md b/jackson-jaxb/README.md index a9bdbf4ca..6015ae7a4 100644 --- a/jackson-jaxb/README.md +++ b/jackson-jaxb/README.md @@ -7,7 +7,7 @@ Add `JacksonJaxbJsonEncoder` and/or `JacksonJaxbJsonDecoder` to your `Feign.Buil ```java GitHub github = Feign.builder() - .encoder(new JacksonJaxbJsonEncoder()) + .encoders(new JacksonJaxbJsonEncoder()) .decoder(new JacksonJaxbJsonDecoder()) .target(GitHub.class, "https://api.github.com"); ``` @@ -21,7 +21,7 @@ ObjectMapper mapper = new ObjectMapper() .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); GitHub github = Feign.builder() - .encoder(new JacksonJaxbJsonEncoder(mapper)) + .encoders(new JacksonJaxbJsonEncoder(mapper)) .decoder(new JacksonJaxbJsonDecoder(mapper)) .target(GitHub.class, "https://api.github.com"); ``` 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..a49f64d29 100644 --- a/jackson-jaxb/src/main/java/feign/jackson/jaxb/JacksonJaxbJsonEncoder.java +++ b/jackson-jaxb/src/main/java/feign/jackson/jaxb/JacksonJaxbJsonEncoder.java @@ -23,12 +23,12 @@ import feign.Request; import feign.RequestTemplate; import feign.codec.EncodeException; -import feign.codec.Encoder; +import feign.codec.JsonEncoder; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.lang.reflect.Type; -public final class JacksonJaxbJsonEncoder implements Encoder { +public final class JacksonJaxbJsonEncoder implements JsonEncoder { private final JacksonJaxbJsonProvider jacksonJaxbJsonProvider; public JacksonJaxbJsonEncoder() { diff --git a/jackson-jr/README.md b/jackson-jr/README.md index fcc586579..0b594b9f1 100644 --- a/jackson-jr/README.md +++ b/jackson-jr/README.md @@ -9,7 +9,7 @@ Add `JacksonJrEncoder` and/or `JacksonJrDecoder` to your `Feign.Builder` like so ```java GitHub github = Feign.builder() - .encoder(new JacksonJrEncoder()) + .encoders(new JacksonJrEncoder()) .decoder(new JacksonJrDecoder()) .target(GitHub.class, "https://api.github.com"); ``` @@ -24,7 +24,7 @@ JSON json = Json.builder() .build(); GitHub github = Feign.builder() - .encoder(new JacksonJrEncoder(json)) + .encoders(new JacksonJrEncoder(json)) .decoder(new JacksonJrDecoder(json)) .target(GitHub.class, "https://api.github.com"); ``` @@ -37,7 +37,7 @@ List extensions = singletonList(JacksonAnnotationExtension.b .withVisibility(JsonAutoDetect.Value.defaultVisibility()) .build()); GitHub github = Feign.builder() - .encoder(new JacksonJrEncoder(extensions)) + .encoders(new JacksonJrEncoder(extensions)) .decoder(new JacksonJrDecoder(extensions)) .target(GitHub.class, "https://api.github.com"); ``` 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..2104ab056 100644 --- a/jackson-jr/src/main/java/feign/jackson/jr/JacksonJrEncoder.java +++ b/jackson-jr/src/main/java/feign/jackson/jr/JacksonJrEncoder.java @@ -21,11 +21,12 @@ import feign.RequestTemplate; import feign.codec.EncodeException; import feign.codec.Encoder; +import feign.codec.JsonEncoder; import java.io.IOException; import java.lang.reflect.Type; /** A {@link Encoder} that uses Jackson Jr to convert objects to String or byte representation. */ -public class JacksonJrEncoder extends JacksonJrMapper implements Encoder { +public class JacksonJrEncoder extends JacksonJrMapper implements JsonEncoder { public JacksonJrEncoder() { super(); diff --git a/jackson/README.md b/jackson/README.md index 31e8cbd43..e4e59da5e 100644 --- a/jackson/README.md +++ b/jackson/README.md @@ -28,7 +28,7 @@ You can also configure the encoder and decoder separately: ```java GitHub github = Feign.builder() - .encoder(new JacksonEncoder()) + .encoders(new JacksonEncoder()) .decoder(new JacksonDecoder()) .target(GitHub.class, "https://api.github.com"); ``` diff --git a/jackson3/README.md b/jackson3/README.md index 457fd6cce..b746ee805 100644 --- a/jackson3/README.md +++ b/jackson3/README.md @@ -31,7 +31,7 @@ You can also configure the encoder and decoder separately: ```java GitHub github = Feign.builder() - .encoder(new Jackson3Encoder()) + .encoders(new Jackson3Encoder()) .decoder(new Jackson3Decoder()) .target(GitHub.class, "https://api.github.com"); ``` diff --git a/java11/src/test/java/feign/http2client/test/Http2ClientAsyncTest.java b/java11/src/test/java/feign/http2client/test/Http2ClientAsyncTest.java index d8eb6c122..921108199 100644 --- a/java11/src/test/java/feign/http2client/test/Http2ClientAsyncTest.java +++ b/java11/src/test/java/feign/http2client/test/Http2ClientAsyncTest.java @@ -172,6 +172,11 @@ void bodyTypeCorrespondsWithParameterType() throws Exception { public void encode(Object object, Type bodyType, RequestTemplate template) { encodedType.set(bodyType); } + + @Override + public boolean canEncode(Object object, Type bodyType, RequestTemplate template) { + return true; + } }) .target("http://localhost:" + server.getPort()); @@ -648,8 +653,17 @@ void okIfEncodeRootCauseHasNoMessage() throws Throwable { final TestInterfaceAsync api = newAsyncBuilder() .encoder( - (_, _, _) -> { - throw new RuntimeException(); + new Encoder() { + @Override + public void encode(Object object, Type bodyType, RequestTemplate template) + throws EncodeException { + throw new RuntimeException(); + } + + @Override + public boolean canEncode(Object object, Type bodyType, RequestTemplate template) { + return true; + } }) .target("http://localhost:" + server.getPort()); @@ -1029,12 +1043,21 @@ static final class TestInterfaceAsyncBuilder { AsyncFeign.builder() .client(new Http2Client()) .decoder(new DefaultDecoder()) - .encoder( - (object, _, template) -> { - if (object instanceof Map) { - template.body(Request.Body.of(new Gson().toJson(object))); - } else { - template.body(Request.Body.of(object.toString())); + .encoders( + new Encoder() { + @Override + public void encode(Object object, Type bodyType, RequestTemplate template) + throws EncodeException { + if (object instanceof Map) { + template.body(Request.Body.of(new Gson().toJson(object))); + } else { + template.body(Request.Body.of(object.toString())); + } + } + + @Override + public boolean canEncode(Object object, Type bodyType, RequestTemplate template) { + return true; } }); @@ -1044,7 +1067,7 @@ TestInterfaceAsyncBuilder requestInterceptor(RequestInterceptor requestIntercept } TestInterfaceAsyncBuilder encoder(Encoder encoder) { - delegate.encoder(encoder); + delegate.encoders(encoder); return this; } diff --git a/jaxb-jakarta/src/main/java/feign/jaxb/JAXBEncoder.java b/jaxb-jakarta/src/main/java/feign/jaxb/JAXBEncoder.java index a25845800..871f6396d 100644 --- a/jaxb-jakarta/src/main/java/feign/jaxb/JAXBEncoder.java +++ b/jaxb-jakarta/src/main/java/feign/jaxb/JAXBEncoder.java @@ -18,7 +18,7 @@ import feign.Request; import feign.RequestTemplate; import feign.codec.EncodeException; -import feign.codec.Encoder; +import feign.codec.XmlEncoder; import jakarta.xml.bind.JAXBException; import jakarta.xml.bind.Marshaller; import java.io.StringWriter; @@ -36,14 +36,14 @@ * .build(); * * api = Feign.builder() - * .encoder(new JAXBEncoder(jaxbFactory)) + * .encoders(new JAXBEncoder(jaxbFactory)) * .target(MyApi.class, "http://api"); * * *

The JAXBContextFactory should be reused across requests as it caches the created JAXB * contexts. */ -public class JAXBEncoder implements Encoder { +public class JAXBEncoder implements XmlEncoder { private final JAXBContextFactory jaxbContextFactory; diff --git a/jaxb/README.md b/jaxb/README.md index 8ba82407a..8477f0a02 100644 --- a/jaxb/README.md +++ b/jaxb/README.md @@ -20,7 +20,7 @@ You can also configure the encoder and decoder separately: ```java Response response = Feign.builder() - .encoder(new JAXBEncoder(jaxbFactory)) + .encoders(new JAXBEncoder(jaxbFactory)) .decoder(new JAXBDecoder(jaxbFactory)) .target(Response.class, "https://apihost"); ``` diff --git a/jaxb/src/main/java/feign/jaxb/JAXBEncoder.java b/jaxb/src/main/java/feign/jaxb/JAXBEncoder.java index 4c05d82e7..21d05c5d1 100644 --- a/jaxb/src/main/java/feign/jaxb/JAXBEncoder.java +++ b/jaxb/src/main/java/feign/jaxb/JAXBEncoder.java @@ -18,7 +18,7 @@ import feign.Request; import feign.RequestTemplate; import feign.codec.EncodeException; -import feign.codec.Encoder; +import feign.codec.XmlEncoder; import java.io.StringWriter; import java.lang.reflect.Type; import javax.xml.bind.JAXBException; @@ -36,14 +36,14 @@ * .build(); * * api = Feign.builder() - * .encoder(new JAXBEncoder(jaxbFactory)) + * .encoders(new JAXBEncoder(jaxbFactory)) * .target(MyApi.class, "http://api"); * * *

The JAXBContextFactory should be reused across requests as it caches the created JAXB * contexts. */ -public class JAXBEncoder implements Encoder { +public class JAXBEncoder implements XmlEncoder { private final JAXBContextFactory jaxbContextFactory; diff --git a/json/README.md b/json/README.md index 108e6b8f0..0deb0ac6b 100644 --- a/json/README.md +++ b/json/README.md @@ -8,7 +8,7 @@ Add `JsonEncoder` and/or `JsonDecoder` to your `Feign.Builder` like so: ```java api = Feign.builder() .decoder(new JsonDecoder()) - .encoder(new JsonEncoder()) + .encoders(new JsonEncoder()) .target(GitHub.class, "https://api"); ``` diff --git a/json/src/main/java/feign/json/JsonEncoder.java b/json/src/main/java/feign/json/JsonEncoder.java index 1ef3cd6fb..237e09ecd 100644 --- a/json/src/main/java/feign/json/JsonEncoder.java +++ b/json/src/main/java/feign/json/JsonEncoder.java @@ -42,7 +42,7 @@ * * GitHub github = Feign.builder() * .decoder(new JsonDecoder()) - * .encoder(new JsonEncoder()) + * .encoders(new JsonEncoder()) * .target(GitHub.class, "https://api.github.com"); * * JSONObject contributor = new JSONObject(); @@ -64,4 +64,17 @@ public void encode(Object object, Type bodyType, RequestTemplate template) throw new EncodeException(format("%s is not a type supported by this encoder.", bodyType)); } } + + /** + * {@inheritDoc} + * + * @param object {@inheritDoc} + * @param bodyType {@inheritDoc} + * @param template {@inheritDoc} + * @return {@inheritDoc} + */ + @Override + public boolean canEncode(Object object, Type bodyType, RequestTemplate template) { + return object == null || object instanceof JSONArray || object instanceof JSONObject; + } } diff --git a/json/src/test/java/feign/json/JsonCodecTest.java b/json/src/test/java/feign/json/JsonCodecTest.java index 9063222d4..620be45b3 100644 --- a/json/src/test/java/feign/json/JsonCodecTest.java +++ b/json/src/test/java/feign/json/JsonCodecTest.java @@ -54,7 +54,7 @@ void setUp() { github = Feign.builder() .decoder(new JsonDecoder()) - .encoder(new JsonEncoder()) + .encoders(new JsonEncoder()) .client(mockClient) .target(new MockTarget<>(GitHub.class)); } diff --git a/kotlin/src/main/java/feign/kotlin/CoroutineFeign.java b/kotlin/src/main/java/feign/kotlin/CoroutineFeign.java index b20f56aea..352a2f26a 100644 --- a/kotlin/src/main/java/feign/kotlin/CoroutineFeign.java +++ b/kotlin/src/main/java/feign/kotlin/CoroutineFeign.java @@ -172,7 +172,7 @@ public CoroutineFeign internalBuild() { .contract(contract) .retryer(retryer) .logger(logger) - .encoder(encoder) + .encoders(encoder) .queryMapEncoder(queryMapEncoder) .options(options) .requestInterceptors(requestInterceptors) diff --git a/kotlin/src/test/kotlin/feign/kotlin/CoroutineFeignTest.kt b/kotlin/src/test/kotlin/feign/kotlin/CoroutineFeignTest.kt index a25e3d5a8..e391c97a9 100644 --- a/kotlin/src/test/kotlin/feign/kotlin/CoroutineFeignTest.kt +++ b/kotlin/src/test/kotlin/feign/kotlin/CoroutineFeignTest.kt @@ -22,6 +22,7 @@ import feign.QueryMapEncoder import feign.Request import feign.RequestInterceptor import feign.RequestLine +import feign.RequestTemplate import feign.Response import feign.Util import feign.codec.Decoder @@ -189,13 +190,19 @@ 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())) + .decoder(DefaultDecoder()).encoders(object : Encoder { + override fun encode(`object`: Any?, bodyType: Type, template: RequestTemplate) { + if (`object` is Map<*, *>) { + template.body(Request.Body.of(Gson().toJson(`object`))) + } else { + template.body(Request.Body.of(`object`.toString())) + } } - } + + override fun canEncode(`object`: Any?, bodyType: Type, template: RequestTemplate): Boolean { + return true + } + }) fun requestInterceptor(requestInterceptor: RequestInterceptor?): TestInterfaceAsyncBuilder { delegate.requestInterceptor(requestInterceptor) @@ -203,7 +210,7 @@ class CoroutineFeignTest { } fun encoder(encoder: Encoder?): TestInterfaceAsyncBuilder { - delegate.encoder(encoder) + delegate.encoders(encoder) return this } diff --git a/micrometer/src/main/java/feign/micrometer/MeteredEncoder.java b/micrometer/src/main/java/feign/micrometer/MeteredEncoder.java index 784aa86c3..be2510b38 100644 --- a/micrometer/src/main/java/feign/micrometer/MeteredEncoder.java +++ b/micrometer/src/main/java/feign/micrometer/MeteredEncoder.java @@ -65,6 +65,19 @@ public void encode(Object object, Type bodyType, RequestTemplate template) createSummary(object, bodyType, template).record(Long.parseLong(contentLength))); } + /** + * {@inheritDoc} + * + * @param object {@inheritDoc} + * @param bodyType {@inheritDoc} + * @param template {@inheritDoc} + * @return {@inheritDoc} + */ + @Override + public boolean canEncode(Object object, Type bodyType, RequestTemplate template) { + return encoder.canEncode(object, bodyType, template); + } + protected Timer createTimer(Object object, Type bodyType, RequestTemplate template) { final Tags allTags = metricTagResolver.tag( diff --git a/moshi/README.md b/moshi/README.md index 6715ad4e7..e97e1ef83 100644 --- a/moshi/README.md +++ b/moshi/README.md @@ -15,7 +15,7 @@ You can also configure the encoder and decoder separately: ```java GitHub github = Feign.builder() - .encoder(new MoshiEncoder()) + .encoders(new MoshiEncoder()) .decoder(new MoshiDecoder()) .target(GitHub.class, "https://api.github.com"); ``` diff --git a/moshi/src/test/java/feign/moshi/examples/GithubExample.java b/moshi/src/test/java/feign/moshi/examples/GithubExample.java index b1e5e2c04..752cee5fd 100644 --- a/moshi/src/test/java/feign/moshi/examples/GithubExample.java +++ b/moshi/src/test/java/feign/moshi/examples/GithubExample.java @@ -27,7 +27,7 @@ public class GithubExample { public static void main(String... args) { GitHub github = Feign.builder() - .encoder(new MoshiEncoder()) + .encoders(new MoshiEncoder()) .decoder(new MoshiDecoder()) .target(GitHub.class, "https://api.github.com"); diff --git a/okhttp/src/test/java/feign/okhttp/OkHttpClientAsyncTest.java b/okhttp/src/test/java/feign/okhttp/OkHttpClientAsyncTest.java index d923c3f10..d2b6fce7c 100644 --- a/okhttp/src/test/java/feign/okhttp/OkHttpClientAsyncTest.java +++ b/okhttp/src/test/java/feign/okhttp/OkHttpClientAsyncTest.java @@ -171,6 +171,11 @@ void bodyTypeCorrespondsWithParameterType() throws Exception { public void encode(Object object, Type bodyType, RequestTemplate template) { encodedType.set(bodyType); } + + @Override + public boolean canEncode(Object object, Type bodyType, RequestTemplate template) { + return true; + } }) .target("http://localhost:" + server.getPort()); @@ -647,8 +652,17 @@ void okIfEncodeRootCauseHasNoMessage() throws Throwable { final TestInterfaceAsync api = newAsyncBuilder() .encoder( - (_, _, _) -> { - throw new RuntimeException(); + new Encoder() { + @Override + public void encode(Object object, Type bodyType, RequestTemplate template) + throws EncodeException { + throw new RuntimeException(); + } + + @Override + public boolean canEncode(Object object, Type bodyType, RequestTemplate template) { + return true; + } }) .target("http://localhost:" + server.getPort()); @@ -1028,12 +1042,21 @@ static final class TestInterfaceAsyncBuilder { AsyncFeign.builder() .client(new OkHttpClient()) .decoder(new DefaultDecoder()) - .encoder( - (object, _, template) -> { - if (object instanceof Map) { - template.body(Request.Body.of(new Gson().toJson(object))); - } else { - template.body(Request.Body.of(object.toString())); + .encoders( + new Encoder() { + @Override + public void encode(Object object, Type bodyType, RequestTemplate template) + throws EncodeException { + if (object instanceof Map) { + template.body(Request.Body.of(new Gson().toJson(object))); + } else { + template.body(Request.Body.of(object.toString())); + } + } + + @Override + public boolean canEncode(Object object, Type bodyType, RequestTemplate template) { + return true; } }); @@ -1043,7 +1066,7 @@ TestInterfaceAsyncBuilder requestInterceptor(RequestInterceptor requestIntercept } TestInterfaceAsyncBuilder encoder(Encoder encoder) { - delegate.encoder(encoder); + delegate.encoders(encoder); return this; } diff --git a/reactive/src/test/java/feign/reactive/ReactiveFeignIntegrationTest.java b/reactive/src/test/java/feign/reactive/ReactiveFeignIntegrationTest.java index d892170fc..c3942c65e 100644 --- a/reactive/src/test/java/feign/reactive/ReactiveFeignIntegrationTest.java +++ b/reactive/src/test/java/feign/reactive/ReactiveFeignIntegrationTest.java @@ -113,7 +113,7 @@ void reactorTargetFull() throws Exception { TestReactorService service = ReactorFeign.builder() - .encoder(new JacksonEncoder()) + .encoders(new JacksonEncoder()) .decoder(new ReactorDecoder(new JacksonDecoder())) .logger(new ConsoleLogger()) .dismiss404() @@ -154,7 +154,7 @@ void rxJavaTarget() throws Exception { TestReactiveXService service = RxJavaFeign.builder() - .encoder(new JacksonEncoder()) + .encoders(new JacksonEncoder()) .decoder(new RxJavaDecoder(new JacksonDecoder())) .logger(new ConsoleLogger()) .logLevel(Level.FULL) diff --git a/soap-jakarta/README.md b/soap-jakarta/README.md index dfac4cc43..db9b7b4d7 100644 --- a/soap-jakarta/README.md +++ b/soap-jakarta/README.md @@ -42,7 +42,7 @@ You can also configure the encoder and decoder separately: ```java api = Feign.builder() - .encoder(new SOAPEncoder(jaxbFactory)) + .encoders(new SOAPEncoder(jaxbFactory)) .decoder(new SOAPDecoder(jaxbFactory)) .target(MyApi.class, "http://api"); ``` diff --git a/soap-jakarta/src/main/java/feign/soap/SOAPEncoder.java b/soap-jakarta/src/main/java/feign/soap/SOAPEncoder.java index 31bc76830..dd05f673e 100644 --- a/soap-jakarta/src/main/java/feign/soap/SOAPEncoder.java +++ b/soap-jakarta/src/main/java/feign/soap/SOAPEncoder.java @@ -18,7 +18,7 @@ import feign.Request; import feign.RequestTemplate; import feign.codec.EncodeException; -import feign.codec.Encoder; +import feign.codec.XmlEncoder; import feign.jaxb.JAXBContextFactory; import jakarta.xml.bind.JAXBException; import jakarta.xml.bind.Marshaller; @@ -68,7 +68,7 @@ * .build(); * * api = Feign.builder() - * .encoder(new SOAPEncoder(jaxbFactory)) + * .encoders(new SOAPEncoder(jaxbFactory)) * .target(MyApi.class, "http://api"); * * ... @@ -83,7 +83,7 @@ *

The JAXBContextFactory should be reused across requests as it caches the created JAXB * contexts. */ -public class SOAPEncoder implements Encoder { +public class SOAPEncoder implements XmlEncoder { private static final String DEFAULT_SOAP_PROTOCOL = SOAPConstants.SOAP_1_1_PROTOCOL; diff --git a/soap/README.md b/soap/README.md index dfac4cc43..db9b7b4d7 100644 --- a/soap/README.md +++ b/soap/README.md @@ -42,7 +42,7 @@ You can also configure the encoder and decoder separately: ```java api = Feign.builder() - .encoder(new SOAPEncoder(jaxbFactory)) + .encoders(new SOAPEncoder(jaxbFactory)) .decoder(new SOAPDecoder(jaxbFactory)) .target(MyApi.class, "http://api"); ``` diff --git a/soap/src/main/java/feign/soap/SOAPEncoder.java b/soap/src/main/java/feign/soap/SOAPEncoder.java index 711c44171..8ac4b7e59 100644 --- a/soap/src/main/java/feign/soap/SOAPEncoder.java +++ b/soap/src/main/java/feign/soap/SOAPEncoder.java @@ -18,7 +18,7 @@ import feign.Request; import feign.RequestTemplate; import feign.codec.EncodeException; -import feign.codec.Encoder; +import feign.codec.XmlEncoder; import feign.jaxb.JAXBContextFactory; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -68,7 +68,7 @@ * .build(); * * api = Feign.builder() - * .encoder(new SOAPEncoder(jaxbFactory)) + * .encoders(new SOAPEncoder(jaxbFactory)) * .target(MyApi.class, "http://api"); * * ... @@ -83,7 +83,7 @@ *

The JAXBContextFactory should be reused across requests as it caches the created JAXB * contexts. */ -public class SOAPEncoder implements Encoder { +public class SOAPEncoder implements XmlEncoder { private static final String DEFAULT_SOAP_PROTOCOL = SOAPConstants.SOAP_1_1_PROTOCOL; diff --git a/spring/src/test/java/feign/spring/SpringContractTest.java b/spring/src/test/java/feign/spring/SpringContractTest.java index b105ce25d..0316f9f71 100755 --- a/spring/src/test/java/feign/spring/SpringContractTest.java +++ b/spring/src/test/java/feign/spring/SpringContractTest.java @@ -20,6 +20,7 @@ import feign.Feign; import feign.Param; import feign.Request; +import feign.RequestTemplate; import feign.Response; import feign.ResponseMapper; import feign.jackson.JacksonDecoder; @@ -89,7 +90,13 @@ void setup() throws IOException { resource = Feign.builder() .contract(new SpringContract()) - .encoder(new JacksonEncoder()) + .encoders( + new JacksonEncoder() { + @Override + public boolean canEncode(Object object, Type bodyType, RequestTemplate template) { + return true; + } + }) .mapAndDecode(new TextResponseMapper(), new JacksonDecoder()) .client(mockClient) .target(new MockTarget<>(HealthResource.class)); diff --git a/validation-jakarta/src/test/java/feign/validation/BeanValidationMethodInterceptorTest.java b/validation-jakarta/src/test/java/feign/validation/BeanValidationMethodInterceptorTest.java index 970748ae7..2fec4c072 100644 --- a/validation-jakarta/src/test/java/feign/validation/BeanValidationMethodInterceptorTest.java +++ b/validation-jakarta/src/test/java/feign/validation/BeanValidationMethodInterceptorTest.java @@ -22,11 +22,15 @@ import feign.Param; import feign.Request; import feign.RequestLine; +import feign.RequestTemplate; +import feign.codec.EncodeException; +import feign.codec.Encoder; import jakarta.validation.ConstraintViolationException; import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import java.io.IOException; +import java.lang.reflect.Type; import mockwebserver3.MockResponse; import mockwebserver3.MockWebServer; import org.junit.jupiter.api.AfterEach; @@ -84,7 +88,19 @@ interface Api { private Api api() { return Feign.builder() - .encoder((object, _, template) -> template.body(Request.Body.of(String.valueOf(object)))) + .encoders( + new Encoder() { + @Override + public void encode(Object object, Type bodyType, RequestTemplate template) + throws EncodeException { + template.body(Request.Body.of(String.valueOf(object))); + } + + @Override + public boolean canEncode(Object object, Type bodyType, RequestTemplate template) { + 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..a891428c0 100644 --- a/validation/src/test/java/feign/validation/BeanValidationMethodInterceptorTest.java +++ b/validation/src/test/java/feign/validation/BeanValidationMethodInterceptorTest.java @@ -22,7 +22,11 @@ import feign.Param; import feign.Request; import feign.RequestLine; +import feign.RequestTemplate; +import feign.codec.EncodeException; +import feign.codec.Encoder; import java.io.IOException; +import java.lang.reflect.Type; import javax.validation.ConstraintViolationException; import javax.validation.Valid; import javax.validation.constraints.NotBlank; @@ -84,7 +88,19 @@ interface Api { private Api api() { return Feign.builder() - .encoder((object, _, template) -> template.body(Request.Body.of(String.valueOf(object)))) + .encoders( + new Encoder() { + @Override + public void encode(Object object, Type bodyType, RequestTemplate template) + throws EncodeException { + template.body(Request.Body.of(String.valueOf(object))); + } + + @Override + public boolean canEncode(Object object, Type bodyType, RequestTemplate template) { + return true; + } + }) .methodInterceptor(BeanValidationMethodInterceptor.usingDefaultFactory()) .target(Api.class, "http://localhost:" + server.getPort()); } diff --git a/vertx/README.md b/vertx/README.md index 4e89037ac..9ce2d558d 100644 --- a/vertx/README.md +++ b/vertx/README.md @@ -67,7 +67,7 @@ WebClient webClient = WebClient.create(vertx); // create Vert.x WebClient IcecreamServiceApi icecreamApi = VertxFeign .builder() .webClient(webClient) // provide WebClient instance - .encoder(new JacksonEncoder()) + .encoders(new JacksonEncoder()) .decoder(new JacksonDecoder()) .target(IcecreamServiceApi.class, "https://www.icecream.com"); diff --git a/vertx/feign-vertx/src/main/java/feign/VertxFeign.java b/vertx/feign-vertx/src/main/java/feign/VertxFeign.java index 36945ed6f..8e1310964 100644 --- a/vertx/feign-vertx/src/main/java/feign/VertxFeign.java +++ b/vertx/feign-vertx/src/main/java/feign/VertxFeign.java @@ -20,6 +20,7 @@ import feign.InvocationHandlerFactory.MethodHandler; import feign.codec.Decoder; +import feign.codec.DelegatingEncoder; import feign.codec.Encoder; import feign.codec.ErrorDecoder; import feign.core.DefaultContract; @@ -38,6 +39,7 @@ import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -192,14 +194,39 @@ public Builder logger(final Logger logger) { } /** - * Sets encoder. + * {@inheritDoc} * - * @param encoder encoder - * @return this builder + * @deprecated use {@link #encoders(Encoder...)} or {@link #encoders(List)} instead. + * @param encoder {@inheritDoc} + * @return {@inheritDoc} */ @Override + @Deprecated(since = "14", forRemoval = true) public Builder encoder(final Encoder encoder) { - this.encoder = checkNotNull(encoder, "Argument encoder must be not null"); + return encoders(checkNotNull(encoder, "Argument encoder must be not null")); + } + + /** + * {@inheritDoc} + * + * @param encoders {@inheritDoc} + * @return {@inheritDoc} + */ + @Override + public Builder encoders(final Encoder... encoders) { + return encoders(Arrays.asList(encoders)); + } + + /** + * {@inheritDoc} + * + * @param encoders {@inheritDoc} + * @return {@inheritDoc} + */ + @Override + public Builder encoders(final List encoders) { + this.encoder = + new DelegatingEncoder(checkNotNull(encoders, "Argument encoders must be not null")); return this; } diff --git a/vertx/feign-vertx/src/test/java/feign/vertx/ConnectionsLeakTests.java b/vertx/feign-vertx/src/test/java/feign/vertx/ConnectionsLeakTests.java index baabdc4c9..2e522b065 100644 --- a/vertx/feign-vertx/src/test/java/feign/vertx/ConnectionsLeakTests.java +++ b/vertx/feign-vertx/src/test/java/feign/vertx/ConnectionsLeakTests.java @@ -87,7 +87,7 @@ void http11NoConnectionLeak(Vertx vertx, VertxTestContext testContext) { VertxFeign.builder() .vertx(vertx) .webClient(webClient) - .encoder(new JacksonEncoder()) + .encoders(new JacksonEncoder()) .decoder(new JacksonDecoder()) .target(HelloServiceAPI.class, "http://localhost:8091"); @@ -115,7 +115,7 @@ void http2NoConnectionLeak(Vertx vertx, VertxTestContext testContext) { VertxFeign.builder() .vertx(vertx) .webClient(webClient) - .encoder(new JacksonEncoder()) + .encoders(new JacksonEncoder()) .decoder(new JacksonDecoder()) .target(HelloServiceAPI.class, "http://localhost:8091"); diff --git a/vertx/feign-vertx/src/test/java/feign/vertx/Http11ClientReconnectTest.java b/vertx/feign-vertx/src/test/java/feign/vertx/Http11ClientReconnectTest.java index 88dab3713..c37216d3b 100644 --- a/vertx/feign-vertx/src/test/java/feign/vertx/Http11ClientReconnectTest.java +++ b/vertx/feign-vertx/src/test/java/feign/vertx/Http11ClientReconnectTest.java @@ -40,7 +40,7 @@ protected void createClient(final Vertx vertx) { VertxFeign.builder() .vertx(vertx) .webClient(webClient) - .encoder(new JacksonEncoder()) + .encoders(new JacksonEncoder()) .decoder(new JacksonDecoder()) .target(HelloServiceAPI.class, baseUrl); } diff --git a/vertx/feign-vertx/src/test/java/feign/vertx/Http2ClientReconnectTest.java b/vertx/feign-vertx/src/test/java/feign/vertx/Http2ClientReconnectTest.java index 522edac84..d429d90e5 100644 --- a/vertx/feign-vertx/src/test/java/feign/vertx/Http2ClientReconnectTest.java +++ b/vertx/feign-vertx/src/test/java/feign/vertx/Http2ClientReconnectTest.java @@ -42,7 +42,7 @@ protected void createClient(Vertx vertx) { client = VertxFeign.builder() .webClient(webClient) - .encoder(new JacksonEncoder()) + .encoders(new JacksonEncoder()) .decoder(new JacksonDecoder()) .target(HelloServiceAPI.class, baseUrl); } diff --git a/vertx/feign-vertx/src/test/java/feign/vertx/RawContractTest.java b/vertx/feign-vertx/src/test/java/feign/vertx/RawContractTest.java index e81662247..8a2d8d8c0 100644 --- a/vertx/feign-vertx/src/test/java/feign/vertx/RawContractTest.java +++ b/vertx/feign-vertx/src/test/java/feign/vertx/RawContractTest.java @@ -47,7 +47,7 @@ static void createClient(Vertx vertx) { VertxFeign.builder() .vertx(vertx) .webClient(WebClient.create(vertx)) - .encoder(new JacksonEncoder(TestUtils.MAPPER)) + .encoders(new JacksonEncoder(TestUtils.MAPPER)) .decoder(new JacksonDecoder(TestUtils.MAPPER)) .target(RawServiceAPI.class, wireMock.baseUrl()); } diff --git a/vertx/feign-vertx/src/test/java/feign/vertx/VertxHttpClientTest.java b/vertx/feign-vertx/src/test/java/feign/vertx/VertxHttpClientTest.java index 7182080ec..eb3a3c13f 100644 --- a/vertx/feign-vertx/src/test/java/feign/vertx/VertxHttpClientTest.java +++ b/vertx/feign-vertx/src/test/java/feign/vertx/VertxHttpClientTest.java @@ -220,7 +220,7 @@ void createClient(Vertx vertx) { VertxFeign.builder() .vertx(vertx) .webClient(WebClient.create(vertx)) - .encoder(new JacksonEncoder(TestUtils.MAPPER)) + .encoders(new JacksonEncoder(TestUtils.MAPPER)) .decoder(new JacksonDecoder(TestUtils.MAPPER)) .target(IcecreamServiceApi.class, wireMock.baseUrl()); } diff --git a/vertx/feign-vertx4-test/src/test/java/feign/vertx/ConnectionsLeakTests.java b/vertx/feign-vertx4-test/src/test/java/feign/vertx/ConnectionsLeakTests.java index 3006d493c..6a3b124c6 100644 --- a/vertx/feign-vertx4-test/src/test/java/feign/vertx/ConnectionsLeakTests.java +++ b/vertx/feign-vertx4-test/src/test/java/feign/vertx/ConnectionsLeakTests.java @@ -90,7 +90,7 @@ void http11NoConnectionLeak(Vertx vertx, VertxTestContext testContext) { VertxFeign.builder() .vertx(vertx) .webClient(webClient) - .encoder(new JacksonEncoder()) + .encoders(new JacksonEncoder()) .decoder(new JacksonDecoder()) .target(HelloServiceAPI.class, "http://localhost:8091"); @@ -118,7 +118,7 @@ void http2NoConnectionLeak(Vertx vertx, VertxTestContext testContext) { VertxFeign.builder() .vertx(vertx) .webClient(webClient) - .encoder(new JacksonEncoder()) + .encoders(new JacksonEncoder()) .decoder(new JacksonDecoder()) .target(HelloServiceAPI.class, "http://localhost:8091"); diff --git a/vertx/feign-vertx4-test/src/test/java/feign/vertx/Http11ClientReconnectTest.java b/vertx/feign-vertx4-test/src/test/java/feign/vertx/Http11ClientReconnectTest.java index bb6edaf4a..eb4f1c6ad 100644 --- a/vertx/feign-vertx4-test/src/test/java/feign/vertx/Http11ClientReconnectTest.java +++ b/vertx/feign-vertx4-test/src/test/java/feign/vertx/Http11ClientReconnectTest.java @@ -38,7 +38,7 @@ protected void createClient(final Vertx vertx) { VertxFeign.builder() .vertx(vertx) .webClient(webClient) - .encoder(new JacksonEncoder()) + .encoders(new JacksonEncoder()) .decoder(new JacksonDecoder()) .target(HelloServiceAPI.class, baseUrl); } diff --git a/vertx/feign-vertx4-test/src/test/java/feign/vertx/Http2ClientReconnectTest.java b/vertx/feign-vertx4-test/src/test/java/feign/vertx/Http2ClientReconnectTest.java index e6612729c..148c60d0a 100644 --- a/vertx/feign-vertx4-test/src/test/java/feign/vertx/Http2ClientReconnectTest.java +++ b/vertx/feign-vertx4-test/src/test/java/feign/vertx/Http2ClientReconnectTest.java @@ -41,7 +41,7 @@ protected void createClient(Vertx vertx) { client = VertxFeign.builder() .webClient(webClient) - .encoder(new JacksonEncoder()) + .encoders(new JacksonEncoder()) .decoder(new JacksonDecoder()) .target(HelloServiceAPI.class, baseUrl); } diff --git a/vertx/feign-vertx4-test/src/test/java/feign/vertx/RawContractTest.java b/vertx/feign-vertx4-test/src/test/java/feign/vertx/RawContractTest.java index e81662247..8a2d8d8c0 100644 --- a/vertx/feign-vertx4-test/src/test/java/feign/vertx/RawContractTest.java +++ b/vertx/feign-vertx4-test/src/test/java/feign/vertx/RawContractTest.java @@ -47,7 +47,7 @@ static void createClient(Vertx vertx) { VertxFeign.builder() .vertx(vertx) .webClient(WebClient.create(vertx)) - .encoder(new JacksonEncoder(TestUtils.MAPPER)) + .encoders(new JacksonEncoder(TestUtils.MAPPER)) .decoder(new JacksonDecoder(TestUtils.MAPPER)) .target(RawServiceAPI.class, wireMock.baseUrl()); } diff --git a/vertx/feign-vertx4-test/src/test/java/feign/vertx/VertxHttpClientTest.java b/vertx/feign-vertx4-test/src/test/java/feign/vertx/VertxHttpClientTest.java index 7182080ec..eb3a3c13f 100644 --- a/vertx/feign-vertx4-test/src/test/java/feign/vertx/VertxHttpClientTest.java +++ b/vertx/feign-vertx4-test/src/test/java/feign/vertx/VertxHttpClientTest.java @@ -220,7 +220,7 @@ void createClient(Vertx vertx) { VertxFeign.builder() .vertx(vertx) .webClient(WebClient.create(vertx)) - .encoder(new JacksonEncoder(TestUtils.MAPPER)) + .encoders(new JacksonEncoder(TestUtils.MAPPER)) .decoder(new JacksonDecoder(TestUtils.MAPPER)) .target(IcecreamServiceApi.class, wireMock.baseUrl()); } From cd9b364bfd6d458efcfccdf324547bba6eabd898 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 14 Jul 2026 05:13:23 +0000 Subject: [PATCH 18/63] build(deps-dev): Bump vertx.version in /vertx/feign-vertx4-test Bumps `vertx.version` from 4.5.29 to 4.5.30. Updates `io.vertx:vertx-junit5` from 4.5.29 to 4.5.30 - [Commits](https://github.com/eclipse-vertx/vertx-junit5/compare/4.5.29...4.5.30) Updates `io.vertx:vertx-web-client` from 4.5.29 to 4.5.30 - [Commits](https://github.com/vert-x3/vertx-web/compare/4.5.29...4.5.30) --- updated-dependencies: - dependency-name: io.vertx:vertx-junit5 dependency-version: 4.5.30 dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: io.vertx:vertx-web-client dependency-version: 4.5.30 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- vertx/feign-vertx4-test/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vertx/feign-vertx4-test/pom.xml b/vertx/feign-vertx4-test/pom.xml index f2f3ce617..88fe45da5 100644 --- a/vertx/feign-vertx4-test/pom.xml +++ b/vertx/feign-vertx4-test/pom.xml @@ -30,7 +30,7 @@ Tests with Vertx 4.x. - 4.5.29 + 4.5.30 From a3403f0cf2f56610d666f0bdeaf9f1548b1489dc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 14 Jul 2026 05:13:24 +0000 Subject: [PATCH 19/63] build(deps-dev): Bump vertx.version in /vertx/feign-vertx5-test Bumps `vertx.version` from 5.1.4 to 5.1.5. Updates `io.vertx:vertx-junit5` from 5.1.4 to 5.1.5 - [Commits](https://github.com/eclipse-vertx/vertx-junit5/compare/5.1.4...5.1.5) Updates `io.vertx:vertx-web-client` from 5.1.4 to 5.1.5 - [Commits](https://github.com/vert-x3/vertx-web/compare/5.1.4...5.1.5) --- updated-dependencies: - dependency-name: io.vertx:vertx-junit5 dependency-version: 5.1.5 dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: io.vertx:vertx-web-client dependency-version: 5.1.5 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- vertx/feign-vertx5-test/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vertx/feign-vertx5-test/pom.xml b/vertx/feign-vertx5-test/pom.xml index b18333fd8..e97876b8c 100644 --- a/vertx/feign-vertx5-test/pom.xml +++ b/vertx/feign-vertx5-test/pom.xml @@ -30,7 +30,7 @@ Tests with Vertx 5.x. - 5.1.4 + 5.1.5 From e9b0f3540d707c66d07c381dac64e81ef91731c7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 Jul 2026 05:21:33 +0000 Subject: [PATCH 20/63] build(deps): Bump kotlin.version from 2.4.0 to 2.4.10 Bumps `kotlin.version` from 2.4.0 to 2.4.10. Updates `org.jetbrains.kotlin:kotlin-stdlib-jdk8` from 2.4.0 to 2.4.10 - [Release notes](https://github.com/JetBrains/kotlin/releases) - [Changelog](https://github.com/JetBrains/kotlin/blob/master/ChangeLog.md) - [Commits](https://github.com/JetBrains/kotlin/compare/v2.4.0...v2.4.10) Updates `org.jetbrains.kotlin:kotlin-reflect` from 2.4.0 to 2.4.10 - [Release notes](https://github.com/JetBrains/kotlin/releases) - [Changelog](https://github.com/JetBrains/kotlin/blob/master/ChangeLog.md) - [Commits](https://github.com/JetBrains/kotlin/compare/v2.4.0...v2.4.10) Updates `org.jetbrains.kotlin:kotlin-maven-plugin` from 2.4.0 to 2.4.10 --- updated-dependencies: - dependency-name: org.jetbrains.kotlin:kotlin-stdlib-jdk8 dependency-version: 2.4.10 dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.jetbrains.kotlin:kotlin-reflect dependency-version: 2.4.10 dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.jetbrains.kotlin:kotlin-maven-plugin dependency-version: 2.4.10 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- kotlin/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kotlin/pom.xml b/kotlin/pom.xml index 8200b3135..4f757250d 100644 --- a/kotlin/pom.xml +++ b/kotlin/pom.xml @@ -30,7 +30,7 @@ Feign Kotlin - 2.4.0 + 2.4.10 1.11.0 From 855c1f688b0915f4efb807df1808b43f77fe04f8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 Jul 2026 05:21:39 +0000 Subject: [PATCH 21/63] build(deps-dev): Bump org.openrewrite.maven:rewrite-maven-plugin Bumps [org.openrewrite.maven:rewrite-maven-plugin](https://github.com/openrewrite/rewrite-maven-plugin) from 6.43.0 to 6.44.0. - [Release notes](https://github.com/openrewrite/rewrite-maven-plugin/releases) - [Commits](https://github.com/openrewrite/rewrite-maven-plugin/compare/v6.43.0...v6.44.0) --- updated-dependencies: - dependency-name: org.openrewrite.maven:rewrite-maven-plugin dependency-version: 6.44.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 24f1dab12..752aaa253 100644 --- a/pom.xml +++ b/pom.xml @@ -205,7 +205,7 @@ 3.2.0 1.2.8 4.0.0 - 6.43.0 + 6.44.0 3.41.0 3.39.0 0.26.1 From b5ab18f2e6417d9b9f25bbe232a81a6b0f823430 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 Jul 2026 05:22:19 +0000 Subject: [PATCH 22/63] build(deps): Bump org.openrewrite.recipe:rewrite-migrate-java Bumps [org.openrewrite.recipe:rewrite-migrate-java](https://github.com/openrewrite/rewrite-migrate-java) from 3.39.0 to 3.40.0. - [Release notes](https://github.com/openrewrite/rewrite-migrate-java/releases) - [Commits](https://github.com/openrewrite/rewrite-migrate-java/compare/v3.39.0...v3.40.0) --- updated-dependencies: - dependency-name: org.openrewrite.recipe:rewrite-migrate-java dependency-version: 3.40.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 24f1dab12..f2441eda3 100644 --- a/pom.xml +++ b/pom.xml @@ -207,7 +207,7 @@ 4.0.0 6.43.0 3.41.0 - 3.39.0 + 3.40.0 0.26.1 1.0 From e461dc4f4a99c3f684198c435d6f835adebeeb6a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 Jul 2026 05:34:23 +0000 Subject: [PATCH 23/63] build(deps): Bump org.openrewrite.recipe:rewrite-testing-frameworks Bumps [org.openrewrite.recipe:rewrite-testing-frameworks](https://github.com/openrewrite/rewrite-testing-frameworks) from 3.41.0 to 3.42.0. - [Release notes](https://github.com/openrewrite/rewrite-testing-frameworks/releases) - [Commits](https://github.com/openrewrite/rewrite-testing-frameworks/compare/v3.41.0...v3.42.0) --- updated-dependencies: - dependency-name: org.openrewrite.recipe:rewrite-testing-frameworks dependency-version: 3.42.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index e7dec3a82..94e7b9f7a 100644 --- a/pom.xml +++ b/pom.xml @@ -206,7 +206,7 @@ 1.2.8 4.0.0 6.44.0 - 3.41.0 + 3.42.0 3.40.0 0.26.1 1.0 From df2c15b9ae1afbf08b3de5a3d696dff26e3dc929 Mon Sep 17 00:00:00 2001 From: Yevhen Vasyliev Date: Wed, 15 Jul 2026 16:19:01 +0300 Subject: [PATCH 24/63] feat: make `feign.json.JsonEncoder` implement `feign.codec.JsonEncoder` Co-authored-by: trumpetinc <6618744+trumpetinc@users.noreply.github.com> --- json/src/main/java/feign/json/JsonEncoder.java | 16 +--------------- json/src/test/java/feign/json/JsonCodecTest.java | 2 ++ 2 files changed, 3 insertions(+), 15 deletions(-) diff --git a/json/src/main/java/feign/json/JsonEncoder.java b/json/src/main/java/feign/json/JsonEncoder.java index 237e09ecd..4b9ee99a5 100644 --- a/json/src/main/java/feign/json/JsonEncoder.java +++ b/json/src/main/java/feign/json/JsonEncoder.java @@ -20,7 +20,6 @@ import feign.Request; import feign.RequestTemplate; import feign.codec.EncodeException; -import feign.codec.Encoder; import java.lang.reflect.Type; import org.json.JSONArray; import org.json.JSONObject; @@ -52,7 +51,7 @@ * github.create("openfeign", "feign", contributor); * */ -public class JsonEncoder implements Encoder { +public class JsonEncoder implements feign.codec.JsonEncoder { @Override public void encode(Object object, Type bodyType, RequestTemplate template) @@ -64,17 +63,4 @@ public void encode(Object object, Type bodyType, RequestTemplate template) throw new EncodeException(format("%s is not a type supported by this encoder.", bodyType)); } } - - /** - * {@inheritDoc} - * - * @param object {@inheritDoc} - * @param bodyType {@inheritDoc} - * @param template {@inheritDoc} - * @return {@inheritDoc} - */ - @Override - public boolean canEncode(Object object, Type bodyType, RequestTemplate template) { - return object == null || object instanceof JSONArray || object instanceof JSONObject; - } } diff --git a/json/src/test/java/feign/json/JsonCodecTest.java b/json/src/test/java/feign/json/JsonCodecTest.java index 620be45b3..9624d811b 100644 --- a/json/src/test/java/feign/json/JsonCodecTest.java +++ b/json/src/test/java/feign/json/JsonCodecTest.java @@ -20,6 +20,7 @@ import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import feign.Feign; +import feign.Headers; import feign.Param; import feign.Request; import feign.RequestLine; @@ -39,6 +40,7 @@ interface GitHub { JSONArray contributors(@Param("owner") String owner, @Param("repo") String repo); @RequestLine("POST /repos/{owner}/{repo}/contributors") + @Headers("Content-Type: application/json") JSONObject create( @Param("owner") String owner, @Param("repo") String repo, JSONObject contributor); } From f130cceac71699c7828f0256b3d03be7d9eb877b Mon Sep 17 00:00:00 2001 From: Yevhen Vasyliev Date: Wed, 15 Jul 2026 16:32:53 +0300 Subject: [PATCH 25/63] docs: document new multi-encoder features Co-authored-by: trumpetinc <6618744+trumpetinc@users.noreply.github.com> --- CHANGELOG.md | 5 ++ MIGRATION-v14.md | 123 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 128 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b6b745f14..ff52a6b3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,11 @@ * `DefaultEncoder` now supports streaming request bodies for `File`, `Path`, `InputStream`, and `Request.Body` types, avoiding in-memory buffering. New `Request.PathBody` and `Request.InputStreamBody` implementations are provided for these cases. (https://github.com/OpenFeign/feign/pull/3396) +* `Encoder` now supports multiple encoder registration via `Feign.builder().encoders(...)`. New + `DelegatingEncoder` picks the first encoder whose `canEncode()` returns `true`. New `JsonEncoder` + and `XmlEncoder` interfaces provide Content-Type-based auto-detection. `BaseBuilder.encoder()` is + deprecated in favor of `encoders()`. Existing custom `Encoder` implementations must implement the + new `canEncode()` method. (https://github.com/OpenFeign/feign/pull/3476) ### Version 13.12 diff --git a/MIGRATION-v14.md b/MIGRATION-v14.md index e91bfbe27..6359da93c 100644 --- a/MIGRATION-v14.md +++ b/MIGRATION-v14.md @@ -355,6 +355,129 @@ VertxFeign.builder() --- +### 14. `Encoder.canEncode()` — new required method (https://github.com/OpenFeign/feign/pull/3476) + +`Encoder` now requires a `canEncode(Object, Type, RequestTemplate)` method. There is **no default +implementation** — every custom `Encoder` must implement it. + +**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 void encode(Object object, Type bodyType, RequestTemplate template) { + template.body(Request.Body.of(serialize(object))); + } + + @Override + public boolean canEncode(Object object, Type bodyType, RequestTemplate template) { + return true; // or implement content-type / type-based logic + } +} +``` + +Built-in encoders (`DefaultEncoder`, `FormEncoder`, `MeteredEncoder`, `GraphqlEncoder`, and all +`JsonEncoder`/`XmlEncoder` subtypes) already implement this method. If your encoder returns `true` +from `canEncode()`, it may still throw `EncodeException` from `encode()` if encoding fails. + +--- + +### 15. `Encoder` moved from `core` to `api` module + +`feign.codec.Encoder` has been relocated from the `feign-core` module to the new `feign-api` +module. The package name (`feign.codec`) is unchanged. If you have a direct dependency on +`feign-core` without `feign-api`, you need to add `feign-api` to your classpath. + +--- + +### 16. `Encoder.Default` removed + +The deprecated inner class `Encoder.Default` (which extended `DefaultEncoder`) has been removed. + +**Before:** + +```java +new Encoder.Default() +``` + +**After:** + +```java +new feign.core.codec.DefaultEncoder() +``` + +--- + +### 17. `BaseBuilder.encoder()` deprecated — use `encoders()` (https://github.com/OpenFeign/feign/pull/3476) + +The single-encoder setter `encoder(Encoder)` is deprecated (`forRemoval = true`) and replaced by +variadic `encoders(Encoder...)` and `encoders(List)`. + +**Before:** + +```java +Feign.builder() + .encoder(new JacksonEncoder()) + .target(MyApi.class, "https://api.example.com"); +``` + +**After:** + +```java +Feign.builder() + .encoders(new JacksonEncoder()) + .target(MyApi.class, "https://api.example.com"); +``` + +The new methods wrap the supplied encoders in a `DelegatingEncoder`, which delegates to the first +encoder whose `canEncode()` returns `true`. + +--- + +### 18. Multi-encoder support — `DelegatingEncoder`, `JsonEncoder`, `XmlEncoder` (https://github.com/OpenFeign/feign/pull/3476) + +You can now register multiple encoders, and Feign will pick the right one at request time based on +`canEncode()`. 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() + .encoders( + new FormEncoder(), // handles multipart/form-urlencoded by Content-Type + new JacksonEncoder(), // auto-detects JSON via Content-Type (implements JsonEncoder) + new JAXBEncoder(factory) // auto-detects XML via Content-Type (implements XmlEncoder) + ) + .target(MyApi.class, "https://api.example.com"); +``` + +New marker interfaces `JsonEncoder` and `XmlEncoder` provide default `canEncode()` implementations +that check the `Content-Type` request header. All existing JSON/XMl encoders (Jackson, Gson, Moshi, +JAXB, SOAP, Fastjson2, Jackson-Jr) now implement the appropriate interface. You can also use +`DelegatingEncoder` directly to compose custom encoder chains. + +--- + ## Implementing a Custom Streaming Body If you want to stream a body (e.g., from a file or `InputStream`), implement `Request.Body` directly. Because From 10c9265b1deb182af73b3a5b1ee32489c757af52 Mon Sep 17 00:00:00 2001 From: Yevhen Vasyliev Date: Wed, 15 Jul 2026 16:45:34 +0300 Subject: [PATCH 26/63] docs: fix typos in docs Co-authored-by: trumpetinc <6618744+trumpetinc@users.noreply.github.com> --- CHANGELOG.md | 2 +- MIGRATION-v14.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ff52a6b3d..e46c6cff4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ avoiding in-memory buffering. New `Request.PathBody` and `Request.InputStreamBody` implementations are provided for these cases. (https://github.com/OpenFeign/feign/pull/3396) * `Encoder` now supports multiple encoder registration via `Feign.builder().encoders(...)`. New - `DelegatingEncoder` picks the first encoder whose `canEncode()` returns `true`. New `JsonEncoder` + `DelegatingEncoder` picks the first encoder whose `canEncode()` returns `true`. `JsonEncoder` and `XmlEncoder` interfaces provide Content-Type-based auto-detection. `BaseBuilder.encoder()` is deprecated in favor of `encoders()`. Existing custom `Encoder` implementations must implement the new `canEncode()` method. (https://github.com/OpenFeign/feign/pull/3476) diff --git a/MIGRATION-v14.md b/MIGRATION-v14.md index 6359da93c..3adb57faa 100644 --- a/MIGRATION-v14.md +++ b/MIGRATION-v14.md @@ -471,7 +471,7 @@ Feign.builder() .target(MyApi.class, "https://api.example.com"); ``` -New marker interfaces `JsonEncoder` and `XmlEncoder` provide default `canEncode()` implementations +Marker interfaces `JsonEncoder` and `XmlEncoder` provide default `canEncode()` implementations that check the `Content-Type` request header. All existing JSON/XMl encoders (Jackson, Gson, Moshi, JAXB, SOAP, Fastjson2, Jackson-Jr) now implement the appropriate interface. You can also use `DelegatingEncoder` directly to compose custom encoder chains. From a696a4a1e0398c7fdac820f2fdd9360b15ffca55 Mon Sep 17 00:00:00 2001 From: Yevhen Vasyliev Date: Fri, 17 Jul 2026 20:42:20 +0300 Subject: [PATCH 27/63] feat: make `Encode#encode` method return boolean Co-authored-by: trumpetinc <6618744+trumpetinc@users.noreply.github.com> --- CHANGELOG.md | 13 ++-- MIGRATION-v14.md | 76 +++++++++++-------- README.md | 18 ++--- api/src/main/java/feign/AsyncFeign.java | 31 -------- api/src/main/java/feign/BaseBuilder.java | 34 +-------- api/src/main/java/feign/Feign.java | 31 -------- .../feign/RequestTemplateFactoryResolver.java | 11 +-- api/src/main/java/feign/Util.java | 30 ++++++++ .../java/feign/codec/DelegatingEncoder.java | 27 ++----- api/src/main/java/feign/codec/Encoder.java | 43 +++++++---- .../main/java/feign/codec/JsonEncoder.java | 28 +------ api/src/main/java/feign/codec/XmlEncoder.java | 47 ------------ api/src/test/java/feign/UtilTest.java | 45 +++++++++++ .../feign/codec/DelegatingEncoderTest.java | 19 +---- .../example/github/GitHubExample.java | 2 +- .../java/feign/core/codec/DefaultEncoder.java | 30 +------- .../feign/AlwaysEncodeBodyContractTest.java | 18 ++--- core/src/test/java/feign/AsyncFeignTest.java | 43 +++-------- core/src/test/java/feign/BaseApiTest.java | 20 ++--- .../src/test/java/feign/FeignBuilderTest.java | 20 ++--- core/src/test/java/feign/FeignTest.java | 43 +++-------- .../test/java/feign/FeignUnderAsyncTest.java | 43 +++-------- .../feign/MethodMetadataPresenceTest.java | 25 ++---- .../feign/core/codec/DefaultEncoderTest.java | 35 +-------- .../java/feign/metrics4/MeteredEncoder.java | 19 +---- .../java/feign/metrics5/MeteredEncoder.java | 19 +---- .../main/java/example/github/GitHubExample.kt | 2 +- .../java/example/github/GitHubExample.java | 2 +- fastjson2/README.md | 2 +- .../feign/fastjson2/Fastjson2Encoder.java | 7 +- .../feign/fastjson2/FastJsonCodecTest.java | 2 + .../feign/form/spring/SpringFormEncoder.java | 10 +-- .../java/feign/form/feign/spring/Client.java | 4 +- .../feign/spring/SpringFormEncoderTest.java | 4 +- .../src/main/java/feign/form/FormEncoder.java | 22 +----- .../test/java/feign/form/BasicClientTest.java | 2 +- .../java/feign/form/ByteArrayClientTest.java | 2 +- .../java/feign/form/CustomClientTest.java | 2 +- .../java/feign/form/FormPropertyTest.java | 2 +- .../feign/form/MultipartBoundaryTest.java | 14 +--- .../UrlencodedFormContentProcessorTest.java | 2 +- .../test/java/feign/form/WildCardMapTest.java | 2 +- .../java/feign/form/issues/Issue63Test.java | 2 +- .../form/multipart/DelegateWriterTest.java | 33 ++------ .../java/feign/graphql/GraphqlEncoder.java | 19 +---- .../feign/graphql/GraphqlEncoderTest.java | 3 + gson/README.md | 2 +- .../src/main/java/feign/gson/GsonEncoder.java | 7 +- .../test/java/feign/gson/GsonCodecTest.java | 3 + .../feign/hc5/AsyncApacheHttp5ClientTest.java | 43 +++-------- .../main/java/feign/hystrix/HystrixFeign.java | 31 -------- jackson-jaxb/README.md | 4 +- .../jackson/jaxb/JacksonJaxbJsonEncoder.java | 11 ++- .../jackson/jaxb/JacksonJaxbCodecTest.java | 1 + jackson-jr/README.md | 6 +- .../feign/jackson/jr/JacksonJrEncoder.java | 10 ++- .../feign/jackson/jr/JacksonCodecTest.java | 4 + jackson/README.md | 2 +- .../java/feign/jackson/JacksonEncoder.java | 7 +- .../java/feign/jackson/JacksonCodecTest.java | 3 + jackson3/README.md | 2 +- .../java/feign/jackson3/Jackson3Encoder.java | 7 +- .../feign/jackson3/Jackson3CodecTest.java | 3 + .../test/Http2ClientAsyncTest.java | 43 +++-------- .../src/main/java/feign/jaxb/JAXBEncoder.java | 13 +++- .../test/java/feign/jaxb/JAXBCodecTest.java | 8 ++ jaxb/README.md | 2 +- .../src/main/java/feign/jaxb/JAXBEncoder.java | 13 +++- .../test/java/feign/jaxb/JAXBCodecTest.java | 8 ++ json/README.md | 2 +- .../src/main/java/feign/json/JsonEncoder.java | 10 ++- .../test/java/feign/json/JsonCodecTest.java | 4 +- .../java/feign/kotlin/CoroutineFeign.java | 2 +- .../kotlin/feign/kotlin/CoroutineFeignTest.kt | 18 ++--- .../java/feign/micrometer/MeteredEncoder.java | 21 ++--- moshi/README.md | 2 +- .../main/java/feign/moshi/MoshiEncoder.java | 7 +- .../java/feign/moshi/MoshiEncoderTest.java | 4 + .../feign/moshi/examples/GithubExample.java | 2 +- .../feign/okhttp/OkHttpClientAsyncTest.java | 43 +++-------- pom.xml | 30 ++++++++ .../ReactiveFeignIntegrationTest.java | 4 +- soap-jakarta/README.md | 2 +- .../src/main/java/feign/soap/SOAPEncoder.java | 13 +++- .../test/java/feign/soap/SOAPCodecTest.java | 8 ++ soap/README.md | 2 +- .../src/main/java/feign/soap/SOAPEncoder.java | 13 +++- .../test/java/feign/soap/SOAPCodecTest.java | 8 ++ .../java/feign/spring/SpringContractTest.java | 14 ++-- .../BeanValidationMethodInterceptorTest.java | 20 +---- .../BeanValidationMethodInterceptorTest.java | 20 +---- vertx/README.md | 2 +- .../src/main/java/feign/VertxFeign.java | 35 +-------- .../feign/vertx/ConnectionsLeakTests.java | 4 +- .../vertx/Http11ClientReconnectTest.java | 2 +- .../feign/vertx/Http2ClientReconnectTest.java | 2 +- .../java/feign/vertx/RawContractTest.java | 2 +- .../java/feign/vertx/VertxHttpClientTest.java | 2 +- .../feign/vertx/ConnectionsLeakTests.java | 4 +- .../vertx/Http11ClientReconnectTest.java | 2 +- .../feign/vertx/Http2ClientReconnectTest.java | 2 +- .../java/feign/vertx/RawContractTest.java | 2 +- .../java/feign/vertx/VertxHttpClientTest.java | 2 +- 103 files changed, 547 insertions(+), 904 deletions(-) delete mode 100644 api/src/main/java/feign/codec/XmlEncoder.java diff --git a/CHANGELOG.md b/CHANGELOG.md index e46c6cff4..ada9b2fd8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,11 +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` now supports multiple encoder registration via `Feign.builder().encoders(...)`. New - `DelegatingEncoder` picks the first encoder whose `canEncode()` returns `true`. `JsonEncoder` - and `XmlEncoder` interfaces provide Content-Type-based auto-detection. `BaseBuilder.encoder()` is - deprecated in favor of `encoders()`. Existing custom `Encoder` implementations must implement the - new `canEncode()` method. (https://github.com/OpenFeign/feign/pull/3476) +* `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 3adb57faa..5f1a8649e 100644 --- a/MIGRATION-v14.md +++ b/MIGRATION-v14.md @@ -355,10 +355,10 @@ VertxFeign.builder() --- -### 14. `Encoder.canEncode()` — new required method (https://github.com/OpenFeign/feign/pull/3476) +### 14. `Encoder.encode()` now returns `boolean` (https://github.com/OpenFeign/feign/pull/3476) -`Encoder` now requires a `canEncode(Object, Type, RequestTemplate)` method. There is **no default -implementation** — every custom `Encoder` must implement it. +`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:** @@ -376,20 +376,26 @@ public class MyEncoder implements Encoder { ```java public class MyEncoder implements Encoder { @Override - public void encode(Object object, Type bodyType, RequestTemplate template) { + public boolean encode(Object object, Type bodyType, RequestTemplate template) { template.body(Request.Body.of(serialize(object))); - } - - @Override - public boolean canEncode(Object object, Type bodyType, RequestTemplate template) { - return true; // or implement content-type / type-based logic + return true; // or return false if the encoder does not handle this type } } ``` -Built-in encoders (`DefaultEncoder`, `FormEncoder`, `MeteredEncoder`, `GraphqlEncoder`, and all -`JsonEncoder`/`XmlEncoder` subtypes) already implement this method. If your encoder returns `true` -from `canEncode()`, it may still throw `EncodeException` from `encode()` if encoding fails. +Built-in encoders (`DefaultEncoder`, `FormEncoder`, `MeteredEncoder`, `GraphqlEncoder`, etc.) already return `boolean` +from `encode()`. If your encoder returns `false`, the `DelegatingEncoder` (see section 18) will try the next encoder. If +no encoder returns `true`, an `EncodeException` is thrown. + +**Built-in JSON and XML encoders now gate on the `Content-Type` header:** +- 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()` (section 18), this means a JSON or XML encoder will +only claim the request if a matching `Content-Type` header is present. --- @@ -419,10 +425,10 @@ new feign.core.codec.DefaultEncoder() --- -### 17. `BaseBuilder.encoder()` deprecated — use `encoders()` (https://github.com/OpenFeign/feign/pull/3476) +### 17. Composing multiple encoders with `Encoder.of()` (https://github.com/OpenFeign/feign/pull/3476) -The single-encoder setter `encoder(Encoder)` is deprecated (`forRemoval = true`) and replaced by -variadic `encoders(Encoder...)` and `encoders(List)`. +Use `Encoder.of(...)` to compose multiple encoders into a single `DelegatingEncoder`, which +delegates to the first encoder whose `encode()` returns `true`. **Before:** @@ -432,23 +438,36 @@ Feign.builder() .target(MyApi.class, "https://api.example.com"); ``` -**After:** +**After (multiple encoders):** ```java Feign.builder() - .encoders(new JacksonEncoder()) + .encoder(Encoder.of( + new FormEncoder(), + new JacksonEncoder(), + new JAXBEncoder(factory) + )) .target(MyApi.class, "https://api.example.com"); ``` -The new methods wrap the supplied encoders in a `DelegatingEncoder`, which delegates to the first -encoder whose `canEncode()` returns `true`. +**After (single encoder is unchanged):** + +```java +Feign.builder() + .encoder(new JacksonEncoder()) + .target(MyApi.class, "https://api.example.com"); +``` + +The `Encoder.of()` factory wraps the supplied encoders in a `DelegatingEncoder`, which tries +each encoder's `encode()` and uses the first one that returns `true`. --- -### 18. Multi-encoder support — `DelegatingEncoder`, `JsonEncoder`, `XmlEncoder` (https://github.com/OpenFeign/feign/pull/3476) +### 18. Multi-encoder support — `DelegatingEncoder` (https://github.com/OpenFeign/feign/pull/3476) -You can now register multiple encoders, and Feign will pick the right one at request time based on -`canEncode()`. This is especially useful for APIs that mix JSON, XML, and other content types: +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:** @@ -463,19 +482,14 @@ Feign.builder() ```java Feign.builder() - .encoders( + .encoder(Encoder.of( new FormEncoder(), // handles multipart/form-urlencoded by Content-Type - new JacksonEncoder(), // auto-detects JSON via Content-Type (implements JsonEncoder) - new JAXBEncoder(factory) // auto-detects XML via Content-Type (implements XmlEncoder) - ) + new JacksonEncoder(), // implements JsonEncoder marker interface + new JAXBEncoder(factory) // handles XML + )) .target(MyApi.class, "https://api.example.com"); ``` -Marker interfaces `JsonEncoder` and `XmlEncoder` provide default `canEncode()` implementations -that check the `Content-Type` request header. All existing JSON/XMl encoders (Jackson, Gson, Moshi, -JAXB, SOAP, Fastjson2, Jackson-Jr) now implement the appropriate interface. You can also use -`DelegatingEncoder` directly to compose custom encoder chains. - --- ## Implementing a Custom Streaming Body diff --git a/README.md b/README.md index ac1a83ca3..cbaae1076 100644 --- a/README.md +++ b/README.md @@ -386,7 +386,7 @@ You can also configure the encoder and decoder separately: ```java GitHub github = Feign.builder() - .encoders(new GsonEncoder()) + .encoder(new GsonEncoder()) .decoder(new GsonDecoder()) .target(GitHub.class, "https://api.github.com"); ``` @@ -404,7 +404,7 @@ You can also configure the encoder and decoder separately: ```java GitHub github = Feign.builder() - .encoders(new JacksonEncoder()) + .encoder(new JacksonEncoder()) .decoder(new JacksonDecoder()) .target(GitHub.class, "https://api.github.com"); ``` @@ -425,7 +425,7 @@ You can also configure the encoder and decoder separately: ```java GitHub github = Feign.builder() - .encoders(new MoshiEncoder()) + .encoder(new MoshiEncoder()) .decoder(new MoshiDecoder()) .target(GitHub.class, "https://api.github.com"); ``` @@ -455,7 +455,7 @@ You can also configure the encoder and decoder separately: ```java Api api = Feign.builder() - .encoders(new JAXBEncoder(jaxbFactory)) + .encoder(new JAXBEncoder(jaxbFactory)) .decoder(new JAXBDecoder(jaxbFactory)) .target(Api.class, "https://apihost"); ``` @@ -476,7 +476,7 @@ You can also configure the encoder and decoder separately: ```java Api api = Feign.builder() - .encoders(new SOAPEncoder(jaxbFactory)) + .encoder(new SOAPEncoder(jaxbFactory)) .decoder(new SOAPDecoder(jaxbFactory)) .errorDecoder(new SOAPErrorDecoder()) .target(MyApi.class, "http://api"); @@ -498,7 +498,7 @@ You can also configure the encoder and decoder separately: ```java GitHub github = Feign.builder() - .encoders(new Fastjson2Encoder()) + .encoder(new Fastjson2Encoder()) .decoder(new Fastjson2Decoder()) .target(GitHub.class, "https://api.github.com"); ``` @@ -701,7 +701,7 @@ interface LoginClient { public class Example { public static void main(String[] args) { LoginClient client = Feign.builder() - .encoders(new GsonEncoder()) + .encoder(new GsonEncoder()) .target(LoginClient.class, "https://foo.com"); client.login(new Credentials("denominator", "secret")); @@ -1343,7 +1343,7 @@ Add `FormEncoder` to your `Feign.Builder` like so: ```java SomeApi github = Feign.builder() - .encoders(new FormEncoder()) + .encoder(new FormEncoder()) .target(SomeApi.class, "http://api.some.org"); ``` @@ -1351,7 +1351,7 @@ Moreover, you can decorate the existing encoder, for example JsonEncoder like th ```java SomeApi github = Feign.builder() - .encoders(new FormEncoder(new JacksonEncoder())) + .encoder(new FormEncoder(new JacksonEncoder())) .target(SomeApi.class, "http://api.some.org"); ``` diff --git a/api/src/main/java/feign/AsyncFeign.java b/api/src/main/java/feign/AsyncFeign.java index 591abe771..14a6341b5 100644 --- a/api/src/main/java/feign/AsyncFeign.java +++ b/api/src/main/java/feign/AsyncFeign.java @@ -23,7 +23,6 @@ import feign.codec.Encoder; import feign.codec.ErrorDecoder; import feign.interceptor.MethodInterceptor; -import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -164,41 +163,11 @@ public AsyncBuilder logger(Logger logger) { return super.logger(logger); } - /** - * {@inheritDoc} - * - * @deprecated use {@link #encoders(Encoder...)} or {@link #encoders(List)} instead - * @param encoder {@inheritDoc} - * @return {@inheritDoc} - */ @Override - @Deprecated(since = "14", forRemoval = true) public AsyncBuilder encoder(Encoder encoder) { return super.encoder(encoder); } - /** - * {@inheritDoc} - * - * @param encoders {@inheritDoc} - * @return {@inheritDoc} - */ - @Override - public AsyncBuilder encoders(Encoder... encoders) { - return super.encoders(encoders); - } - - /** - * {@inheritDoc} - * - * @param encoders {@inheritDoc} - * @return {@inheritDoc} - */ - @Override - public AsyncBuilder encoders(List encoders) { - return super.encoders(encoders); - } - @Override public AsyncBuilder queryMapEncoder(QueryMapEncoder queryMapEncoder) { return super.queryMapEncoder(queryMapEncoder); diff --git a/api/src/main/java/feign/BaseBuilder.java b/api/src/main/java/feign/BaseBuilder.java index 168172087..265e33022 100644 --- a/api/src/main/java/feign/BaseBuilder.java +++ b/api/src/main/java/feign/BaseBuilder.java @@ -21,10 +21,8 @@ import feign.Request.Options; import feign.codec.Codec; import feign.codec.Decoder; -import feign.codec.DelegatingEncoder; import feign.codec.Encoder; import feign.codec.ErrorDecoder; -import feign.interceptor.Invocation; import feign.interceptor.MethodInterceptor; import feign.interceptor.MethodInterceptors; import java.lang.reflect.Field; @@ -93,38 +91,8 @@ public B logger(Logger logger) { return thisB; } - /** - * Sets the encoder. - * - * @deprecated use {@link #encoders(Encoder...)} or {@link #encoders(List)} instead - * @param encoder the encoder to use for encoding request bodies. Must not be {@code null}. - * @return this builder - */ - @Deprecated(since = "14", forRemoval = true) public B encoder(Encoder encoder) { - return encoders(encoder); - } - - /** - * Sets the encoders. - * - * @param encoders the encoders to use for encoding request bodies. Must not be {@code null}. - * @return this builder - * @since 14 - */ - public B encoders(Encoder... encoders) { - return encoders(Arrays.asList(encoders)); - } - - /** - * Sets the encoders. - * - * @param encoders the encoders to use for encoding request bodies. Must not be {@code null}. - * @return this builder - * @since 14 - */ - public B encoders(List encoders) { - this.encoder = new DelegatingEncoder(encoders); + this.encoder = encoder; return thisB; } diff --git a/api/src/main/java/feign/Feign.java b/api/src/main/java/feign/Feign.java index ad0fdc24d..45c4d63fd 100644 --- a/api/src/main/java/feign/Feign.java +++ b/api/src/main/java/feign/Feign.java @@ -25,7 +25,6 @@ import java.io.IOException; import java.lang.reflect.Method; import java.lang.reflect.Type; -import java.util.List; /** * Feign's purpose is to ease development against http apis that feign restfulness.
@@ -125,41 +124,11 @@ public Builder logger(Logger logger) { return super.logger(logger); } - /** - * {@inheritDoc} - * - * @deprecated use {@link #encoders(Encoder...)} or {@link #encoders(List)} instead - * @param encoder {@inheritDoc} - * @return {@inheritDoc} - */ @Override - @Deprecated(since = "14", forRemoval = true) public Builder encoder(Encoder encoder) { return super.encoder(encoder); } - /** - * {@inheritDoc} - * - * @param encoders {@inheritDoc} - * @return {@inheritDoc} - */ - @Override - public Builder encoders(Encoder... encoders) { - return super.encoders(encoders); - } - - /** - * {@inheritDoc} - * - * @param encoders {@inheritDoc} - * @return {@inheritDoc} - */ - @Override - public Builder encoders(List encoders) { - return super.encoders(encoders); - } - @Override public Builder decoder(Decoder decoder) { return super.decoder(decoder); diff --git a/api/src/main/java/feign/RequestTemplateFactoryResolver.java b/api/src/main/java/feign/RequestTemplateFactoryResolver.java index 5340e6c5f..8f4413757 100644 --- a/api/src/main/java/feign/RequestTemplateFactoryResolver.java +++ b/api/src/main/java/feign/RequestTemplateFactoryResolver.java @@ -232,11 +232,10 @@ protected RequestTemplate resolve( formVariables.put(entry.getKey(), entry.getValue()); } } - if (!encoder.canEncode(formVariables, Encoder.MAP_STRING_WILDCARD, mutable)) { - throw new EncodeException("This encoder does not support form encoding: " + encoder); - } 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) { @@ -287,7 +286,7 @@ protected RequestTemplate resolve( private void encode(Object object, Type bodyType, RequestTemplate mutable) throws EncodeException { - if (!encoder.canEncode(object, bodyType, mutable)) { + if (!encoder.encode(object, bodyType, mutable)) { throw new EncodeException( "This encoder does not support encoding of type: " + bodyType @@ -296,8 +295,6 @@ private void encode(Object object, Type bodyType, RequestTemplate mutable) + ", encoder: " + encoder); } - - encoder.encode(object, bodyType, mutable); } } } 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 index 724719fed..9e0142bac 100644 --- a/api/src/main/java/feign/codec/DelegatingEncoder.java +++ b/api/src/main/java/feign/codec/DelegatingEncoder.java @@ -26,7 +26,7 @@ * * @since 14 */ -public class DelegatingEncoder implements Encoder { +class DelegatingEncoder implements Encoder { private final List delegates; /** @@ -35,7 +35,7 @@ public class DelegatingEncoder implements Encoder { * @param delegates the list of delegates to use for encoding. Both list and its elements must not * be {@code null}. */ - public DelegatingEncoder(List delegates) { + DelegatingEncoder(List delegates) { this.delegates = Objects.requireNonNull(delegates, "delegates cannot be null"); } @@ -49,10 +49,11 @@ public DelegatingEncoder(List delegates) { * @throws EncodeException {@inheritDoc} */ @Override - public void encode(Object object, Type bodyType, RequestTemplate template) + public boolean encode(Object object, Type bodyType, RequestTemplate template) throws EncodeException { - delegates.stream() - .filter(encoder -> encoder.canEncode(object, bodyType, template)) + return delegates.stream() + .map(encoder -> encoder.encode(object, bodyType, template)) + .filter(Boolean::booleanValue) .findFirst() .orElseThrow( () -> @@ -60,21 +61,7 @@ public void encode(Object object, Type bodyType, RequestTemplate template) "No suitable encoder found for object encoding: " + object + ", encoders: " - + delegates)) - .encode(object, bodyType, template); - } - - /** - * Checks if any of the delegates can encode the given object. - * - * @param object {@inheritDoc} - * @param bodyType {@inheritDoc} - * @param template {@inheritDoc} - * @return {@inheritDoc} - */ - @Override - public boolean canEncode(Object object, Type bodyType, RequestTemplate template) { - return delegates.stream().anyMatch(delegate -> delegate.canEncode(object, bodyType, template)); + + delegates)); } /** diff --git a/api/src/main/java/feign/codec/Encoder.java b/api/src/main/java/feign/codec/Encoder.java index 03cbd9697..bfb40bfa1 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,21 +103,8 @@ 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. + * @return {@code true} if the encoder handled the object, {@code false} otherwise. * @throws EncodeException when encoding failed due to a checked exception. */ - void encode(Object object, Type bodyType, RequestTemplate template) throws EncodeException; - - /** - * Checks if the encoder can encode the given object. - * - * @param object what to encode as the request body - * @param bodyType the type the object should be encoded as. {@link #MAP_STRING_WILDCARD} - * indicates form encoding - * @param template the request template to populate - * @return {@code true} if the encoder can encode the object, {@code false} otherwise - * @since 14 - * @apiNote when this method returns {@code true}, the {@link EncodeException} may still be thrown - * by {@link #encode(Object, Type, RequestTemplate)} if the encoding fails. - */ - boolean canEncode(Object object, Type bodyType, RequestTemplate template); + boolean encode(Object object, Type bodyType, RequestTemplate template) throws EncodeException; } diff --git a/api/src/main/java/feign/codec/JsonEncoder.java b/api/src/main/java/feign/codec/JsonEncoder.java index 6bce20b7f..47d800e74 100644 --- a/api/src/main/java/feign/codec/JsonEncoder.java +++ b/api/src/main/java/feign/codec/JsonEncoder.java @@ -15,29 +15,7 @@ */ package feign.codec; -import feign.RequestTemplate; -import java.lang.reflect.Type; -import java.util.List; +import feign.Experimental; -/** An encoder that encodes objects to JSON. */ -@FunctionalInterface -public interface JsonEncoder extends Encoder { - /** - * Checks if the given object can be encoded to JSON based on the {@code Content-Type} header in - * the {@link RequestTemplate}. - * - * @param object {@inheritDoc} - * @param bodyType {@inheritDoc} - * @param template {@inheritDoc} - * @return {@code true} if the given {@code Content-Type} header is compatible with JSON, {@code - * false} otherwise - */ - @Override - default boolean canEncode(Object object, Type bodyType, RequestTemplate template) { - return template.headers().getOrDefault("Content-Type", List.of()).stream() - .anyMatch( - contentType -> - contentType != null - && contentType.trim().matches("(?i)\\w+/(?:[\\w._-]+\\+)?json.*")); - } -} +@Experimental +public interface JsonEncoder extends Encoder {} diff --git a/api/src/main/java/feign/codec/XmlEncoder.java b/api/src/main/java/feign/codec/XmlEncoder.java deleted file mode 100644 index 53a54c4d1..000000000 --- a/api/src/main/java/feign/codec/XmlEncoder.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * 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; - -/** - * An encoder that encodes objects to XML. - * - * @since 14 - */ -@FunctionalInterface -public interface XmlEncoder extends Encoder { - /** - * Checks if the given object can be encoded to XML based on the {@code Content-Type} header in - * the {@link RequestTemplate}. - * - * @param object {@inheritDoc} - * @param bodyType {@inheritDoc} - * @param template {@inheritDoc} - * @return {@code true} if the given {@code Content-Type} header is compatible with XML, {@code - * false} otherwise - */ - @Override - default boolean canEncode(Object object, Type bodyType, 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/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 index 9c14d6e8c..99c3ca555 100644 --- a/api/src/test/java/feign/codec/DelegatingEncoderTest.java +++ b/api/src/test/java/feign/codec/DelegatingEncoderTest.java @@ -15,7 +15,6 @@ */ package feign.codec; -import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; @@ -26,8 +25,6 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; @@ -41,27 +38,13 @@ void setUp() { encoder = new DelegatingEncoder(List.of(delegate)); } - @ParameterizedTest - @ValueSource(booleans = {true, false}) - void testCanEncode(boolean expected) { - var object = "Hello, World!"; - var bodyType = String.class; - var requestTemplate = mock(RequestTemplate.class); - - when(delegate.canEncode(object, bodyType, requestTemplate)).thenReturn(expected); - - var actual = encoder.canEncode(object, bodyType, requestTemplate); - - assertEquals(expected, actual); - } - @Test void shouldEncode() { var object = "Hello, World!"; var bodyType = String.class; var requestTemplate = mock(RequestTemplate.class); - when(delegate.canEncode(object, bodyType, requestTemplate)).thenReturn(true); + when(delegate.encode(object, bodyType, requestTemplate)).thenReturn(true); encoder.encode(object, bodyType, requestTemplate); diff --git a/apt-test-generator/src/test/resources/example/github/GitHubExample.java b/apt-test-generator/src/test/resources/example/github/GitHubExample.java index dced22948..2539e5b02 100644 --- a/apt-test-generator/src/test/resources/example/github/GitHubExample.java +++ b/apt-test-generator/src/test/resources/example/github/GitHubExample.java @@ -73,7 +73,7 @@ static GitHub connect() { final Decoder decoder = new GsonDecoder(); final Encoder encoder = new GsonEncoder(); return Feign.builder() - .encoders(encoder) + .encoder(encoder) .decoder(decoder) .errorDecoder(new GitHubErrorDecoder(decoder)) .logger(new Logger.ErrorLogger()) diff --git a/core/src/main/java/feign/core/codec/DefaultEncoder.java b/core/src/main/java/feign/core/codec/DefaultEncoder.java index ecc99a663..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,28 +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; } - } - - /** - * {@inheritDoc} - * - * @param object {@inheritDoc} - * @param bodyType {@inheritDoc} - * @param template {@inheritDoc} - * @return {@inheritDoc} - */ - @Override - public boolean canEncode(Object object, Type bodyType, RequestTemplate template) { - return object == null - || bodyType == String.class - || bodyType == byte[].class - || object instanceof File - || object instanceof Path - || object instanceof InputStream - || object instanceof Request.Body; + return true; } } diff --git a/core/src/test/java/feign/AlwaysEncodeBodyContractTest.java b/core/src/test/java/feign/AlwaysEncodeBodyContractTest.java index 5fb24b5e4..afcc79e57 100644 --- a/core/src/test/java/feign/AlwaysEncodeBodyContractTest.java +++ b/core/src/test/java/feign/AlwaysEncodeBodyContractTest.java @@ -61,29 +61,21 @@ 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)); - } - - @Override - public boolean canEncode(Object object, Type bodyType, RequestTemplate template) { 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))); - } - - @Override - public boolean canEncode(Object object, Type bodyType, RequestTemplate template) { return true; } } @@ -112,7 +104,7 @@ void alwaysEncodeBodyTrueTest() { SampleTargetMultipleNonAnnotatedParameters sampleClient1 = Feign.builder() .contract(new SampleContract()) - .encoders(new AllParametersSampleEncoder()) + .encoder(new AllParametersSampleEncoder()) .client(new SampleClient()) .target(SampleTargetMultipleNonAnnotatedParameters.class, "http://localhost"); assertThat(sampleClient1.concatenate("foo", "bar", "char")).isEqualTo("foobarchar"); @@ -120,7 +112,7 @@ void alwaysEncodeBodyTrueTest() { SampleTargetNoParameters sampleClient2 = Feign.builder() .contract(new SampleContract()) - .encoders(new AllParametersSampleEncoder()) + .encoder(new AllParametersSampleEncoder()) .client(new SampleClient()) .target(SampleTargetNoParameters.class, "http://localhost"); assertThat(sampleClient2.concatenate()).isEmpty(); @@ -128,7 +120,7 @@ void alwaysEncodeBodyTrueTest() { SampleTargetOneParameter sampleClient3 = Feign.builder() .contract(new SampleContract()) - .encoders(new AllParametersSampleEncoder()) + .encoder(new AllParametersSampleEncoder()) .client(new SampleClient()) .target(SampleTargetOneParameter.class, "http://localhost"); assertThat(sampleClient3.concatenate("moo")).isEqualTo("moo"); diff --git a/core/src/test/java/feign/AsyncFeignTest.java b/core/src/test/java/feign/AsyncFeignTest.java index 572abfb7e..c40ff45bb 100644 --- a/core/src/test/java/feign/AsyncFeignTest.java +++ b/core/src/test/java/feign/AsyncFeignTest.java @@ -149,12 +149,8 @@ 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); - } - - @Override - public boolean canEncode(Object object, Type bodyType, RequestTemplate template) { return true; } }) @@ -868,17 +864,8 @@ void okIfEncodeRootCauseHasNoMessage() throws Throwable { TestInterfaceAsync api = new TestInterfaceAsyncBuilder() .encoder( - new Encoder() { - @Override - public void encode(Object object, Type bodyType, RequestTemplate template) - throws EncodeException { - throw new RuntimeException(); - } - - @Override - public boolean canEncode(Object object, Type bodyType, RequestTemplate template) { - return true; - } + (_, _, _) -> { + throw new RuntimeException(); }) .target("http://localhost:" + server.getPort()); @@ -1249,22 +1236,14 @@ static final class TestInterfaceAsyncBuilder { private final AsyncFeign.AsyncBuilder delegate = AsyncFeign.builder() .decoder(new DefaultDecoder()) - .encoders( - new Encoder() { - @Override - public void encode(Object object, Type bodyType, RequestTemplate template) - throws EncodeException { - if (object instanceof Map) { - template.body(Request.Body.of(new Gson().toJson(object))); - } else { - template.body(Request.Body.of(object.toString())); - } - } - - @Override - public boolean canEncode(Object object, Type bodyType, RequestTemplate template) { - return true; + .encoder( + (object, _, template) -> { + if (object instanceof Map) { + template.body(Request.Body.of(new Gson().toJson(object))); + } else { + template.body(Request.Body.of(object.toString())); } + return true; }); TestInterfaceAsyncBuilder requestInterceptor(RequestInterceptor requestInterceptor) { @@ -1273,7 +1252,7 @@ TestInterfaceAsyncBuilder requestInterceptor(RequestInterceptor requestIntercept } TestInterfaceAsyncBuilder encoder(Encoder encoder) { - delegate.encoders(encoder); + delegate.encoder(encoder); return this; } diff --git a/core/src/test/java/feign/BaseApiTest.java b/core/src/test/java/feign/BaseApiTest.java index 85e4e15e0..4b48edd21 100644 --- a/core/src/test/java/feign/BaseApiTest.java +++ b/core/src/test/java/feign/BaseApiTest.java @@ -16,12 +16,10 @@ package feign; import static feign.assertj.MockWebServerAssertions.assertThat; +import static org.assertj.core.api.Assertions.assertThat; import com.google.gson.reflect.TypeToken; -import feign.codec.EncodeException; -import feign.codec.Encoder; import java.io.IOException; -import java.lang.reflect.Type; import java.util.List; import mockwebserver3.MockResponse; import mockwebserver3.MockWebServer; @@ -85,18 +83,10 @@ void resolvesBodyParameter() throws Exception { String baseUrl = server.url("/default").toString(); Feign.builder() - .encoders( - new Encoder() { - @Override - public void encode(Object object, Type bodyType, RequestTemplate template) - throws EncodeException { - assertThat(bodyType).isEqualTo(new TypeToken>() {}.getType()); - } - - @Override - public boolean canEncode(Object object, Type bodyType, RequestTemplate template) { - return true; - } + .encoder( + (_, bodyType, _) -> { + assertThat(bodyType).isEqualTo(new TypeToken>() {}.getType()); + return true; }) .decoder( (_, type) -> { diff --git a/core/src/test/java/feign/FeignBuilderTest.java b/core/src/test/java/feign/FeignBuilderTest.java index 81a30e2c8..e20c18cc9 100644 --- a/core/src/test/java/feign/FeignBuilderTest.java +++ b/core/src/test/java/feign/FeignBuilderTest.java @@ -21,7 +21,6 @@ import static org.assertj.core.api.Assertions.failBecauseExceptionWasNotThrown; import feign.codec.Decoder; -import feign.codec.EncodeException; import feign.codec.Encoder; import feign.core.DefaultClient; import java.io.IOException; @@ -29,7 +28,6 @@ import java.io.Reader; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; -import java.lang.reflect.Type; import java.nio.charset.Charset; import java.util.Arrays; import java.util.Collections; @@ -211,25 +209,17 @@ void urlPathConcatNoInitialSlashOnPathNoTrailingSlashOnUrl() throws Exception { } @Test - void overrideEncoders() throws Exception { + void overrideEncoder() throws Exception { server.enqueue(new MockResponse.Builder().body("response data").build()); String url = "http://localhost:" + server.getPort(); Encoder encoder = - new Encoder() { - @Override - public void encode(Object object, Type bodyType, RequestTemplate template) - throws EncodeException { - template.body(Request.Body.of(object.toString())); - } - - @Override - public boolean canEncode(Object object, Type bodyType, RequestTemplate template) { - return true; - } + (object, _, template) -> { + template.body(Request.Body.of(object.toString())); + return true; }; - TestInterface api = Feign.builder().encoders(encoder).target(TestInterface.class, url); + TestInterface api = Feign.builder().encoder(encoder).target(TestInterface.class, url); api.encodedPost(Arrays.asList("This", "is", "my", "request")); assertThat(server.takeRequest()).hasBody("[This, is, my, request]"); diff --git a/core/src/test/java/feign/FeignTest.java b/core/src/test/java/feign/FeignTest.java index 0af6c4f27..d9e877d65 100755 --- a/core/src/test/java/feign/FeignTest.java +++ b/core/src/test/java/feign/FeignTest.java @@ -170,12 +170,8 @@ 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); - } - - @Override - public boolean canEncode(Object object, Type bodyType, RequestTemplate template) { return true; } }) @@ -883,17 +879,8 @@ void okIfEncodeRootCauseHasNoMessage() throws Exception { TestInterface api = new TestInterfaceBuilder() .encoder( - new Encoder() { - @Override - public void encode(Object object, Type bodyType, RequestTemplate template) - throws EncodeException { - throw new RuntimeException(); - } - - @Override - public boolean canEncode(Object object, Type bodyType, RequestTemplate template) { - return true; - } + (_, _, _) -> { + throw new RuntimeException(); }) .target("http://localhost:" + server.getPort()); @@ -1517,22 +1504,14 @@ static final class TestInterfaceBuilder { private final Feign.Builder delegate = new Feign.Builder() .decoder(new DefaultDecoder()) - .encoders( - new Encoder() { - @Override - public void encode(Object object, Type bodyType, RequestTemplate template) - throws EncodeException { - if (object instanceof Map) { - template.body(Request.Body.of(new Gson().toJson(object))); - } else { - template.body(Request.Body.of(object.toString())); - } - } - - @Override - public boolean canEncode(Object object, Type bodyType, RequestTemplate template) { - return true; + .encoder( + (object, _, template) -> { + if (object instanceof Map) { + template.body(Request.Body.of(new Gson().toJson(object))); + } else { + template.body(Request.Body.of(object.toString())); } + return true; }); TestInterfaceBuilder requestInterceptor(RequestInterceptor requestInterceptor) { @@ -1551,7 +1530,7 @@ TestInterfaceBuilder responseInterceptors(Iterable response } TestInterfaceBuilder encoder(Encoder encoder) { - delegate.encoders(encoder); + delegate.encoder(encoder); return this; } diff --git a/core/src/test/java/feign/FeignUnderAsyncTest.java b/core/src/test/java/feign/FeignUnderAsyncTest.java index eea18e8a1..5002e6c4c 100644 --- a/core/src/test/java/feign/FeignUnderAsyncTest.java +++ b/core/src/test/java/feign/FeignUnderAsyncTest.java @@ -133,12 +133,8 @@ 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); - } - - @Override - public boolean canEncode(Object object, Type bodyType, RequestTemplate template) { return true; } }) @@ -633,17 +629,8 @@ void okIfEncodeRootCauseHasNoMessage() throws Exception { TestInterface api = new TestInterfaceBuilder() .encoder( - new Encoder() { - @Override - public void encode(Object object, Type bodyType, RequestTemplate template) - throws EncodeException { - throw new RuntimeException(); - } - - @Override - public boolean canEncode(Object object, Type bodyType, RequestTemplate template) { - return true; - } + (_, _, _) -> { + throw new RuntimeException(); }) .target("http://localhost:" + server.getPort()); @@ -979,22 +966,14 @@ static final class TestInterfaceBuilder { private final AsyncFeign.AsyncBuilder delegate = AsyncFeign.builder() .decoder(new DefaultDecoder()) - .encoders( - new Encoder() { - @Override - public void encode(Object object, Type bodyType, RequestTemplate template) - throws EncodeException { - if (object instanceof Map) { - template.body(Request.Body.of(new Gson().toJson(object))); - } else { - template.body(Request.Body.of(object.toString())); - } - } - - @Override - public boolean canEncode(Object object, Type bodyType, RequestTemplate template) { - return true; + .encoder( + (object, _, template) -> { + if (object instanceof Map) { + template.body(Request.Body.of(new Gson().toJson(object))); + } else { + template.body(Request.Body.of(object.toString())); } + return true; }); TestInterfaceBuilder requestInterceptor(RequestInterceptor requestInterceptor) { @@ -1003,7 +982,7 @@ TestInterfaceBuilder requestInterceptor(RequestInterceptor requestInterceptor) { } TestInterfaceBuilder encoder(Encoder encoder) { - delegate.encoders(encoder); + delegate.encoder(encoder); return this; } diff --git a/core/src/test/java/feign/MethodMetadataPresenceTest.java b/core/src/test/java/feign/MethodMetadataPresenceTest.java index a6d0da7d3..89ddcb587 100644 --- a/core/src/test/java/feign/MethodMetadataPresenceTest.java +++ b/core/src/test/java/feign/MethodMetadataPresenceTest.java @@ -16,15 +16,13 @@ package feign; import static feign.assertj.MockWebServerAssertions.assertThat; +import static org.assertj.core.api.Assertions.assertThat; import feign.FeignBuilderTest.TestInterface; -import feign.codec.EncodeException; -import feign.codec.Encoder; import feign.core.DefaultClient; import feign.core.codec.DefaultDecoder; import feign.core.codec.DefaultEncoder; import java.io.IOException; -import java.lang.reflect.Type; import mockwebserver3.MockResponse; import mockwebserver3.MockWebServer; import org.junit.jupiter.api.AfterEach; @@ -64,21 +62,12 @@ void encoder() throws Exception { final String url = "http://localhost:" + server.getPort(); final TestInterface api = Feign.builder() - .encoders( - new Encoder() { - @Override - public void encode(Object object, Type bodyType, RequestTemplate template) - throws EncodeException { - assertThat(template).isNotNull(); - assertThat(template.methodMetadata()).isNotNull(); - assertThat(template.feignTarget()).isNotNull(); - new DefaultEncoder().encode(object, bodyType, template); - } - - @Override - public boolean canEncode(Object object, Type bodyType, RequestTemplate template) { - return true; - } + .encoder( + (object, bodyType, template) -> { + assertThat(template).isNotNull(); + assertThat(template.methodMetadata()).isNotNull(); + assertThat(template.feignTarget()).isNotNull(); + 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 06c399fb0..4279262de 100644 --- a/core/src/test/java/feign/core/codec/DefaultEncoderTest.java +++ b/core/src/test/java/feign/core/codec/DefaultEncoderTest.java @@ -16,45 +16,25 @@ 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 static org.junit.jupiter.params.provider.Arguments.arguments; import feign.Request; import feign.RequestTemplate; -import feign.codec.EncodeException; import feign.codec.Encoder; import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; -import java.lang.reflect.Type; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.time.Clock; import java.util.Optional; -import java.util.function.Supplier; -import java.util.stream.Stream; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.FieldSource; class DefaultEncoderTest { - private static final Supplier> testCanEncode = - () -> - Stream.of( - arguments(null, Object.class, true), - arguments("string", String.class, true), - arguments(new byte[] {1, 2, 3}, byte[].class, true), - arguments(new File("file"), File.class, true), - arguments(Path.of("path"), Path.class, true), - arguments(new ByteArrayInputStream(new byte[] {1, 2, 3}), InputStream.class, true), - arguments(Request.Body.of("body"), Request.Body.class, true), - arguments(new DefaultEncoderTest(), DefaultEncoderTest.class, false)); private final Encoder encoder = new DefaultEncoder(); @@ -92,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 @@ -147,15 +123,6 @@ void shouldEncodeRequestBody() { verifyBody(template, expected.length(), true, expected); } - @ParameterizedTest - @FieldSource - void testCanEncode(Object object, Type bodyType, boolean expected) { - var template = new RequestTemplate(); - var actual = encoder.canEncode(object, bodyType, template); - - assertEquals(expected, actual); - } - void verifyBody( RequestTemplate template, long contentLength, boolean repeatable, String expected) { var optionalBody = template.requestBody(); diff --git a/dropwizard-metrics4/src/main/java/feign/metrics4/MeteredEncoder.java b/dropwizard-metrics4/src/main/java/feign/metrics4/MeteredEncoder.java index f85fd5e64..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,18 +61,6 @@ public void encode(Object object, Type bodyType, RequestTemplate template) template.methodMetadata(), template.feignTarget(), "request_size"), metricSuppliers.histograms()) .update(body.contentLength())); - } - - /** - * {@inheritDoc} - * - * @param object {@inheritDoc} - * @param bodyType {@inheritDoc} - * @param template {@inheritDoc} - * @return {@inheritDoc} - */ - @Override - public boolean canEncode(Object object, Type bodyType, RequestTemplate template) { - return encoder.canEncode(object, bodyType, template); + 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 0595a223a..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,18 +73,6 @@ public void encode(Object object, Type bodyType, RequestTemplate template) template.methodMetadata(), template.feignTarget(), "request_size"), metricSuppliers.histograms()) .update(body.contentLength())); - } - - /** - * {@inheritDoc} - * - * @param object {@inheritDoc} - * @param bodyType {@inheritDoc} - * @param template {@inheritDoc} - * @return {@inheritDoc} - */ - @Override - public boolean canEncode(Object object, Type bodyType, RequestTemplate template) { - return encoder.canEncode(object, bodyType, template); + return isEncoded; } } diff --git a/example-github-with-coroutine/src/main/java/example/github/GitHubExample.kt b/example-github-with-coroutine/src/main/java/example/github/GitHubExample.kt index a1e8e4375..3058435b1 100644 --- a/example-github-with-coroutine/src/main/java/example/github/GitHubExample.kt +++ b/example-github-with-coroutine/src/main/java/example/github/GitHubExample.kt @@ -90,7 +90,7 @@ interface GitHub { val decoder: Decoder = feign.gson.GsonDecoder() val encoder: Encoder = GsonEncoder() return CoroutineFeign.builder() - .encoders(encoder) + .encoder(encoder) .decoder(decoder) .errorDecoder(GitHubErrorDecoder(decoder)) .logger(ErrorLogger()) diff --git a/example-github/src/main/java/example/github/GitHubExample.java b/example-github/src/main/java/example/github/GitHubExample.java index dced22948..2539e5b02 100644 --- a/example-github/src/main/java/example/github/GitHubExample.java +++ b/example-github/src/main/java/example/github/GitHubExample.java @@ -73,7 +73,7 @@ static GitHub connect() { final Decoder decoder = new GsonDecoder(); final Encoder encoder = new GsonEncoder(); return Feign.builder() - .encoders(encoder) + .encoder(encoder) .decoder(decoder) .errorDecoder(new GitHubErrorDecoder(decoder)) .logger(new Logger.ErrorLogger()) diff --git a/fastjson2/README.md b/fastjson2/README.md index 6633c6300..714995dba 100644 --- a/fastjson2/README.md +++ b/fastjson2/README.md @@ -15,7 +15,7 @@ You can also configure the encoder and decoder separately: ```java GitHub github = Feign.builder() - .encoders(new Fastjson2Encoder()) + .encoder(new Fastjson2Encoder()) .decoder(new Fastjson2Decoder()) .target(GitHub.class, "https://api.github.com"); ``` 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/Client.java b/form-spring/src/test/java/feign/form/feign/spring/Client.java index f98b69b79..dbd2040a9 100644 --- a/form-spring/src/test/java/feign/form/feign/spring/Client.java +++ b/form-spring/src/test/java/feign/form/feign/spring/Client.java @@ -21,7 +21,6 @@ import feign.Logger; import feign.Response; -import feign.codec.DelegatingEncoder; import feign.codec.Encoder; import feign.form.spring.SpringFormEncoder; import java.util.List; @@ -93,8 +92,7 @@ class ClientConfiguration { @Bean Encoder feignEncoder(ObjectProvider messageConverters) { - return new DelegatingEncoder( - List.of(new SpringEncoder(messageConverters), new SpringFormEncoder())); + return new SpringFormEncoder(new SpringEncoder(messageConverters)); } @Bean 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 b26cb5f9c..c65bb1436 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 @@ -32,8 +32,6 @@ import org.springframework.test.annotation.DirtiesContext.ClassMode; import org.springframework.web.multipart.MultipartFile; -// TODO: enable when Spring Cloud OpenFiegn migrates to Feign 14 -@Disabled("Disabled util Spring Cloud OpenFiegn migrates to Feign 14") @SpringBootTest( webEnvironment = DEFINED_PORT, classes = Server.class, @@ -79,6 +77,8 @@ void uploadFileNameAndContentTypeTest() throws Exception { .isEqualTo(file.getOriginalFilename() + ':' + file.getContentType() + ':' + folder); } + // TODO: enable when Spring Cloud OpenFiegn migrates to Feign 14 + @Disabled("Disabled util Spring Cloud OpenFiegn 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 cef546b37..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,25 +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); - } - - /** - * {@inheritDoc} - * - * @param object {@inheritDoc} - * @param bodyType {@inheritDoc} - * @param template {@inheritDoc} - * @return {@inheritDoc} - */ - @Override - public boolean canEncode(Object object, Type bodyType, RequestTemplate template) { - return processors.containsKey(ContentType.of(getContentTypeValue(template.headers()))); + return true; } /** diff --git a/form/src/test/java/feign/form/BasicClientTest.java b/form/src/test/java/feign/form/BasicClientTest.java index ed0fb5451..b88748c59 100644 --- a/form/src/test/java/feign/form/BasicClientTest.java +++ b/form/src/test/java/feign/form/BasicClientTest.java @@ -48,8 +48,8 @@ static void configureClient() { API = Feign.builder() + .encoder(new FormEncoder(new JacksonEncoder())) .logger(new JavaLogger(BasicClientTest.class).appendToFile(logFile)) - .encoders(new FormEncoder(), new JacksonEncoder()) .logLevel(FULL) .target(TestClient.class, "http://localhost:8080"); } diff --git a/form/src/test/java/feign/form/ByteArrayClientTest.java b/form/src/test/java/feign/form/ByteArrayClientTest.java index c622cb3ed..476dfea1e 100644 --- a/form/src/test/java/feign/form/ByteArrayClientTest.java +++ b/form/src/test/java/feign/form/ByteArrayClientTest.java @@ -46,8 +46,8 @@ static void configureClient() { API = Feign.builder() + .encoder(encoder) .logger(new JavaLogger(ByteArrayClientTest.class).appendToFile(logFile)) - .encoders(encoder) .logLevel(FULL) .target(CustomClient.class, "http://localhost:8080"); } diff --git a/form/src/test/java/feign/form/CustomClientTest.java b/form/src/test/java/feign/form/CustomClientTest.java index dacb4d300..7fd85b6e5 100644 --- a/form/src/test/java/feign/form/CustomClientTest.java +++ b/form/src/test/java/feign/form/CustomClientTest.java @@ -52,8 +52,8 @@ static void configureClient() { API = Feign.builder() + .encoder(encoder) .logger(new JavaLogger(CustomClientTest.class).appendToFile(logFile)) - .encoders(encoder) .logLevel(FULL) .target(CustomClient.class, "http://localhost:8080"); } diff --git a/form/src/test/java/feign/form/FormPropertyTest.java b/form/src/test/java/feign/form/FormPropertyTest.java index f755374f5..745092882 100644 --- a/form/src/test/java/feign/form/FormPropertyTest.java +++ b/form/src/test/java/feign/form/FormPropertyTest.java @@ -43,8 +43,8 @@ static void configureClient() { API = Feign.builder() + .encoder(new FormEncoder(new JacksonEncoder())) .logger(new JavaLogger(FormPropertyTest.class).appendToFile(logFile)) - .encoders(new FormEncoder(new JacksonEncoder())) .logLevel(FULL) .target(FormClient.class, "http://localhost:8080"); } diff --git a/form/src/test/java/feign/form/MultipartBoundaryTest.java b/form/src/test/java/feign/form/MultipartBoundaryTest.java index c833d354f..dd2fc9783 100644 --- a/form/src/test/java/feign/form/MultipartBoundaryTest.java +++ b/form/src/test/java/feign/form/MultipartBoundaryTest.java @@ -19,26 +19,14 @@ import static org.assertj.core.api.Assertions.assertThat; import feign.RequestTemplate; -import feign.codec.EncodeException; import feign.codec.Encoder; -import java.lang.reflect.Type; import java.util.LinkedHashMap; import java.util.Map; import org.junit.jupiter.api.Test; class MultipartBoundaryTest { - private static final Encoder NOOP_DELEGATE = - new Encoder() { - @Override - public void encode(Object object, Type bodyType, RequestTemplate template) - throws EncodeException {} - - @Override - public boolean canEncode(Object object, Type bodyType, RequestTemplate template) { - return true; - } - }; + private static final Encoder NOOP_DELEGATE = (object, bodyType, template) -> true; @Test void boundaryIsNotDerivedFromTheClock() { diff --git a/form/src/test/java/feign/form/UrlencodedFormContentProcessorTest.java b/form/src/test/java/feign/form/UrlencodedFormContentProcessorTest.java index dfa26a615..be915fab9 100644 --- a/form/src/test/java/feign/form/UrlencodedFormContentProcessorTest.java +++ b/form/src/test/java/feign/form/UrlencodedFormContentProcessorTest.java @@ -126,7 +126,7 @@ private void assertEncodedBody( .start()) { var client = Feign.builder() - .encoders(new FormEncoder(new JacksonEncoder())) + .encoder(new FormEncoder(new JacksonEncoder())) .target(Client.class, server.getConnectUrl()); var data = createRequestData(tags); diff --git a/form/src/test/java/feign/form/WildCardMapTest.java b/form/src/test/java/feign/form/WildCardMapTest.java index 60d76317f..04305c5cb 100644 --- a/form/src/test/java/feign/form/WildCardMapTest.java +++ b/form/src/test/java/feign/form/WildCardMapTest.java @@ -45,8 +45,8 @@ static void configureClient() { api = Feign.builder() + .encoder(new FormEncoder()) .logger(new JavaLogger(WildCardMapTest.class).appendToFile(logFile)) - .encoders(new FormEncoder()) .logLevel(FULL) .target(FormUrlEncodedApi.class, "http://localhost:8080"); } diff --git a/form/src/test/java/feign/form/issues/Issue63Test.java b/form/src/test/java/feign/form/issues/Issue63Test.java index d8b354daa..e0a6dafd0 100644 --- a/form/src/test/java/feign/form/issues/Issue63Test.java +++ b/form/src/test/java/feign/form/issues/Issue63Test.java @@ -36,7 +36,7 @@ void test() { try (var server = UndertowServer.builder().callback(this::handleRequest).start()) { var client = Feign.builder() - .encoders(new FormEncoder(new JacksonEncoder())) + .encoder(new FormEncoder(new JacksonEncoder())) .target(Client.class, server.getConnectUrl()); var data = new HashMap(); diff --git a/form/src/test/java/feign/form/multipart/DelegateWriterTest.java b/form/src/test/java/feign/form/multipart/DelegateWriterTest.java index 223a94032..f2515cedc 100644 --- a/form/src/test/java/feign/form/multipart/DelegateWriterTest.java +++ b/form/src/test/java/feign/form/multipart/DelegateWriterTest.java @@ -18,10 +18,7 @@ import static org.assertj.core.api.Assertions.assertThat; import feign.Request; -import feign.RequestTemplate; -import feign.codec.EncodeException; import feign.codec.Encoder; -import java.lang.reflect.Type; import java.nio.charset.StandardCharsets; import org.junit.jupiter.api.Test; @@ -34,18 +31,10 @@ class DelegateWriterTest { @Test void usesContentTypeFromDelegate() throws Exception { Encoder delegate = - new Encoder() { - @Override - public void encode(Object object, Type bodyType, RequestTemplate template) - throws EncodeException { - template.header("Content-Type", "application/json"); - template.body(Request.Body.of("{\"hash\":\"somehash\"}")); - } - - @Override - public boolean canEncode(Object object, Type bodyType, RequestTemplate template) { - return true; - } + (object, bodyType, template) -> { + template.header("Content-Type", "application/json"); + template.body(Request.Body.of("{\"hash\":\"somehash\"}")); + return true; }; assertThat(write(delegate)) @@ -56,17 +45,9 @@ public boolean canEncode(Object object, Type bodyType, RequestTemplate template) @Test void fallsBackToTextPlainWhenDelegateSetsNoContentType() throws Exception { Encoder delegate = - new Encoder() { - @Override - public void encode(Object object, Type bodyType, RequestTemplate template) - throws EncodeException { - template.body(Request.Body.of("plain")); - } - - @Override - public boolean canEncode(Object object, Type bodyType, RequestTemplate template) { - return true; - } + (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 e45122042..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,18 +51,6 @@ public void encode(Object object, Type bodyType, RequestTemplate template) } delegate.encode(graphqlBody, MAP_STRING_WILDCARD, template); - } - - /** - * {@inheritDoc} - * - * @param object {@inheritDoc} - * @param bodyType {@inheritDoc} - * @param template {@inheritDoc} - * @return {@inheritDoc} - */ - @Override - public boolean canEncode(Object object, Type bodyType, RequestTemplate template) { - return contract.lookupMetadata(template) != null; + 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/README.md b/gson/README.md index 8e08f08f5..c005699be 100644 --- a/gson/README.md +++ b/gson/README.md @@ -15,7 +15,7 @@ You can also configure the encoder and decoder separately: ```java GitHub github = Feign.builder() - .encoders(new GsonEncoder()) + .encoder(new GsonEncoder()) .decoder(new GsonDecoder()) .target(GitHub.class, "https://api.github.com"); ``` 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 52024212f..1b0bcbf5e 100644 --- a/hc5/src/test/java/feign/hc5/AsyncApacheHttp5ClientTest.java +++ b/hc5/src/test/java/feign/hc5/AsyncApacheHttp5ClientTest.java @@ -162,12 +162,8 @@ 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); - } - - @Override - public boolean canEncode(Object object, Type bodyType, RequestTemplate template) { return true; } }) @@ -665,17 +661,8 @@ void okIfEncodeRootCauseHasNoMessage() throws Throwable { final TestInterfaceAsync api = new TestInterfaceAsyncBuilder() .encoder( - new Encoder() { - @Override - public void encode(Object object, Type bodyType, RequestTemplate template) - throws EncodeException { - throw new RuntimeException(); - } - - @Override - public boolean canEncode(Object object, Type bodyType, RequestTemplate template) { - return true; - } + (_, _, _) -> { + throw new RuntimeException(); }) .target("http://localhost:" + server.getPort()); @@ -1100,22 +1087,14 @@ static final class TestInterfaceAsyncBuilder { AsyncFeign.builder() .client(new AsyncApacheHttp5Client()) .decoder(new DefaultDecoder()) - .encoders( - new Encoder() { - @Override - public void encode(Object object, Type bodyType, RequestTemplate template) - throws EncodeException { - if (object instanceof Map) { - template.body(Request.Body.of(new Gson().toJson(object))); - } else { - template.body(Request.Body.of(object.toString())); - } - } - - @Override - public boolean canEncode(Object object, Type bodyType, RequestTemplate template) { - return true; + .encoder( + (object, _, template) -> { + if (object instanceof Map) { + template.body(Request.Body.of(new Gson().toJson(object))); + } else { + template.body(Request.Body.of(object.toString())); } + return true; }); TestInterfaceAsyncBuilder requestInterceptor(RequestInterceptor requestInterceptor) { @@ -1124,7 +1103,7 @@ TestInterfaceAsyncBuilder requestInterceptor(RequestInterceptor requestIntercept } TestInterfaceAsyncBuilder encoder(Encoder encoder) { - delegate.encoders(encoder); + delegate.encoder(encoder); return this; } diff --git a/hystrix/src/main/java/feign/hystrix/HystrixFeign.java b/hystrix/src/main/java/feign/hystrix/HystrixFeign.java index e1ddf2d2f..08ea3fed2 100644 --- a/hystrix/src/main/java/feign/hystrix/HystrixFeign.java +++ b/hystrix/src/main/java/feign/hystrix/HystrixFeign.java @@ -31,7 +31,6 @@ import feign.codec.ErrorDecoder; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; -import java.util.List; import java.util.Map; /** @@ -169,41 +168,11 @@ public Builder logger(Logger logger) { return (Builder) super.logger(logger); } - /** - * {@inheritDoc} - * - * @deprecated use {@link #encoders(Encoder...)} or {@link #encoders(List)} instead - * @param encoder {@inheritDoc} - * @return {@inheritDoc} - */ @Override - @Deprecated(since = "14", forRemoval = true) public Builder encoder(Encoder encoder) { return (Builder) super.encoder(encoder); } - /** - * {@inheritDoc} - * - * @param encoders {@inheritDoc} - * @return {@inheritDoc} - */ - @Override - public Builder encoders(Encoder... encoders) { - return (Builder) super.encoders(encoders); - } - - /** - * {@inheritDoc} - * - * @param encoders {@inheritDoc} - * @return {@inheritDoc} - */ - @Override - public Builder encoders(List encoders) { - return (Builder) super.encoders(encoders); - } - @Override public Builder decoder(Decoder decoder) { return (Builder) super.decoder(decoder); diff --git a/jackson-jaxb/README.md b/jackson-jaxb/README.md index 6015ae7a4..a9bdbf4ca 100644 --- a/jackson-jaxb/README.md +++ b/jackson-jaxb/README.md @@ -7,7 +7,7 @@ Add `JacksonJaxbJsonEncoder` and/or `JacksonJaxbJsonDecoder` to your `Feign.Buil ```java GitHub github = Feign.builder() - .encoders(new JacksonJaxbJsonEncoder()) + .encoder(new JacksonJaxbJsonEncoder()) .decoder(new JacksonJaxbJsonDecoder()) .target(GitHub.class, "https://api.github.com"); ``` @@ -21,7 +21,7 @@ ObjectMapper mapper = new ObjectMapper() .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); GitHub github = Feign.builder() - .encoders(new JacksonJaxbJsonEncoder(mapper)) + .encoder(new JacksonJaxbJsonEncoder(mapper)) .decoder(new JacksonJaxbJsonDecoder(mapper)) .target(GitHub.class, "https://api.github.com"); ``` 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 a49f64d29..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,13 +22,14 @@ import com.fasterxml.jackson.jaxrs.json.JacksonJaxbJsonProvider; import feign.Request; import feign.RequestTemplate; +import feign.Util; import feign.codec.EncodeException; -import feign.codec.JsonEncoder; +import feign.codec.Encoder; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.lang.reflect.Type; -public final class JacksonJaxbJsonEncoder implements JsonEncoder { +public final class JacksonJaxbJsonEncoder implements Encoder { private final JacksonJaxbJsonProvider jacksonJaxbJsonProvider; public JacksonJaxbJsonEncoder() { @@ -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/README.md b/jackson-jr/README.md index 0b594b9f1..fcc586579 100644 --- a/jackson-jr/README.md +++ b/jackson-jr/README.md @@ -9,7 +9,7 @@ Add `JacksonJrEncoder` and/or `JacksonJrDecoder` to your `Feign.Builder` like so ```java GitHub github = Feign.builder() - .encoders(new JacksonJrEncoder()) + .encoder(new JacksonJrEncoder()) .decoder(new JacksonJrDecoder()) .target(GitHub.class, "https://api.github.com"); ``` @@ -24,7 +24,7 @@ JSON json = Json.builder() .build(); GitHub github = Feign.builder() - .encoders(new JacksonJrEncoder(json)) + .encoder(new JacksonJrEncoder(json)) .decoder(new JacksonJrDecoder(json)) .target(GitHub.class, "https://api.github.com"); ``` @@ -37,7 +37,7 @@ List extensions = singletonList(JacksonAnnotationExtension.b .withVisibility(JsonAutoDetect.Value.defaultVisibility()) .build()); GitHub github = Feign.builder() - .encoders(new JacksonJrEncoder(extensions)) + .encoder(new JacksonJrEncoder(extensions)) .decoder(new JacksonJrDecoder(extensions)) .target(GitHub.class, "https://api.github.com"); ``` 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 2104ab056..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,14 +19,14 @@ 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 feign.codec.JsonEncoder; import java.io.IOException; import java.lang.reflect.Type; /** A {@link Encoder} that uses Jackson Jr to convert objects to String or byte representation. */ -public class JacksonJrEncoder extends JacksonJrMapper implements JsonEncoder { +public class JacksonJrEncoder extends JacksonJrMapper implements Encoder { public JacksonJrEncoder() { super(); @@ -52,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/README.md b/jackson/README.md index e4e59da5e..31e8cbd43 100644 --- a/jackson/README.md +++ b/jackson/README.md @@ -28,7 +28,7 @@ You can also configure the encoder and decoder separately: ```java GitHub github = Feign.builder() - .encoders(new JacksonEncoder()) + .encoder(new JacksonEncoder()) .decoder(new JacksonDecoder()) .target(GitHub.class, "https://api.github.com"); ``` 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/README.md b/jackson3/README.md index b746ee805..457fd6cce 100644 --- a/jackson3/README.md +++ b/jackson3/README.md @@ -31,7 +31,7 @@ You can also configure the encoder and decoder separately: ```java GitHub github = Feign.builder() - .encoders(new Jackson3Encoder()) + .encoder(new Jackson3Encoder()) .decoder(new Jackson3Decoder()) .target(GitHub.class, "https://api.github.com"); ``` 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 921108199..d386814df 100644 --- a/java11/src/test/java/feign/http2client/test/Http2ClientAsyncTest.java +++ b/java11/src/test/java/feign/http2client/test/Http2ClientAsyncTest.java @@ -169,12 +169,8 @@ 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); - } - - @Override - public boolean canEncode(Object object, Type bodyType, RequestTemplate template) { return true; } }) @@ -653,17 +649,8 @@ void okIfEncodeRootCauseHasNoMessage() throws Throwable { final TestInterfaceAsync api = newAsyncBuilder() .encoder( - new Encoder() { - @Override - public void encode(Object object, Type bodyType, RequestTemplate template) - throws EncodeException { - throw new RuntimeException(); - } - - @Override - public boolean canEncode(Object object, Type bodyType, RequestTemplate template) { - return true; - } + (_, _, _) -> { + throw new RuntimeException(); }) .target("http://localhost:" + server.getPort()); @@ -1043,22 +1030,14 @@ static final class TestInterfaceAsyncBuilder { AsyncFeign.builder() .client(new Http2Client()) .decoder(new DefaultDecoder()) - .encoders( - new Encoder() { - @Override - public void encode(Object object, Type bodyType, RequestTemplate template) - throws EncodeException { - if (object instanceof Map) { - template.body(Request.Body.of(new Gson().toJson(object))); - } else { - template.body(Request.Body.of(object.toString())); - } - } - - @Override - public boolean canEncode(Object object, Type bodyType, RequestTemplate template) { - return true; + .encoder( + (object, _, template) -> { + if (object instanceof Map) { + template.body(Request.Body.of(new Gson().toJson(object))); + } else { + template.body(Request.Body.of(object.toString())); } + return true; }); TestInterfaceAsyncBuilder requestInterceptor(RequestInterceptor requestInterceptor) { @@ -1067,7 +1046,7 @@ TestInterfaceAsyncBuilder requestInterceptor(RequestInterceptor requestIntercept } TestInterfaceAsyncBuilder encoder(Encoder encoder) { - delegate.encoders(encoder); + delegate.encoder(encoder); return this; } diff --git a/jaxb-jakarta/src/main/java/feign/jaxb/JAXBEncoder.java b/jaxb-jakarta/src/main/java/feign/jaxb/JAXBEncoder.java index 871f6396d..472bc8638 100644 --- a/jaxb-jakarta/src/main/java/feign/jaxb/JAXBEncoder.java +++ b/jaxb-jakarta/src/main/java/feign/jaxb/JAXBEncoder.java @@ -17,8 +17,9 @@ import feign.Request; import feign.RequestTemplate; +import feign.Util; import feign.codec.EncodeException; -import feign.codec.XmlEncoder; +import feign.codec.Encoder; import jakarta.xml.bind.JAXBException; import jakarta.xml.bind.Marshaller; import java.io.StringWriter; @@ -36,14 +37,14 @@ * .build(); * * api = Feign.builder() - * .encoders(new JAXBEncoder(jaxbFactory)) + * .encoder(new JAXBEncoder(jaxbFactory)) * .target(MyApi.class, "http://api"); * * *

The JAXBContextFactory should be reused across requests as it caches the created JAXB * contexts. */ -public class JAXBEncoder implements XmlEncoder { +public class JAXBEncoder implements Encoder { private final JAXBContextFactory jaxbContextFactory; @@ -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/README.md b/jaxb/README.md index 8477f0a02..8ba82407a 100644 --- a/jaxb/README.md +++ b/jaxb/README.md @@ -20,7 +20,7 @@ You can also configure the encoder and decoder separately: ```java Response response = Feign.builder() - .encoders(new JAXBEncoder(jaxbFactory)) + .encoder(new JAXBEncoder(jaxbFactory)) .decoder(new JAXBDecoder(jaxbFactory)) .target(Response.class, "https://apihost"); ``` diff --git a/jaxb/src/main/java/feign/jaxb/JAXBEncoder.java b/jaxb/src/main/java/feign/jaxb/JAXBEncoder.java index 21d05c5d1..10fbdcffc 100644 --- a/jaxb/src/main/java/feign/jaxb/JAXBEncoder.java +++ b/jaxb/src/main/java/feign/jaxb/JAXBEncoder.java @@ -17,8 +17,9 @@ import feign.Request; import feign.RequestTemplate; +import feign.Util; import feign.codec.EncodeException; -import feign.codec.XmlEncoder; +import feign.codec.Encoder; import java.io.StringWriter; import java.lang.reflect.Type; import javax.xml.bind.JAXBException; @@ -36,14 +37,14 @@ * .build(); * * api = Feign.builder() - * .encoders(new JAXBEncoder(jaxbFactory)) + * .encoder(new JAXBEncoder(jaxbFactory)) * .target(MyApi.class, "http://api"); * * *

The JAXBContextFactory should be reused across requests as it caches the created JAXB * contexts. */ -public class JAXBEncoder implements XmlEncoder { +public class JAXBEncoder implements Encoder { private final JAXBContextFactory jaxbContextFactory; @@ -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/README.md b/json/README.md index 0deb0ac6b..108e6b8f0 100644 --- a/json/README.md +++ b/json/README.md @@ -8,7 +8,7 @@ Add `JsonEncoder` and/or `JsonDecoder` to your `Feign.Builder` like so: ```java api = Feign.builder() .decoder(new JsonDecoder()) - .encoders(new JsonEncoder()) + .encoder(new JsonEncoder()) .target(GitHub.class, "https://api"); ``` diff --git a/json/src/main/java/feign/json/JsonEncoder.java b/json/src/main/java/feign/json/JsonEncoder.java index 4b9ee99a5..a841132aa 100644 --- a/json/src/main/java/feign/json/JsonEncoder.java +++ b/json/src/main/java/feign/json/JsonEncoder.java @@ -20,6 +20,7 @@ import feign.Request; import feign.RequestTemplate; import feign.codec.EncodeException; +import feign.codec.Encoder; import java.lang.reflect.Type; import org.json.JSONArray; import org.json.JSONObject; @@ -41,7 +42,7 @@ * * GitHub github = Feign.builder() * .decoder(new JsonDecoder()) - * .encoders(new JsonEncoder()) + * .encoder(new JsonEncoder()) * .target(GitHub.class, "https://api.github.com"); * * JSONObject contributor = new JSONObject(); @@ -51,14 +52,15 @@ * github.create("openfeign", "feign", contributor); * */ -public class JsonEncoder implements feign.codec.JsonEncoder { +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/json/src/test/java/feign/json/JsonCodecTest.java b/json/src/test/java/feign/json/JsonCodecTest.java index 9624d811b..9063222d4 100644 --- a/json/src/test/java/feign/json/JsonCodecTest.java +++ b/json/src/test/java/feign/json/JsonCodecTest.java @@ -20,7 +20,6 @@ import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import feign.Feign; -import feign.Headers; import feign.Param; import feign.Request; import feign.RequestLine; @@ -40,7 +39,6 @@ interface GitHub { JSONArray contributors(@Param("owner") String owner, @Param("repo") String repo); @RequestLine("POST /repos/{owner}/{repo}/contributors") - @Headers("Content-Type: application/json") JSONObject create( @Param("owner") String owner, @Param("repo") String repo, JSONObject contributor); } @@ -56,7 +54,7 @@ void setUp() { github = Feign.builder() .decoder(new JsonDecoder()) - .encoders(new JsonEncoder()) + .encoder(new JsonEncoder()) .client(mockClient) .target(new MockTarget<>(GitHub.class)); } diff --git a/kotlin/src/main/java/feign/kotlin/CoroutineFeign.java b/kotlin/src/main/java/feign/kotlin/CoroutineFeign.java index 352a2f26a..b20f56aea 100644 --- a/kotlin/src/main/java/feign/kotlin/CoroutineFeign.java +++ b/kotlin/src/main/java/feign/kotlin/CoroutineFeign.java @@ -172,7 +172,7 @@ public CoroutineFeign internalBuild() { .contract(contract) .retryer(retryer) .logger(logger) - .encoders(encoder) + .encoder(encoder) .queryMapEncoder(queryMapEncoder) .options(options) .requestInterceptors(requestInterceptors) diff --git a/kotlin/src/test/kotlin/feign/kotlin/CoroutineFeignTest.kt b/kotlin/src/test/kotlin/feign/kotlin/CoroutineFeignTest.kt index e391c97a9..e9309a745 100644 --- a/kotlin/src/test/kotlin/feign/kotlin/CoroutineFeignTest.kt +++ b/kotlin/src/test/kotlin/feign/kotlin/CoroutineFeignTest.kt @@ -22,7 +22,6 @@ import feign.QueryMapEncoder import feign.Request import feign.RequestInterceptor import feign.RequestLine -import feign.RequestTemplate import feign.Response import feign.Util import feign.codec.Decoder @@ -190,19 +189,14 @@ class CoroutineFeignTest { internal class TestInterfaceAsyncBuilder { private val delegate = CoroutineFeign.builder() - .decoder(DefaultDecoder()).encoders(object : Encoder { - override fun encode(`object`: Any?, bodyType: Type, template: RequestTemplate) { + .decoder(DefaultDecoder()).encoder { `object`, bodyType, template -> if (`object` is Map<*, *>) { - template.body(Request.Body.of(Gson().toJson(`object`))) + template.body(Request.Body.of(Gson().toJson(`object`))); } else { - template.body(Request.Body.of(`object`.toString())) + template.body(Request.Body.of(`object`.toString())); } - } - - override fun canEncode(`object`: Any?, bodyType: Type, template: RequestTemplate): Boolean { - return true - } - }) + return@encoder true; + } fun requestInterceptor(requestInterceptor: RequestInterceptor?): TestInterfaceAsyncBuilder { delegate.requestInterceptor(requestInterceptor) @@ -210,7 +204,7 @@ class CoroutineFeignTest { } fun encoder(encoder: Encoder?): TestInterfaceAsyncBuilder { - delegate.encoders(encoder) + delegate.encoder(encoder) return this } diff --git a/micrometer/src/main/java/feign/micrometer/MeteredEncoder.java b/micrometer/src/main/java/feign/micrometer/MeteredEncoder.java index be2510b38..49442aa42 100644 --- a/micrometer/src/main/java/feign/micrometer/MeteredEncoder.java +++ b/micrometer/src/main/java/feign/micrometer/MeteredEncoder.java @@ -53,29 +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))); - } - - /** - * {@inheritDoc} - * - * @param object {@inheritDoc} - * @param bodyType {@inheritDoc} - * @param template {@inheritDoc} - * @return {@inheritDoc} - */ - @Override - public boolean canEncode(Object object, Type bodyType, RequestTemplate template) { - return encoder.canEncode(object, bodyType, template); + return isEncoded; } protected Timer createTimer(Object object, Type bodyType, RequestTemplate template) { diff --git a/moshi/README.md b/moshi/README.md index e97e1ef83..6715ad4e7 100644 --- a/moshi/README.md +++ b/moshi/README.md @@ -15,7 +15,7 @@ You can also configure the encoder and decoder separately: ```java GitHub github = Feign.builder() - .encoders(new MoshiEncoder()) + .encoder(new MoshiEncoder()) .decoder(new MoshiDecoder()) .target(GitHub.class, "https://api.github.com"); ``` 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/moshi/src/test/java/feign/moshi/examples/GithubExample.java b/moshi/src/test/java/feign/moshi/examples/GithubExample.java index 752cee5fd..b1e5e2c04 100644 --- a/moshi/src/test/java/feign/moshi/examples/GithubExample.java +++ b/moshi/src/test/java/feign/moshi/examples/GithubExample.java @@ -27,7 +27,7 @@ public class GithubExample { public static void main(String... args) { GitHub github = Feign.builder() - .encoders(new MoshiEncoder()) + .encoder(new MoshiEncoder()) .decoder(new MoshiDecoder()) .target(GitHub.class, "https://api.github.com"); diff --git a/okhttp/src/test/java/feign/okhttp/OkHttpClientAsyncTest.java b/okhttp/src/test/java/feign/okhttp/OkHttpClientAsyncTest.java index d2b6fce7c..e237c766d 100644 --- a/okhttp/src/test/java/feign/okhttp/OkHttpClientAsyncTest.java +++ b/okhttp/src/test/java/feign/okhttp/OkHttpClientAsyncTest.java @@ -168,12 +168,8 @@ 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); - } - - @Override - public boolean canEncode(Object object, Type bodyType, RequestTemplate template) { return true; } }) @@ -652,17 +648,8 @@ void okIfEncodeRootCauseHasNoMessage() throws Throwable { final TestInterfaceAsync api = newAsyncBuilder() .encoder( - new Encoder() { - @Override - public void encode(Object object, Type bodyType, RequestTemplate template) - throws EncodeException { - throw new RuntimeException(); - } - - @Override - public boolean canEncode(Object object, Type bodyType, RequestTemplate template) { - return true; - } + (_, _, _) -> { + throw new RuntimeException(); }) .target("http://localhost:" + server.getPort()); @@ -1042,22 +1029,14 @@ static final class TestInterfaceAsyncBuilder { AsyncFeign.builder() .client(new OkHttpClient()) .decoder(new DefaultDecoder()) - .encoders( - new Encoder() { - @Override - public void encode(Object object, Type bodyType, RequestTemplate template) - throws EncodeException { - if (object instanceof Map) { - template.body(Request.Body.of(new Gson().toJson(object))); - } else { - template.body(Request.Body.of(object.toString())); - } - } - - @Override - public boolean canEncode(Object object, Type bodyType, RequestTemplate template) { - return true; + .encoder( + (object, _, template) -> { + if (object instanceof Map) { + template.body(Request.Body.of(new Gson().toJson(object))); + } else { + template.body(Request.Body.of(object.toString())); } + return true; }); TestInterfaceAsyncBuilder requestInterceptor(RequestInterceptor requestInterceptor) { @@ -1066,7 +1045,7 @@ TestInterfaceAsyncBuilder requestInterceptor(RequestInterceptor requestIntercept } TestInterfaceAsyncBuilder encoder(Encoder encoder) { - delegate.encoders(encoder); + delegate.encoder(encoder); return this; } 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/reactive/src/test/java/feign/reactive/ReactiveFeignIntegrationTest.java b/reactive/src/test/java/feign/reactive/ReactiveFeignIntegrationTest.java index c3942c65e..d892170fc 100644 --- a/reactive/src/test/java/feign/reactive/ReactiveFeignIntegrationTest.java +++ b/reactive/src/test/java/feign/reactive/ReactiveFeignIntegrationTest.java @@ -113,7 +113,7 @@ void reactorTargetFull() throws Exception { TestReactorService service = ReactorFeign.builder() - .encoders(new JacksonEncoder()) + .encoder(new JacksonEncoder()) .decoder(new ReactorDecoder(new JacksonDecoder())) .logger(new ConsoleLogger()) .dismiss404() @@ -154,7 +154,7 @@ void rxJavaTarget() throws Exception { TestReactiveXService service = RxJavaFeign.builder() - .encoders(new JacksonEncoder()) + .encoder(new JacksonEncoder()) .decoder(new RxJavaDecoder(new JacksonDecoder())) .logger(new ConsoleLogger()) .logLevel(Level.FULL) diff --git a/soap-jakarta/README.md b/soap-jakarta/README.md index db9b7b4d7..dfac4cc43 100644 --- a/soap-jakarta/README.md +++ b/soap-jakarta/README.md @@ -42,7 +42,7 @@ You can also configure the encoder and decoder separately: ```java api = Feign.builder() - .encoders(new SOAPEncoder(jaxbFactory)) + .encoder(new SOAPEncoder(jaxbFactory)) .decoder(new SOAPDecoder(jaxbFactory)) .target(MyApi.class, "http://api"); ``` diff --git a/soap-jakarta/src/main/java/feign/soap/SOAPEncoder.java b/soap-jakarta/src/main/java/feign/soap/SOAPEncoder.java index dd05f673e..97ab5a666 100644 --- a/soap-jakarta/src/main/java/feign/soap/SOAPEncoder.java +++ b/soap-jakarta/src/main/java/feign/soap/SOAPEncoder.java @@ -17,8 +17,9 @@ import feign.Request; import feign.RequestTemplate; +import feign.Util; import feign.codec.EncodeException; -import feign.codec.XmlEncoder; +import feign.codec.Encoder; import feign.jaxb.JAXBContextFactory; import jakarta.xml.bind.JAXBException; import jakarta.xml.bind.Marshaller; @@ -68,7 +69,7 @@ * .build(); * * api = Feign.builder() - * .encoders(new SOAPEncoder(jaxbFactory)) + * .encoder(new SOAPEncoder(jaxbFactory)) * .target(MyApi.class, "http://api"); * * ... @@ -83,7 +84,7 @@ *

The JAXBContextFactory should be reused across requests as it caches the created JAXB * contexts. */ -public class SOAPEncoder implements XmlEncoder { +public class SOAPEncoder implements Encoder { private static final String DEFAULT_SOAP_PROTOCOL = SOAPConstants.SOAP_1_1_PROTOCOL; @@ -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/README.md b/soap/README.md index db9b7b4d7..dfac4cc43 100644 --- a/soap/README.md +++ b/soap/README.md @@ -42,7 +42,7 @@ You can also configure the encoder and decoder separately: ```java api = Feign.builder() - .encoders(new SOAPEncoder(jaxbFactory)) + .encoder(new SOAPEncoder(jaxbFactory)) .decoder(new SOAPDecoder(jaxbFactory)) .target(MyApi.class, "http://api"); ``` diff --git a/soap/src/main/java/feign/soap/SOAPEncoder.java b/soap/src/main/java/feign/soap/SOAPEncoder.java index 8ac4b7e59..9f4017071 100644 --- a/soap/src/main/java/feign/soap/SOAPEncoder.java +++ b/soap/src/main/java/feign/soap/SOAPEncoder.java @@ -17,8 +17,9 @@ import feign.Request; import feign.RequestTemplate; +import feign.Util; import feign.codec.EncodeException; -import feign.codec.XmlEncoder; +import feign.codec.Encoder; import feign.jaxb.JAXBContextFactory; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -68,7 +69,7 @@ * .build(); * * api = Feign.builder() - * .encoders(new SOAPEncoder(jaxbFactory)) + * .encoder(new SOAPEncoder(jaxbFactory)) * .target(MyApi.class, "http://api"); * * ... @@ -83,7 +84,7 @@ *

The JAXBContextFactory should be reused across requests as it caches the created JAXB * contexts. */ -public class SOAPEncoder implements XmlEncoder { +public class SOAPEncoder implements Encoder { private static final String DEFAULT_SOAP_PROTOCOL = SOAPConstants.SOAP_1_1_PROTOCOL; @@ -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 0316f9f71..0aba410bb 100755 --- a/spring/src/test/java/feign/spring/SpringContractTest.java +++ b/spring/src/test/java/feign/spring/SpringContractTest.java @@ -20,7 +20,6 @@ import feign.Feign; import feign.Param; import feign.Request; -import feign.RequestTemplate; import feign.Response; import feign.ResponseMapper; import feign.jackson.JacksonDecoder; @@ -90,13 +89,7 @@ void setup() throws IOException { resource = Feign.builder() .contract(new SpringContract()) - .encoders( - new JacksonEncoder() { - @Override - public boolean canEncode(Object object, Type bodyType, RequestTemplate template) { - return true; - } - }) + .encoder(new JacksonEncoder()) .mapAndDecode(new TextResponseMapper(), new JacksonDecoder()) .client(mockClient) .target(new MockTarget<>(HealthResource.class)); @@ -355,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 2fec4c072..b01243058 100644 --- a/validation-jakarta/src/test/java/feign/validation/BeanValidationMethodInterceptorTest.java +++ b/validation-jakarta/src/test/java/feign/validation/BeanValidationMethodInterceptorTest.java @@ -22,15 +22,11 @@ import feign.Param; import feign.Request; import feign.RequestLine; -import feign.RequestTemplate; -import feign.codec.EncodeException; -import feign.codec.Encoder; import jakarta.validation.ConstraintViolationException; import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import java.io.IOException; -import java.lang.reflect.Type; import mockwebserver3.MockResponse; import mockwebserver3.MockWebServer; import org.junit.jupiter.api.AfterEach; @@ -88,18 +84,10 @@ interface Api { private Api api() { return Feign.builder() - .encoders( - new Encoder() { - @Override - public void encode(Object object, Type bodyType, RequestTemplate template) - throws EncodeException { - template.body(Request.Body.of(String.valueOf(object))); - } - - @Override - public boolean canEncode(Object object, Type bodyType, RequestTemplate template) { - return true; - } + .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 a891428c0..1fe04d256 100644 --- a/validation/src/test/java/feign/validation/BeanValidationMethodInterceptorTest.java +++ b/validation/src/test/java/feign/validation/BeanValidationMethodInterceptorTest.java @@ -22,11 +22,7 @@ import feign.Param; import feign.Request; import feign.RequestLine; -import feign.RequestTemplate; -import feign.codec.EncodeException; -import feign.codec.Encoder; import java.io.IOException; -import java.lang.reflect.Type; import javax.validation.ConstraintViolationException; import javax.validation.Valid; import javax.validation.constraints.NotBlank; @@ -88,18 +84,10 @@ interface Api { private Api api() { return Feign.builder() - .encoders( - new Encoder() { - @Override - public void encode(Object object, Type bodyType, RequestTemplate template) - throws EncodeException { - template.body(Request.Body.of(String.valueOf(object))); - } - - @Override - public boolean canEncode(Object object, Type bodyType, RequestTemplate template) { - return true; - } + .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/vertx/README.md b/vertx/README.md index 9ce2d558d..4e89037ac 100644 --- a/vertx/README.md +++ b/vertx/README.md @@ -67,7 +67,7 @@ WebClient webClient = WebClient.create(vertx); // create Vert.x WebClient IcecreamServiceApi icecreamApi = VertxFeign .builder() .webClient(webClient) // provide WebClient instance - .encoders(new JacksonEncoder()) + .encoder(new JacksonEncoder()) .decoder(new JacksonDecoder()) .target(IcecreamServiceApi.class, "https://www.icecream.com"); diff --git a/vertx/feign-vertx/src/main/java/feign/VertxFeign.java b/vertx/feign-vertx/src/main/java/feign/VertxFeign.java index 8e1310964..36945ed6f 100644 --- a/vertx/feign-vertx/src/main/java/feign/VertxFeign.java +++ b/vertx/feign-vertx/src/main/java/feign/VertxFeign.java @@ -20,7 +20,6 @@ import feign.InvocationHandlerFactory.MethodHandler; import feign.codec.Decoder; -import feign.codec.DelegatingEncoder; import feign.codec.Encoder; import feign.codec.ErrorDecoder; import feign.core.DefaultContract; @@ -39,7 +38,6 @@ import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.util.ArrayList; -import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -194,39 +192,14 @@ public Builder logger(final Logger logger) { } /** - * {@inheritDoc} + * Sets encoder. * - * @deprecated use {@link #encoders(Encoder...)} or {@link #encoders(List)} instead. - * @param encoder {@inheritDoc} - * @return {@inheritDoc} + * @param encoder encoder + * @return this builder */ @Override - @Deprecated(since = "14", forRemoval = true) public Builder encoder(final Encoder encoder) { - return encoders(checkNotNull(encoder, "Argument encoder must be not null")); - } - - /** - * {@inheritDoc} - * - * @param encoders {@inheritDoc} - * @return {@inheritDoc} - */ - @Override - public Builder encoders(final Encoder... encoders) { - return encoders(Arrays.asList(encoders)); - } - - /** - * {@inheritDoc} - * - * @param encoders {@inheritDoc} - * @return {@inheritDoc} - */ - @Override - public Builder encoders(final List encoders) { - this.encoder = - new DelegatingEncoder(checkNotNull(encoders, "Argument encoders must be not null")); + this.encoder = checkNotNull(encoder, "Argument encoder must be not null"); return this; } diff --git a/vertx/feign-vertx/src/test/java/feign/vertx/ConnectionsLeakTests.java b/vertx/feign-vertx/src/test/java/feign/vertx/ConnectionsLeakTests.java index 2e522b065..baabdc4c9 100644 --- a/vertx/feign-vertx/src/test/java/feign/vertx/ConnectionsLeakTests.java +++ b/vertx/feign-vertx/src/test/java/feign/vertx/ConnectionsLeakTests.java @@ -87,7 +87,7 @@ void http11NoConnectionLeak(Vertx vertx, VertxTestContext testContext) { VertxFeign.builder() .vertx(vertx) .webClient(webClient) - .encoders(new JacksonEncoder()) + .encoder(new JacksonEncoder()) .decoder(new JacksonDecoder()) .target(HelloServiceAPI.class, "http://localhost:8091"); @@ -115,7 +115,7 @@ void http2NoConnectionLeak(Vertx vertx, VertxTestContext testContext) { VertxFeign.builder() .vertx(vertx) .webClient(webClient) - .encoders(new JacksonEncoder()) + .encoder(new JacksonEncoder()) .decoder(new JacksonDecoder()) .target(HelloServiceAPI.class, "http://localhost:8091"); diff --git a/vertx/feign-vertx/src/test/java/feign/vertx/Http11ClientReconnectTest.java b/vertx/feign-vertx/src/test/java/feign/vertx/Http11ClientReconnectTest.java index c37216d3b..88dab3713 100644 --- a/vertx/feign-vertx/src/test/java/feign/vertx/Http11ClientReconnectTest.java +++ b/vertx/feign-vertx/src/test/java/feign/vertx/Http11ClientReconnectTest.java @@ -40,7 +40,7 @@ protected void createClient(final Vertx vertx) { VertxFeign.builder() .vertx(vertx) .webClient(webClient) - .encoders(new JacksonEncoder()) + .encoder(new JacksonEncoder()) .decoder(new JacksonDecoder()) .target(HelloServiceAPI.class, baseUrl); } diff --git a/vertx/feign-vertx/src/test/java/feign/vertx/Http2ClientReconnectTest.java b/vertx/feign-vertx/src/test/java/feign/vertx/Http2ClientReconnectTest.java index d429d90e5..522edac84 100644 --- a/vertx/feign-vertx/src/test/java/feign/vertx/Http2ClientReconnectTest.java +++ b/vertx/feign-vertx/src/test/java/feign/vertx/Http2ClientReconnectTest.java @@ -42,7 +42,7 @@ protected void createClient(Vertx vertx) { client = VertxFeign.builder() .webClient(webClient) - .encoders(new JacksonEncoder()) + .encoder(new JacksonEncoder()) .decoder(new JacksonDecoder()) .target(HelloServiceAPI.class, baseUrl); } diff --git a/vertx/feign-vertx/src/test/java/feign/vertx/RawContractTest.java b/vertx/feign-vertx/src/test/java/feign/vertx/RawContractTest.java index 8a2d8d8c0..e81662247 100644 --- a/vertx/feign-vertx/src/test/java/feign/vertx/RawContractTest.java +++ b/vertx/feign-vertx/src/test/java/feign/vertx/RawContractTest.java @@ -47,7 +47,7 @@ static void createClient(Vertx vertx) { VertxFeign.builder() .vertx(vertx) .webClient(WebClient.create(vertx)) - .encoders(new JacksonEncoder(TestUtils.MAPPER)) + .encoder(new JacksonEncoder(TestUtils.MAPPER)) .decoder(new JacksonDecoder(TestUtils.MAPPER)) .target(RawServiceAPI.class, wireMock.baseUrl()); } diff --git a/vertx/feign-vertx/src/test/java/feign/vertx/VertxHttpClientTest.java b/vertx/feign-vertx/src/test/java/feign/vertx/VertxHttpClientTest.java index eb3a3c13f..7182080ec 100644 --- a/vertx/feign-vertx/src/test/java/feign/vertx/VertxHttpClientTest.java +++ b/vertx/feign-vertx/src/test/java/feign/vertx/VertxHttpClientTest.java @@ -220,7 +220,7 @@ void createClient(Vertx vertx) { VertxFeign.builder() .vertx(vertx) .webClient(WebClient.create(vertx)) - .encoders(new JacksonEncoder(TestUtils.MAPPER)) + .encoder(new JacksonEncoder(TestUtils.MAPPER)) .decoder(new JacksonDecoder(TestUtils.MAPPER)) .target(IcecreamServiceApi.class, wireMock.baseUrl()); } diff --git a/vertx/feign-vertx4-test/src/test/java/feign/vertx/ConnectionsLeakTests.java b/vertx/feign-vertx4-test/src/test/java/feign/vertx/ConnectionsLeakTests.java index 6a3b124c6..3006d493c 100644 --- a/vertx/feign-vertx4-test/src/test/java/feign/vertx/ConnectionsLeakTests.java +++ b/vertx/feign-vertx4-test/src/test/java/feign/vertx/ConnectionsLeakTests.java @@ -90,7 +90,7 @@ void http11NoConnectionLeak(Vertx vertx, VertxTestContext testContext) { VertxFeign.builder() .vertx(vertx) .webClient(webClient) - .encoders(new JacksonEncoder()) + .encoder(new JacksonEncoder()) .decoder(new JacksonDecoder()) .target(HelloServiceAPI.class, "http://localhost:8091"); @@ -118,7 +118,7 @@ void http2NoConnectionLeak(Vertx vertx, VertxTestContext testContext) { VertxFeign.builder() .vertx(vertx) .webClient(webClient) - .encoders(new JacksonEncoder()) + .encoder(new JacksonEncoder()) .decoder(new JacksonDecoder()) .target(HelloServiceAPI.class, "http://localhost:8091"); diff --git a/vertx/feign-vertx4-test/src/test/java/feign/vertx/Http11ClientReconnectTest.java b/vertx/feign-vertx4-test/src/test/java/feign/vertx/Http11ClientReconnectTest.java index eb4f1c6ad..bb6edaf4a 100644 --- a/vertx/feign-vertx4-test/src/test/java/feign/vertx/Http11ClientReconnectTest.java +++ b/vertx/feign-vertx4-test/src/test/java/feign/vertx/Http11ClientReconnectTest.java @@ -38,7 +38,7 @@ protected void createClient(final Vertx vertx) { VertxFeign.builder() .vertx(vertx) .webClient(webClient) - .encoders(new JacksonEncoder()) + .encoder(new JacksonEncoder()) .decoder(new JacksonDecoder()) .target(HelloServiceAPI.class, baseUrl); } diff --git a/vertx/feign-vertx4-test/src/test/java/feign/vertx/Http2ClientReconnectTest.java b/vertx/feign-vertx4-test/src/test/java/feign/vertx/Http2ClientReconnectTest.java index 148c60d0a..e6612729c 100644 --- a/vertx/feign-vertx4-test/src/test/java/feign/vertx/Http2ClientReconnectTest.java +++ b/vertx/feign-vertx4-test/src/test/java/feign/vertx/Http2ClientReconnectTest.java @@ -41,7 +41,7 @@ protected void createClient(Vertx vertx) { client = VertxFeign.builder() .webClient(webClient) - .encoders(new JacksonEncoder()) + .encoder(new JacksonEncoder()) .decoder(new JacksonDecoder()) .target(HelloServiceAPI.class, baseUrl); } diff --git a/vertx/feign-vertx4-test/src/test/java/feign/vertx/RawContractTest.java b/vertx/feign-vertx4-test/src/test/java/feign/vertx/RawContractTest.java index 8a2d8d8c0..e81662247 100644 --- a/vertx/feign-vertx4-test/src/test/java/feign/vertx/RawContractTest.java +++ b/vertx/feign-vertx4-test/src/test/java/feign/vertx/RawContractTest.java @@ -47,7 +47,7 @@ static void createClient(Vertx vertx) { VertxFeign.builder() .vertx(vertx) .webClient(WebClient.create(vertx)) - .encoders(new JacksonEncoder(TestUtils.MAPPER)) + .encoder(new JacksonEncoder(TestUtils.MAPPER)) .decoder(new JacksonDecoder(TestUtils.MAPPER)) .target(RawServiceAPI.class, wireMock.baseUrl()); } diff --git a/vertx/feign-vertx4-test/src/test/java/feign/vertx/VertxHttpClientTest.java b/vertx/feign-vertx4-test/src/test/java/feign/vertx/VertxHttpClientTest.java index eb3a3c13f..7182080ec 100644 --- a/vertx/feign-vertx4-test/src/test/java/feign/vertx/VertxHttpClientTest.java +++ b/vertx/feign-vertx4-test/src/test/java/feign/vertx/VertxHttpClientTest.java @@ -220,7 +220,7 @@ void createClient(Vertx vertx) { VertxFeign.builder() .vertx(vertx) .webClient(WebClient.create(vertx)) - .encoders(new JacksonEncoder(TestUtils.MAPPER)) + .encoder(new JacksonEncoder(TestUtils.MAPPER)) .decoder(new JacksonDecoder(TestUtils.MAPPER)) .target(IcecreamServiceApi.class, wireMock.baseUrl()); } From 4621dd7ee7697f444eb263fb520b43a398167dd1 Mon Sep 17 00:00:00 2001 From: Yevhen Vasyliev Date: Sun, 19 Jul 2026 18:58:15 +0300 Subject: [PATCH 28/63] docs: fix typos and add clarity to the docs Co-authored-by: trumpetinc <6618744+trumpetinc@users.noreply.github.com> --- MIGRATION-v14.md | 45 +++++++++++++++---- api/src/main/java/feign/codec/Encoder.java | 5 ++- .../feign/spring/SpringFormEncoderTest.java | 4 +- 3 files changed, 42 insertions(+), 12 deletions(-) diff --git a/MIGRATION-v14.md b/MIGRATION-v14.md index 5f1a8649e..013a3dcdc 100644 --- a/MIGRATION-v14.md +++ b/MIGRATION-v14.md @@ -384,22 +384,51 @@ public class MyEncoder implements Encoder { ``` Built-in encoders (`DefaultEncoder`, `FormEncoder`, `MeteredEncoder`, `GraphqlEncoder`, etc.) already return `boolean` -from `encode()`. If your encoder returns `false`, the `DelegatingEncoder` (see section 18) will try the next encoder. If +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. -**Built-in JSON and XML encoders now gate on the `Content-Type` header:** +--- + +### 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()` (section 18), this means a JSON or XML encoder will -only claim the request if a matching `Content-Type` header is present. +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. --- -### 15. `Encoder` moved from `core` to `api` module +### 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 @@ -407,7 +436,7 @@ module. The package name (`feign.codec`) is unchanged. If you have a direct depe --- -### 16. `Encoder.Default` removed +### 17. `Encoder.Default` removed The deprecated inner class `Encoder.Default` (which extended `DefaultEncoder`) has been removed. @@ -425,7 +454,7 @@ new feign.core.codec.DefaultEncoder() --- -### 17. Composing multiple encoders with `Encoder.of()` (https://github.com/OpenFeign/feign/pull/3476) +### 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`. @@ -463,7 +492,7 @@ each encoder's `encode()` and uses the first one that returns `true`. --- -### 18. Multi-encoder support — `DelegatingEncoder` (https://github.com/OpenFeign/feign/pull/3476) +### 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 diff --git a/api/src/main/java/feign/codec/Encoder.java b/api/src/main/java/feign/codec/Encoder.java index bfb40bfa1..f980a4566 100644 --- a/api/src/main/java/feign/codec/Encoder.java +++ b/api/src/main/java/feign/codec/Encoder.java @@ -103,8 +103,9 @@ static Encoder of(List encoders) { * @param bodyType the type the object should be encoded as. {@link #MAP_STRING_WILDCARD} * indicates form encoding. * @param template the request template to populate. - * @return {@code true} if the encoder handled the object, {@code false} otherwise. - * @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 */ boolean encode(Object object, Type bodyType, RequestTemplate template) throws EncodeException; } 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 c65bb1436..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 @@ -77,8 +77,8 @@ void uploadFileNameAndContentTypeTest() throws Exception { .isEqualTo(file.getOriginalFilename() + ':' + file.getContentType() + ':' + folder); } - // TODO: enable when Spring Cloud OpenFiegn migrates to Feign 14 - @Disabled("Disabled util Spring Cloud OpenFiegn migrates to Feign 14") + // 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(); From 7451e9d6bbd518dd9e89ed8fb39f37dc2188937d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 Jul 2026 05:15:30 +0000 Subject: [PATCH 29/63] build(deps): Bump org.json:json from 20260522 to 20260719 Bumps [org.json:json](https://github.com/douglascrockford/JSON-java) from 20260522 to 20260719. - [Release notes](https://github.com/douglascrockford/JSON-java/releases) - [Changelog](https://github.com/stleary/JSON-java/blob/master/docs/RELEASES.md) - [Commits](https://github.com/douglascrockford/JSON-java/compare/20260522...20260719) --- updated-dependencies: - dependency-name: org.json:json dependency-version: '20260719' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 94e7b9f7a..104f1cf77 100644 --- a/pom.xml +++ b/pom.xml @@ -170,7 +170,7 @@ 2.14.0 1.15.2 2.0.18 - 20260522 + 20260719 4.1.0 6.1.2 From c5c19f25e0791de50db2f0ba5086018932f13474 Mon Sep 17 00:00:00 2001 From: kevin Date: Mon, 20 Jul 2026 15:21:17 -0700 Subject: [PATCH 30/63] Remove json content-type checks --- api/src/test/java/feign/UtilTest.java | 25 +++---------------- .../feign/fastjson2/Fastjson2Encoder.java | 8 +++--- .../src/main/java/feign/gson/GsonEncoder.java | 10 +++----- .../jackson/jaxb/JacksonJaxbJsonEncoder.java | 12 ++++----- .../feign/jackson/jr/JacksonJrEncoder.java | 10 +++----- .../java/feign/jackson/JacksonEncoder.java | 10 +++----- .../java/feign/jackson3/Jackson3Encoder.java | 10 +++----- .../main/java/feign/moshi/MoshiEncoder.java | 8 +++--- pom.xml | 4 +-- 9 files changed, 32 insertions(+), 65 deletions(-) diff --git a/api/src/test/java/feign/UtilTest.java b/api/src/test/java/feign/UtilTest.java index 613aaeb4e..d87797061 100644 --- a/api/src/test/java/feign/UtilTest.java +++ b/api/src/test/java/feign/UtilTest.java @@ -23,7 +23,6 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertEquals; -import feign.codec.Decoder; import java.io.Reader; import java.lang.reflect.Type; import java.util.Arrays; @@ -35,10 +34,13 @@ import java.util.Map; 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; +import feign.codec.Decoder; + class UtilTest { @Test @@ -316,27 +318,6 @@ 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 = diff --git a/fastjson2/src/main/java/feign/fastjson2/Fastjson2Encoder.java b/fastjson2/src/main/java/feign/fastjson2/Fastjson2Encoder.java index ce477aaca..fc287b55f 100644 --- a/fastjson2/src/main/java/feign/fastjson2/Fastjson2Encoder.java +++ b/fastjson2/src/main/java/feign/fastjson2/Fastjson2Encoder.java @@ -15,15 +15,16 @@ */ package feign.fastjson2; +import java.lang.reflect.Type; + import com.alibaba.fastjson2.JSON; 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; -import java.lang.reflect.Type; /** * @author changjin wei(魏昌进) @@ -43,9 +44,6 @@ public Fastjson2Encoder(JSONWriter.Feature[] features) { @Override 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/gson/src/main/java/feign/gson/GsonEncoder.java b/gson/src/main/java/feign/gson/GsonEncoder.java index 105a0fea1..29fa89ae3 100644 --- a/gson/src/main/java/feign/gson/GsonEncoder.java +++ b/gson/src/main/java/feign/gson/GsonEncoder.java @@ -15,15 +15,16 @@ */ package feign.gson; +import java.lang.reflect.Type; +import java.util.Collections; + import com.google.gson.Gson; 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; -import java.util.Collections; public class GsonEncoder implements Encoder, JsonEncoder { @@ -43,9 +44,6 @@ public GsonEncoder(Gson gson) { @Override 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/jackson-jaxb/src/main/java/feign/jackson/jaxb/JacksonJaxbJsonEncoder.java b/jackson-jaxb/src/main/java/feign/jackson/jaxb/JacksonJaxbJsonEncoder.java index 7f5770169..3659a1fb2 100644 --- a/jackson-jaxb/src/main/java/feign/jackson/jaxb/JacksonJaxbJsonEncoder.java +++ b/jackson-jaxb/src/main/java/feign/jackson/jaxb/JacksonJaxbJsonEncoder.java @@ -18,16 +18,17 @@ import static com.fasterxml.jackson.jaxrs.json.JacksonJaxbJsonProvider.DEFAULT_ANNOTATIONS; import static javax.ws.rs.core.MediaType.APPLICATION_JSON_TYPE; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.lang.reflect.Type; + import com.fasterxml.jackson.databind.ObjectMapper; 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; -import java.io.IOException; -import java.lang.reflect.Type; public final class JacksonJaxbJsonEncoder implements Encoder { private final JacksonJaxbJsonProvider jacksonJaxbJsonProvider; @@ -43,9 +44,6 @@ public JacksonJaxbJsonEncoder(ObjectMapper objectMapper) { @Override public boolean encode(Object object, Type bodyType, RequestTemplate template) throws EncodeException { - if (!Util.isJsonContentType(template)) { - return false; - } ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); try { jacksonJaxbJsonProvider.writeTo( 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 93ec95abb..d19f189d5 100644 --- a/jackson-jr/src/main/java/feign/jackson/jr/JacksonJrEncoder.java +++ b/jackson-jr/src/main/java/feign/jackson/jr/JacksonJrEncoder.java @@ -15,15 +15,16 @@ */ package feign.jackson.jr; +import java.io.IOException; +import java.lang.reflect.Type; + import com.fasterxml.jackson.jr.ob.JSON; 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; -import java.lang.reflect.Type; /** A {@link Encoder} that uses Jackson Jr to convert objects to String or byte representation. */ public class JacksonJrEncoder extends JacksonJrMapper implements Encoder { @@ -53,9 +54,6 @@ public JacksonJrEncoder(Iterable iterable) { @Override 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))); diff --git a/jackson/src/main/java/feign/jackson/JacksonEncoder.java b/jackson/src/main/java/feign/jackson/JacksonEncoder.java index 1ad7b4284..886fa4626 100644 --- a/jackson/src/main/java/feign/jackson/JacksonEncoder.java +++ b/jackson/src/main/java/feign/jackson/JacksonEncoder.java @@ -15,20 +15,21 @@ */ package feign.jackson; +import java.lang.reflect.Type; +import java.util.Collections; + import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.Module; import com.fasterxml.jackson.databind.ObjectMapper; 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; -import java.lang.reflect.Type; -import java.util.Collections; public class JacksonEncoder implements Encoder, JsonEncoder { @@ -52,9 +53,6 @@ public JacksonEncoder(ObjectMapper mapper) { @Override 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))); diff --git a/jackson3/src/main/java/feign/jackson3/Jackson3Encoder.java b/jackson3/src/main/java/feign/jackson3/Jackson3Encoder.java index 72277ea96..19927f08b 100644 --- a/jackson3/src/main/java/feign/jackson3/Jackson3Encoder.java +++ b/jackson3/src/main/java/feign/jackson3/Jackson3Encoder.java @@ -15,15 +15,16 @@ */ package feign.jackson3; +import java.lang.reflect.Type; +import java.util.Collections; + 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; -import java.lang.reflect.Type; -import java.util.Collections; import tools.jackson.core.JacksonException; import tools.jackson.databind.JacksonModule; import tools.jackson.databind.JavaType; @@ -54,9 +55,6 @@ public Jackson3Encoder(JsonMapper mapper) { @Override 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))); diff --git a/moshi/src/main/java/feign/moshi/MoshiEncoder.java b/moshi/src/main/java/feign/moshi/MoshiEncoder.java index bbf672de6..4d6ed5855 100644 --- a/moshi/src/main/java/feign/moshi/MoshiEncoder.java +++ b/moshi/src/main/java/feign/moshi/MoshiEncoder.java @@ -15,14 +15,15 @@ */ package feign.moshi; +import java.lang.reflect.Type; + import com.squareup.moshi.JsonAdapter; 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; public class MoshiEncoder implements Encoder, JsonEncoder { @@ -42,9 +43,6 @@ public MoshiEncoder(Iterable> adapters) { @Override 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/pom.xml b/pom.xml index e980bfe38..3cb6a2bd2 100644 --- a/pom.xml +++ b/pom.xml @@ -158,7 +158,7 @@ -Duser.language=en - 11 + 25 25 @@ -188,7 +188,7 @@ 3.4.0 3.12.0 5.0.0 - 3.5.0 + 3.4.2 3.3.1 6.0.2 0.1.1 From 4d7b0630a689328ab5981dd7ac74ce960ebfdf0d Mon Sep 17 00:00:00 2001 From: kevin Date: Mon, 20 Jul 2026 15:22:35 -0700 Subject: [PATCH 31/63] Remove XML content-type checks --- api/src/test/java/feign/UtilTest.java | 21 -------------- .../src/main/java/feign/jaxb/JAXBEncoder.java | 9 ++---- .../src/main/java/feign/jaxb/JAXBEncoder.java | 14 ++++----- .../src/main/java/feign/soap/SOAPEncoder.java | 29 +++++++++---------- .../src/main/java/feign/soap/SOAPEncoder.java | 17 +++++------ 5 files changed, 31 insertions(+), 59 deletions(-) diff --git a/api/src/test/java/feign/UtilTest.java b/api/src/test/java/feign/UtilTest.java index d87797061..bcdcd1fe4 100644 --- a/api/src/test/java/feign/UtilTest.java +++ b/api/src/test/java/feign/UtilTest.java @@ -318,27 +318,6 @@ void nullMap() { assertThat(actualMap).isEmpty(); } - @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/jaxb-jakarta/src/main/java/feign/jaxb/JAXBEncoder.java b/jaxb-jakarta/src/main/java/feign/jaxb/JAXBEncoder.java index 472bc8638..09644b061 100644 --- a/jaxb-jakarta/src/main/java/feign/jaxb/JAXBEncoder.java +++ b/jaxb-jakarta/src/main/java/feign/jaxb/JAXBEncoder.java @@ -15,15 +15,15 @@ */ package feign.jaxb; +import java.io.StringWriter; +import java.lang.reflect.Type; + import feign.Request; import feign.RequestTemplate; -import feign.Util; import feign.codec.EncodeException; import feign.codec.Encoder; import jakarta.xml.bind.JAXBException; import jakarta.xml.bind.Marshaller; -import java.io.StringWriter; -import java.lang.reflect.Type; /** * Encodes requests using JAXB with Jakarta Api.
@@ -54,9 +54,6 @@ public JAXBEncoder(JAXBContextFactory jaxbContextFactory) { @Override 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); diff --git a/jaxb/src/main/java/feign/jaxb/JAXBEncoder.java b/jaxb/src/main/java/feign/jaxb/JAXBEncoder.java index 10fbdcffc..089e422c3 100644 --- a/jaxb/src/main/java/feign/jaxb/JAXBEncoder.java +++ b/jaxb/src/main/java/feign/jaxb/JAXBEncoder.java @@ -15,16 +15,17 @@ */ package feign.jaxb; -import feign.Request; -import feign.RequestTemplate; -import feign.Util; -import feign.codec.EncodeException; -import feign.codec.Encoder; import java.io.StringWriter; import java.lang.reflect.Type; + import javax.xml.bind.JAXBException; import javax.xml.bind.Marshaller; +import feign.Request; +import feign.RequestTemplate; +import feign.codec.EncodeException; +import feign.codec.Encoder; + /** * Encodes requests using JAXB.
* @@ -54,9 +55,6 @@ public JAXBEncoder(JAXBContextFactory jaxbContextFactory) { @Override 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); diff --git a/soap-jakarta/src/main/java/feign/soap/SOAPEncoder.java b/soap-jakarta/src/main/java/feign/soap/SOAPEncoder.java index 97ab5a666..a30e7e1b9 100644 --- a/soap-jakarta/src/main/java/feign/soap/SOAPEncoder.java +++ b/soap-jakarta/src/main/java/feign/soap/SOAPEncoder.java @@ -15,23 +15,12 @@ */ package feign.soap; -import feign.Request; -import feign.RequestTemplate; -import feign.Util; -import feign.codec.EncodeException; -import feign.codec.Encoder; -import feign.jaxb.JAXBContextFactory; -import jakarta.xml.bind.JAXBException; -import jakarta.xml.bind.Marshaller; -import jakarta.xml.soap.MessageFactory; -import jakarta.xml.soap.SOAPConstants; -import jakarta.xml.soap.SOAPException; -import jakarta.xml.soap.SOAPMessage; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.lang.reflect.Type; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; + import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.transform.OutputKeys; @@ -41,8 +30,21 @@ import javax.xml.transform.TransformerFactoryConfigurationError; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; + import org.w3c.dom.Document; +import feign.Request; +import feign.RequestTemplate; +import feign.codec.EncodeException; +import feign.codec.Encoder; +import feign.jaxb.JAXBContextFactory; +import jakarta.xml.bind.JAXBException; +import jakarta.xml.bind.Marshaller; +import jakarta.xml.soap.MessageFactory; +import jakarta.xml.soap.SOAPConstants; +import jakarta.xml.soap.SOAPException; +import jakarta.xml.soap.SOAPMessage; + /** * Encodes requests using SOAPMessage and JAXB for the body part.
* @@ -112,9 +114,6 @@ public SOAPEncoder(JAXBContextFactory jaxbContextFactory) { @Override 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); diff --git a/soap/src/main/java/feign/soap/SOAPEncoder.java b/soap/src/main/java/feign/soap/SOAPEncoder.java index 9f4017071..75cf5607d 100644 --- a/soap/src/main/java/feign/soap/SOAPEncoder.java +++ b/soap/src/main/java/feign/soap/SOAPEncoder.java @@ -15,17 +15,12 @@ */ package feign.soap; -import feign.Request; -import feign.RequestTemplate; -import feign.Util; -import feign.codec.EncodeException; -import feign.codec.Encoder; -import feign.jaxb.JAXBContextFactory; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.lang.reflect.Type; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; + import javax.xml.bind.JAXBException; import javax.xml.bind.Marshaller; import javax.xml.parsers.DocumentBuilderFactory; @@ -41,8 +36,15 @@ import javax.xml.transform.TransformerFactoryConfigurationError; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; + import org.w3c.dom.Document; +import feign.Request; +import feign.RequestTemplate; +import feign.codec.EncodeException; +import feign.codec.Encoder; +import feign.jaxb.JAXBContextFactory; + /** * Encodes requests using SOAPMessage and JAXB for the body part.
* @@ -112,9 +114,6 @@ public SOAPEncoder(JAXBContextFactory jaxbContextFactory) { @Override 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); From 0a767adc6c8c0c647ca270e8a6af1e4ea9745131 Mon Sep 17 00:00:00 2001 From: kevin Date: Mon, 20 Jul 2026 15:31:10 -0700 Subject: [PATCH 32/63] Remove migration note about json and xml content-type headers --- MIGRATION-v14.md | 47 ++++------------------------------------------- 1 file changed, 4 insertions(+), 43 deletions(-) diff --git a/MIGRATION-v14.md b/MIGRATION-v14.md index 013a3dcdc..6725741a7 100644 --- a/MIGRATION-v14.md +++ b/MIGRATION-v14.md @@ -389,46 +389,7 @@ 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 +### 15. `Encoder` moved from `core` to `api` module `feign.codec.Encoder` has been relocated from the `feign-core` module to the new `feign-api` module. The package name (`feign.codec`) is unchanged. If you have a direct dependency on @@ -436,7 +397,7 @@ module. The package name (`feign.codec`) is unchanged. If you have a direct depe --- -### 17. `Encoder.Default` removed +### 16. `Encoder.Default` removed The deprecated inner class `Encoder.Default` (which extended `DefaultEncoder`) has been removed. @@ -454,7 +415,7 @@ new feign.core.codec.DefaultEncoder() --- -### 18. Composing multiple encoders with `Encoder.of()` (https://github.com/OpenFeign/feign/pull/3476) +### 17. 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`. @@ -492,7 +453,7 @@ each encoder's `encode()` and uses the first one that returns `true`. --- -### 19. Multi-encoder support — `DelegatingEncoder` (https://github.com/OpenFeign/feign/pull/3476) +### 18. 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 From fb503f7c4ad5b722e2f0721263247358a971a7fe Mon Sep 17 00:00:00 2001 From: kevin Date: Mon, 20 Jul 2026 15:35:43 -0700 Subject: [PATCH 33/63] Updating PR hyperlink --- MIGRATION-v14.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/MIGRATION-v14.md b/MIGRATION-v14.md index 6725741a7..8f0133a1e 100644 --- a/MIGRATION-v14.md +++ b/MIGRATION-v14.md @@ -355,7 +355,7 @@ VertxFeign.builder() --- -### 14. `Encoder.encode()` now returns `boolean` (https://github.com/OpenFeign/feign/pull/3476) +### 14. `Encoder.encode()` now returns `boolean` (https://github.com/OpenFeign/feign/pull/3485) `Encoder.encode()` now returns `boolean` instead of `void`. Return `true` when the encoder handles the object, `false` otherwise. This replaces the separate `canEncode()` method. @@ -415,7 +415,7 @@ new feign.core.codec.DefaultEncoder() --- -### 17. Composing multiple encoders with `Encoder.of()` (https://github.com/OpenFeign/feign/pull/3476) +### 17. Composing multiple encoders with `Encoder.of()` (https://github.com/OpenFeign/feign/pull/3485) Use `Encoder.of(...)` to compose multiple encoders into a single `DelegatingEncoder`, which delegates to the first encoder whose `encode()` returns `true`. @@ -453,7 +453,7 @@ each encoder's `encode()` and uses the first one that returns `true`. --- -### 18. Multi-encoder support — `DelegatingEncoder` (https://github.com/OpenFeign/feign/pull/3476) +### 18. Multi-encoder support — `DelegatingEncoder` (https://github.com/OpenFeign/feign/pull/3485) You can now compose multiple encoders via `Encoder.of()`, and Feign will pick the right one at request time based on the return value of `encode()`. This is especially useful for APIs that mix From 4ed95e6fafda440743f207ab9b0a391615a1b940 Mon Sep 17 00:00:00 2001 From: kevin Date: Tue, 21 Jul 2026 18:03:03 -0700 Subject: [PATCH 34/63] Put main.java.version back to 11 (sorry - shouldn't have changed it) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 9463c75c2..505f0bc6b 100644 --- a/pom.xml +++ b/pom.xml @@ -158,7 +158,7 @@ -Duser.language=en - 25 + 11 25 From db95df39693f5f2478feef4fe4981cbc038f5ca8 Mon Sep 17 00:00:00 2001 From: kevin Date: Tue, 21 Jul 2026 18:16:04 -0700 Subject: [PATCH 35/63] Clean up unused imports --- api/src/test/java/feign/UtilTest.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/api/src/test/java/feign/UtilTest.java b/api/src/test/java/feign/UtilTest.java index bcdcd1fe4..f80018d7d 100644 --- a/api/src/test/java/feign/UtilTest.java +++ b/api/src/test/java/feign/UtilTest.java @@ -21,7 +21,6 @@ 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 java.io.Reader; import java.lang.reflect.Type; @@ -36,8 +35,6 @@ import java.util.Set; import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.CsvSource; import feign.codec.Decoder; From 6e912116e14c6763af882b72849f112d72e0a907 Mon Sep 17 00:00:00 2001 From: kevin Date: Mon, 27 Jul 2026 12:18:13 -0700 Subject: [PATCH 36/63] trying to clean up code formatting for git-code-format-maven-plugin --- api/src/test/java/feign/UtilTest.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/api/src/test/java/feign/UtilTest.java b/api/src/test/java/feign/UtilTest.java index f80018d7d..d0a232c19 100644 --- a/api/src/test/java/feign/UtilTest.java +++ b/api/src/test/java/feign/UtilTest.java @@ -22,6 +22,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import feign.codec.Decoder; import java.io.Reader; import java.lang.reflect.Type; import java.util.Arrays; @@ -33,11 +34,8 @@ import java.util.Map; import java.util.Optional; import java.util.Set; - import org.junit.jupiter.api.Test; -import feign.codec.Decoder; - class UtilTest { @Test From 5b11c91dcae94c29fa5fd6c3d93caf21ab3f6aaa Mon Sep 17 00:00:00 2001 From: kevin Date: Mon, 27 Jul 2026 14:58:23 -0700 Subject: [PATCH 37/63] Rename DelegateEncoder to MultiEncoder --- .../src/main/java/feign/core/codec/MultiEncoder.java | 0 .../src/test/java/feign/core/codec/MultiEncoderTest.java | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename api/src/main/java/feign/codec/DelegatingEncoder.java => core/src/main/java/feign/core/codec/MultiEncoder.java (100%) rename api/src/test/java/feign/codec/DelegatingEncoderTest.java => core/src/test/java/feign/core/codec/MultiEncoderTest.java (100%) diff --git a/api/src/main/java/feign/codec/DelegatingEncoder.java b/core/src/main/java/feign/core/codec/MultiEncoder.java similarity index 100% rename from api/src/main/java/feign/codec/DelegatingEncoder.java rename to core/src/main/java/feign/core/codec/MultiEncoder.java diff --git a/api/src/test/java/feign/codec/DelegatingEncoderTest.java b/core/src/test/java/feign/core/codec/MultiEncoderTest.java similarity index 100% rename from api/src/test/java/feign/codec/DelegatingEncoderTest.java rename to core/src/test/java/feign/core/codec/MultiEncoderTest.java From 36761fc060eac37bff4d1ace703cbe45482fed84 Mon Sep 17 00:00:00 2001 From: kevin Date: Mon, 27 Jul 2026 15:00:07 -0700 Subject: [PATCH 38/63] MultiEncoder can't be referenced from interfaces in api module --- api/src/main/java/feign/codec/Encoder.java | 29 ++-------------------- 1 file changed, 2 insertions(+), 27 deletions(-) diff --git a/api/src/main/java/feign/codec/Encoder.java b/api/src/main/java/feign/codec/Encoder.java index f980a4566..726b7f1e6 100644 --- a/api/src/main/java/feign/codec/Encoder.java +++ b/api/src/main/java/feign/codec/Encoder.java @@ -15,11 +15,10 @@ */ package feign.codec; +import java.lang.reflect.Type; + 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 @@ -72,30 +71,6 @@ 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. * From ffbf4c3d311947f940d949b64d9fec6f60c631f3 Mon Sep 17 00:00:00 2001 From: kevin Date: Mon, 27 Jul 2026 15:00:44 -0700 Subject: [PATCH 39/63] MultiEncoder cleanup --- .../java/feign/core/codec/MultiEncoder.java | 45 ++++++++++++++----- .../feign/core/codec/MultiEncoderTest.java | 30 ++++++++++--- 2 files changed, 59 insertions(+), 16 deletions(-) diff --git a/core/src/main/java/feign/core/codec/MultiEncoder.java b/core/src/main/java/feign/core/codec/MultiEncoder.java index 9e0142bac..f7ae2ca49 100644 --- a/core/src/main/java/feign/core/codec/MultiEncoder.java +++ b/core/src/main/java/feign/core/codec/MultiEncoder.java @@ -13,10 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package feign.codec; +package feign.core.codec; import feign.RequestTemplate; +import feign.codec.EncodeException; +import feign.codec.Encoder; + import java.lang.reflect.Type; +import java.util.Arrays; import java.util.List; import java.util.Objects; @@ -26,16 +30,40 @@ * * @since 14 */ -class DelegatingEncoder implements Encoder { +class MultiEncoder implements Encoder { private final List delegates; /** - * Creates a new {@link DelegatingEncoder} with the given list of delegates. + * 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 MultiEncoder(encoders); + } + + /** + * Creates a new {@link MultiEncoder} 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) { + MultiEncoder(List delegates) { this.delegates = Objects.requireNonNull(delegates, "delegates cannot be null"); } @@ -55,13 +83,8 @@ public boolean encode(Object object, Type bodyType, RequestTemplate template) .map(encoder -> encoder.encode(object, bodyType, template)) .filter(Boolean::booleanValue) .findFirst() - .orElseThrow( - () -> - new EncodeException( - "No suitable encoder found for object encoding: " - + object - + ", encoders: " - + delegates)); + .orElse(false) + ; } /** diff --git a/core/src/test/java/feign/core/codec/MultiEncoderTest.java b/core/src/test/java/feign/core/codec/MultiEncoderTest.java index 99c3ca555..2a7fae16a 100644 --- a/core/src/test/java/feign/core/codec/MultiEncoderTest.java +++ b/core/src/test/java/feign/core/codec/MultiEncoderTest.java @@ -13,29 +13,34 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package feign.codec; +package feign.core.codec; +import static org.assertj.core.api.Assertions.assertThat; 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; +import feign.RequestTemplate; +import feign.codec.EncodeException; +import feign.codec.Encoder; + @ExtendWith(MockitoExtension.class) -class DelegatingEncoderTest { +class MultiEncoderTest { @Mock private Encoder delegate; private Encoder encoder; @BeforeEach void setUp() { - encoder = new DelegatingEncoder(List.of(delegate)); + encoder = MultiEncoder.of(List.of(delegate)); } @Test @@ -50,13 +55,28 @@ void shouldEncode() { verify(delegate).encode(object, bodyType, requestTemplate); } + + @Test + void shouldNotEncode() { + var object = "Hello, World!"; + var bodyType = String.class; + var requestTemplate = mock(RequestTemplate.class); + + when(delegate.encode(object, bodyType, requestTemplate)).thenReturn(false); + + assertThat(encoder.encode(object, bodyType, requestTemplate)).isFalse(); + + } + @Test - void shouldThrowEncodeExceptionWhenNoSuitableEncoderFound() { + void shouldThrowEncodeExceptionWhenDelegateThrows() { var object = "Hello, World!"; var bodyType = String.class; var requestTemplate = mock(RequestTemplate.class); + when(delegate.encode(object, bodyType, requestTemplate)).thenThrow(EncodeException.class); + assertThrows(EncodeException.class, () -> encoder.encode(object, bodyType, requestTemplate)); } } From d00312cc2a8747965909a4cc4b2f24c708f9cbdb Mon Sep 17 00:00:00 2001 From: kevin Date: Mon, 27 Jul 2026 15:09:29 -0700 Subject: [PATCH 40/63] Add PredicateEncoder and supporting classes --- .../java/feign/codec/EncoderPredicate.java | 35 ++++++++++ core/pom.xml | 10 ++- .../feign/core/codec/PredicateEncoder.java | 23 ++++++ .../core/codec/PredicateEncoderTest.java | 70 +++++++++++++++++++ 4 files changed, 137 insertions(+), 1 deletion(-) create mode 100644 api/src/main/java/feign/codec/EncoderPredicate.java create mode 100644 core/src/main/java/feign/core/codec/PredicateEncoder.java create mode 100644 core/src/test/java/feign/core/codec/PredicateEncoderTest.java diff --git a/api/src/main/java/feign/codec/EncoderPredicate.java b/api/src/main/java/feign/codec/EncoderPredicate.java new file mode 100644 index 000000000..35fc7dd3e --- /dev/null +++ b/api/src/main/java/feign/codec/EncoderPredicate.java @@ -0,0 +1,35 @@ +/* + * 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 java.lang.reflect.Type; + +import feign.RequestTemplate; + +/** A predicate that determines whether a given object can be encoded by an encoder. */ +@FunctionalInterface +public interface EncoderPredicate { + + /** + * Tests whether the given object can be encoded by an encoder. + * + * @param object the object to be encoded + * @param bodyType the type of the object to be encoded + * @param template the request template that will be used to encode the object + * @return {@code true} if the object can be encoded, {@code false} otherwise + */ + boolean test(Object object, Type bodyType, RequestTemplate template); +} diff --git a/core/pom.xml b/core/pom.xml index ce60b8e07..1595a695a 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -85,10 +85,18 @@ org.mockito - mockito-core + mockito-junit-jupiter ${mockito.version} test + + + + org.projectlombok + lombok + ${lombok.version} + provided + diff --git a/core/src/main/java/feign/core/codec/PredicateEncoder.java b/core/src/main/java/feign/core/codec/PredicateEncoder.java new file mode 100644 index 000000000..9faf6a507 --- /dev/null +++ b/core/src/main/java/feign/core/codec/PredicateEncoder.java @@ -0,0 +1,23 @@ +package feign.core.codec; + +import java.lang.reflect.Type; + +import feign.RequestTemplate; +import feign.codec.EncodeException; +import feign.codec.Encoder; +import feign.codec.EncoderPredicate; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class PredicateEncoder implements Encoder { + private final Encoder delegate; + private final EncoderPredicate predicate; + + @Override + public boolean encode(Object object, Type bodyType, RequestTemplate template) throws EncodeException { + if(predicate.test(object, bodyType, template)) + return delegate.encode(object, bodyType, template); + + return false; + } +} diff --git a/core/src/test/java/feign/core/codec/PredicateEncoderTest.java b/core/src/test/java/feign/core/codec/PredicateEncoderTest.java new file mode 100644 index 000000000..d2ce665d5 --- /dev/null +++ b/core/src/test/java/feign/core/codec/PredicateEncoderTest.java @@ -0,0 +1,70 @@ +/* + * 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.core.codec; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +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; + +import feign.RequestTemplate; +import feign.codec.Encoder; +import feign.codec.EncoderPredicate; + +@ExtendWith(MockitoExtension.class) +class PredicateEncoderTest { + + String object = "Hello, World!"; + Class bodyType = String.class; + RequestTemplate requestTemplate = mock(RequestTemplate.class); + + + @Mock private Encoder delegate; + private final EncoderPredicate truePredicate = (o, t, tt) -> true; + private final EncoderPredicate falsePredicate = (o, t, tt) -> false; + + @Test + void shouldEncode() { + + when(delegate.encode(object, bodyType, requestTemplate)).thenReturn(true); + + PredicateEncoder predicateEncoder = new PredicateEncoder(delegate, truePredicate); + + assertThat(predicateEncoder.encode(object, bodyType, requestTemplate)).isTrue(); + + verify(delegate).encode(object, bodyType, requestTemplate); + } + + @Test + void shouldNotEncode() { + PredicateEncoder predicateEncoder = new PredicateEncoder(delegate, falsePredicate); + + assertThat(predicateEncoder.encode(object, bodyType, requestTemplate)).isFalse(); + + verify(delegate, never()).encode(object, bodyType, requestTemplate); + + } + + + +} From ea970f629ace7000bd043b9868f0cf065ec4ea97 Mon Sep 17 00:00:00 2001 From: kevin Date: Mon, 27 Jul 2026 15:12:09 -0700 Subject: [PATCH 41/63] Remove unnused import --- core/src/test/java/feign/core/codec/PredicateEncoderTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/core/src/test/java/feign/core/codec/PredicateEncoderTest.java b/core/src/test/java/feign/core/codec/PredicateEncoderTest.java index d2ce665d5..d34eb329a 100644 --- a/core/src/test/java/feign/core/codec/PredicateEncoderTest.java +++ b/core/src/test/java/feign/core/codec/PredicateEncoderTest.java @@ -21,7 +21,6 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; From 7af1863187239da854a1a1232008bc958eef11ed Mon Sep 17 00:00:00 2001 From: kevin Date: Mon, 27 Jul 2026 15:13:37 -0700 Subject: [PATCH 42/63] Formatting --- .../java/feign/core/codec/MultiEncoder.java | 6 ++-- .../feign/core/codec/PredicateEncoder.java | 24 +++++++------- .../feign/core/codec/MultiEncoderTest.java | 14 +++----- .../core/codec/PredicateEncoderTest.java | 32 ++++++++----------- 4 files changed, 32 insertions(+), 44 deletions(-) diff --git a/core/src/main/java/feign/core/codec/MultiEncoder.java b/core/src/main/java/feign/core/codec/MultiEncoder.java index f7ae2ca49..053079e44 100644 --- a/core/src/main/java/feign/core/codec/MultiEncoder.java +++ b/core/src/main/java/feign/core/codec/MultiEncoder.java @@ -18,7 +18,6 @@ import feign.RequestTemplate; import feign.codec.EncodeException; import feign.codec.Encoder; - import java.lang.reflect.Type; import java.util.Arrays; import java.util.List; @@ -56,7 +55,7 @@ static Encoder of(Encoder... encoders) { static Encoder of(List encoders) { return new MultiEncoder(encoders); } - + /** * Creates a new {@link MultiEncoder} with the given list of delegates. * @@ -83,8 +82,7 @@ public boolean encode(Object object, Type bodyType, RequestTemplate template) .map(encoder -> encoder.encode(object, bodyType, template)) .filter(Boolean::booleanValue) .findFirst() - .orElse(false) - ; + .orElse(false); } /** diff --git a/core/src/main/java/feign/core/codec/PredicateEncoder.java b/core/src/main/java/feign/core/codec/PredicateEncoder.java index 9faf6a507..4c24266c5 100644 --- a/core/src/main/java/feign/core/codec/PredicateEncoder.java +++ b/core/src/main/java/feign/core/codec/PredicateEncoder.java @@ -1,23 +1,23 @@ package feign.core.codec; -import java.lang.reflect.Type; - import feign.RequestTemplate; import feign.codec.EncodeException; import feign.codec.Encoder; import feign.codec.EncoderPredicate; +import java.lang.reflect.Type; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor public class PredicateEncoder implements Encoder { - private final Encoder delegate; - private final EncoderPredicate predicate; - - @Override - public boolean encode(Object object, Type bodyType, RequestTemplate template) throws EncodeException { - if(predicate.test(object, bodyType, template)) - return delegate.encode(object, bodyType, template); - - return false; - } + private final Encoder delegate; + private final EncoderPredicate predicate; + + @Override + public boolean encode(Object object, Type bodyType, RequestTemplate template) + throws EncodeException { + if (predicate.test(object, bodyType, template)) + return delegate.encode(object, bodyType, template); + + return false; + } } diff --git a/core/src/test/java/feign/core/codec/MultiEncoderTest.java b/core/src/test/java/feign/core/codec/MultiEncoderTest.java index 2a7fae16a..131c10b7a 100644 --- a/core/src/test/java/feign/core/codec/MultiEncoderTest.java +++ b/core/src/test/java/feign/core/codec/MultiEncoderTest.java @@ -21,18 +21,16 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import feign.RequestTemplate; +import feign.codec.EncodeException; +import feign.codec.Encoder; 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; -import feign.RequestTemplate; -import feign.codec.EncodeException; -import feign.codec.Encoder; - @ExtendWith(MockitoExtension.class) class MultiEncoderTest { @Mock private Encoder delegate; @@ -55,7 +53,7 @@ void shouldEncode() { verify(delegate).encode(object, bodyType, requestTemplate); } - + @Test void shouldNotEncode() { var object = "Hello, World!"; @@ -65,10 +63,8 @@ void shouldNotEncode() { when(delegate.encode(object, bodyType, requestTemplate)).thenReturn(false); assertThat(encoder.encode(object, bodyType, requestTemplate)).isFalse(); - } - @Test void shouldThrowEncodeExceptionWhenDelegateThrows() { var object = "Hello, World!"; @@ -76,7 +72,7 @@ void shouldThrowEncodeExceptionWhenDelegateThrows() { var requestTemplate = mock(RequestTemplate.class); when(delegate.encode(object, bodyType, requestTemplate)).thenThrow(EncodeException.class); - + assertThrows(EncodeException.class, () -> encoder.encode(object, bodyType, requestTemplate)); } } diff --git a/core/src/test/java/feign/core/codec/PredicateEncoderTest.java b/core/src/test/java/feign/core/codec/PredicateEncoderTest.java index d34eb329a..57a0d2780 100644 --- a/core/src/test/java/feign/core/codec/PredicateEncoderTest.java +++ b/core/src/test/java/feign/core/codec/PredicateEncoderTest.java @@ -21,49 +21,43 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import feign.RequestTemplate; +import feign.codec.Encoder; +import feign.codec.EncoderPredicate; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import feign.RequestTemplate; -import feign.codec.Encoder; -import feign.codec.EncoderPredicate; - @ExtendWith(MockitoExtension.class) class PredicateEncoderTest { - + String object = "Hello, World!"; Class bodyType = String.class; RequestTemplate requestTemplate = mock(RequestTemplate.class); - - + @Mock private Encoder delegate; private final EncoderPredicate truePredicate = (o, t, tt) -> true; private final EncoderPredicate falsePredicate = (o, t, tt) -> false; - + @Test void shouldEncode() { - when(delegate.encode(object, bodyType, requestTemplate)).thenReturn(true); - + when(delegate.encode(object, bodyType, requestTemplate)).thenReturn(true); + PredicateEncoder predicateEncoder = new PredicateEncoder(delegate, truePredicate); - + assertThat(predicateEncoder.encode(object, bodyType, requestTemplate)).isTrue(); verify(delegate).encode(object, bodyType, requestTemplate); } - + @Test void shouldNotEncode() { - PredicateEncoder predicateEncoder = new PredicateEncoder(delegate, falsePredicate); + PredicateEncoder predicateEncoder = new PredicateEncoder(delegate, falsePredicate); - assertThat(predicateEncoder.encode(object, bodyType, requestTemplate)).isFalse(); - - verify(delegate, never()).encode(object, bodyType, requestTemplate); + assertThat(predicateEncoder.encode(object, bodyType, requestTemplate)).isFalse(); + verify(delegate, never()).encode(object, bodyType, requestTemplate); } - - - } From d00ad864c6754832002e786563c669f6e34936da Mon Sep 17 00:00:00 2001 From: kevin Date: Mon, 27 Jul 2026 15:14:12 -0700 Subject: [PATCH 43/63] Formatting --- api/src/main/java/feign/codec/Encoder.java | 3 +-- .../java/feign/codec/EncoderPredicate.java | 3 +-- .../feign/fastjson2/Fastjson2Encoder.java | 4 +-- .../src/main/java/feign/gson/GsonEncoder.java | 6 ++--- .../jackson/jaxb/JacksonJaxbJsonEncoder.java | 8 +++--- .../feign/jackson/jr/JacksonJrEncoder.java | 6 ++--- .../java/feign/jackson/JacksonEncoder.java | 6 ++--- .../java/feign/jackson3/Jackson3Encoder.java | 6 ++--- .../src/main/java/feign/jaxb/JAXBEncoder.java | 5 ++-- .../src/main/java/feign/jaxb/JAXBEncoder.java | 10 +++----- .../main/java/feign/moshi/MoshiEncoder.java | 4 +-- .../src/main/java/feign/soap/SOAPEncoder.java | 25 ++++++++----------- .../src/main/java/feign/soap/SOAPEncoder.java | 13 ++++------ 13 files changed, 37 insertions(+), 62 deletions(-) diff --git a/api/src/main/java/feign/codec/Encoder.java b/api/src/main/java/feign/codec/Encoder.java index 726b7f1e6..aaf9d1dd3 100644 --- a/api/src/main/java/feign/codec/Encoder.java +++ b/api/src/main/java/feign/codec/Encoder.java @@ -15,10 +15,9 @@ */ package feign.codec; -import java.lang.reflect.Type; - import feign.RequestTemplate; import feign.Util; +import java.lang.reflect.Type; /** * Encodes an object into an HTTP request body. Like {@code javax.websocket.Encoder}. {@code diff --git a/api/src/main/java/feign/codec/EncoderPredicate.java b/api/src/main/java/feign/codec/EncoderPredicate.java index 35fc7dd3e..1d96c5bbf 100644 --- a/api/src/main/java/feign/codec/EncoderPredicate.java +++ b/api/src/main/java/feign/codec/EncoderPredicate.java @@ -15,9 +15,8 @@ */ package feign.codec; -import java.lang.reflect.Type; - import feign.RequestTemplate; +import java.lang.reflect.Type; /** A predicate that determines whether a given object can be encoded by an encoder. */ @FunctionalInterface diff --git a/fastjson2/src/main/java/feign/fastjson2/Fastjson2Encoder.java b/fastjson2/src/main/java/feign/fastjson2/Fastjson2Encoder.java index fc287b55f..fb2f73ade 100644 --- a/fastjson2/src/main/java/feign/fastjson2/Fastjson2Encoder.java +++ b/fastjson2/src/main/java/feign/fastjson2/Fastjson2Encoder.java @@ -15,16 +15,14 @@ */ package feign.fastjson2; -import java.lang.reflect.Type; - import com.alibaba.fastjson2.JSON; import com.alibaba.fastjson2.JSONWriter; - import feign.Request; import feign.RequestTemplate; import feign.codec.EncodeException; import feign.codec.Encoder; import feign.codec.JsonEncoder; +import java.lang.reflect.Type; /** * @author changjin wei(魏昌进) diff --git a/gson/src/main/java/feign/gson/GsonEncoder.java b/gson/src/main/java/feign/gson/GsonEncoder.java index 29fa89ae3..221a0cf88 100644 --- a/gson/src/main/java/feign/gson/GsonEncoder.java +++ b/gson/src/main/java/feign/gson/GsonEncoder.java @@ -15,16 +15,14 @@ */ package feign.gson; -import java.lang.reflect.Type; -import java.util.Collections; - import com.google.gson.Gson; import com.google.gson.TypeAdapter; - import feign.Request; import feign.RequestTemplate; import feign.codec.Encoder; import feign.codec.JsonEncoder; +import java.lang.reflect.Type; +import java.util.Collections; public class GsonEncoder implements Encoder, JsonEncoder { 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 3659a1fb2..eec263191 100644 --- a/jackson-jaxb/src/main/java/feign/jackson/jaxb/JacksonJaxbJsonEncoder.java +++ b/jackson-jaxb/src/main/java/feign/jackson/jaxb/JacksonJaxbJsonEncoder.java @@ -18,17 +18,15 @@ import static com.fasterxml.jackson.jaxrs.json.JacksonJaxbJsonProvider.DEFAULT_ANNOTATIONS; import static javax.ws.rs.core.MediaType.APPLICATION_JSON_TYPE; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.lang.reflect.Type; - import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.jaxrs.json.JacksonJaxbJsonProvider; - import feign.Request; import feign.RequestTemplate; import feign.codec.EncodeException; import feign.codec.Encoder; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.lang.reflect.Type; public final class JacksonJaxbJsonEncoder implements Encoder { private final JacksonJaxbJsonProvider jacksonJaxbJsonProvider; 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 d19f189d5..0d23e85b6 100644 --- a/jackson-jr/src/main/java/feign/jackson/jr/JacksonJrEncoder.java +++ b/jackson-jr/src/main/java/feign/jackson/jr/JacksonJrEncoder.java @@ -15,16 +15,14 @@ */ package feign.jackson.jr; -import java.io.IOException; -import java.lang.reflect.Type; - import com.fasterxml.jackson.jr.ob.JSON; import com.fasterxml.jackson.jr.ob.JacksonJrExtension; - import feign.Request; import feign.RequestTemplate; import feign.codec.EncodeException; import feign.codec.Encoder; +import java.io.IOException; +import java.lang.reflect.Type; /** A {@link Encoder} that uses Jackson Jr to convert objects to String or byte representation. */ public class JacksonJrEncoder extends JacksonJrMapper implements Encoder { diff --git a/jackson/src/main/java/feign/jackson/JacksonEncoder.java b/jackson/src/main/java/feign/jackson/JacksonEncoder.java index 886fa4626..75b04d046 100644 --- a/jackson/src/main/java/feign/jackson/JacksonEncoder.java +++ b/jackson/src/main/java/feign/jackson/JacksonEncoder.java @@ -15,21 +15,19 @@ */ package feign.jackson; -import java.lang.reflect.Type; -import java.util.Collections; - import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.Module; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; - import feign.Request; import feign.RequestTemplate; import feign.codec.EncodeException; import feign.codec.Encoder; import feign.codec.JsonEncoder; +import java.lang.reflect.Type; +import java.util.Collections; public class JacksonEncoder implements Encoder, JsonEncoder { diff --git a/jackson3/src/main/java/feign/jackson3/Jackson3Encoder.java b/jackson3/src/main/java/feign/jackson3/Jackson3Encoder.java index 19927f08b..fa1ab9bce 100644 --- a/jackson3/src/main/java/feign/jackson3/Jackson3Encoder.java +++ b/jackson3/src/main/java/feign/jackson3/Jackson3Encoder.java @@ -15,16 +15,14 @@ */ package feign.jackson3; -import java.lang.reflect.Type; -import java.util.Collections; - import com.fasterxml.jackson.annotation.JsonInclude; - import feign.Request; import feign.RequestTemplate; import feign.codec.EncodeException; import feign.codec.Encoder; import feign.codec.JsonEncoder; +import java.lang.reflect.Type; +import java.util.Collections; import tools.jackson.core.JacksonException; import tools.jackson.databind.JacksonModule; import tools.jackson.databind.JavaType; diff --git a/jaxb-jakarta/src/main/java/feign/jaxb/JAXBEncoder.java b/jaxb-jakarta/src/main/java/feign/jaxb/JAXBEncoder.java index 09644b061..7bdcf3bdf 100644 --- a/jaxb-jakarta/src/main/java/feign/jaxb/JAXBEncoder.java +++ b/jaxb-jakarta/src/main/java/feign/jaxb/JAXBEncoder.java @@ -15,15 +15,14 @@ */ package feign.jaxb; -import java.io.StringWriter; -import java.lang.reflect.Type; - import feign.Request; import feign.RequestTemplate; import feign.codec.EncodeException; import feign.codec.Encoder; import jakarta.xml.bind.JAXBException; import jakarta.xml.bind.Marshaller; +import java.io.StringWriter; +import java.lang.reflect.Type; /** * Encodes requests using JAXB with Jakarta Api.
diff --git a/jaxb/src/main/java/feign/jaxb/JAXBEncoder.java b/jaxb/src/main/java/feign/jaxb/JAXBEncoder.java index 089e422c3..613f23214 100644 --- a/jaxb/src/main/java/feign/jaxb/JAXBEncoder.java +++ b/jaxb/src/main/java/feign/jaxb/JAXBEncoder.java @@ -15,16 +15,14 @@ */ package feign.jaxb; -import java.io.StringWriter; -import java.lang.reflect.Type; - -import javax.xml.bind.JAXBException; -import javax.xml.bind.Marshaller; - import feign.Request; import feign.RequestTemplate; import feign.codec.EncodeException; import feign.codec.Encoder; +import java.io.StringWriter; +import java.lang.reflect.Type; +import javax.xml.bind.JAXBException; +import javax.xml.bind.Marshaller; /** * Encodes requests using JAXB.
diff --git a/moshi/src/main/java/feign/moshi/MoshiEncoder.java b/moshi/src/main/java/feign/moshi/MoshiEncoder.java index 4d6ed5855..c444cc093 100644 --- a/moshi/src/main/java/feign/moshi/MoshiEncoder.java +++ b/moshi/src/main/java/feign/moshi/MoshiEncoder.java @@ -15,15 +15,13 @@ */ package feign.moshi; -import java.lang.reflect.Type; - import com.squareup.moshi.JsonAdapter; import com.squareup.moshi.Moshi; - import feign.Request; import feign.RequestTemplate; import feign.codec.Encoder; import feign.codec.JsonEncoder; +import java.lang.reflect.Type; public class MoshiEncoder implements Encoder, JsonEncoder { diff --git a/soap-jakarta/src/main/java/feign/soap/SOAPEncoder.java b/soap-jakarta/src/main/java/feign/soap/SOAPEncoder.java index a30e7e1b9..bf5e09731 100644 --- a/soap-jakarta/src/main/java/feign/soap/SOAPEncoder.java +++ b/soap-jakarta/src/main/java/feign/soap/SOAPEncoder.java @@ -15,12 +15,22 @@ */ package feign.soap; +import feign.Request; +import feign.RequestTemplate; +import feign.codec.EncodeException; +import feign.codec.Encoder; +import feign.jaxb.JAXBContextFactory; +import jakarta.xml.bind.JAXBException; +import jakarta.xml.bind.Marshaller; +import jakarta.xml.soap.MessageFactory; +import jakarta.xml.soap.SOAPConstants; +import jakarta.xml.soap.SOAPException; +import jakarta.xml.soap.SOAPMessage; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.lang.reflect.Type; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; - import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.transform.OutputKeys; @@ -30,21 +40,8 @@ import javax.xml.transform.TransformerFactoryConfigurationError; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; - import org.w3c.dom.Document; -import feign.Request; -import feign.RequestTemplate; -import feign.codec.EncodeException; -import feign.codec.Encoder; -import feign.jaxb.JAXBContextFactory; -import jakarta.xml.bind.JAXBException; -import jakarta.xml.bind.Marshaller; -import jakarta.xml.soap.MessageFactory; -import jakarta.xml.soap.SOAPConstants; -import jakarta.xml.soap.SOAPException; -import jakarta.xml.soap.SOAPMessage; - /** * Encodes requests using SOAPMessage and JAXB for the body part.
* diff --git a/soap/src/main/java/feign/soap/SOAPEncoder.java b/soap/src/main/java/feign/soap/SOAPEncoder.java index 75cf5607d..55150c8ae 100644 --- a/soap/src/main/java/feign/soap/SOAPEncoder.java +++ b/soap/src/main/java/feign/soap/SOAPEncoder.java @@ -15,12 +15,16 @@ */ package feign.soap; +import feign.Request; +import feign.RequestTemplate; +import feign.codec.EncodeException; +import feign.codec.Encoder; +import feign.jaxb.JAXBContextFactory; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.lang.reflect.Type; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; - import javax.xml.bind.JAXBException; import javax.xml.bind.Marshaller; import javax.xml.parsers.DocumentBuilderFactory; @@ -36,15 +40,8 @@ import javax.xml.transform.TransformerFactoryConfigurationError; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; - import org.w3c.dom.Document; -import feign.Request; -import feign.RequestTemplate; -import feign.codec.EncodeException; -import feign.codec.Encoder; -import feign.jaxb.JAXBContextFactory; - /** * Encodes requests using SOAPMessage and JAXB for the body part.
* From 095acdb3d5efbca2c10fc733aefadd8f71a364b5 Mon Sep 17 00:00:00 2001 From: kevin Date: Mon, 27 Jul 2026 15:16:41 -0700 Subject: [PATCH 44/63] Add license --- .../java/feign/core/codec/PredicateEncoder.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/core/src/main/java/feign/core/codec/PredicateEncoder.java b/core/src/main/java/feign/core/codec/PredicateEncoder.java index 4c24266c5..5e8c531ff 100644 --- a/core/src/main/java/feign/core/codec/PredicateEncoder.java +++ b/core/src/main/java/feign/core/codec/PredicateEncoder.java @@ -1,3 +1,18 @@ +/* + * 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.core.codec; import feign.RequestTemplate; From b48427577121940085191d591bb009f6c3e8ecf4 Mon Sep 17 00:00:00 2001 From: kevin Date: Mon, 27 Jul 2026 15:24:04 -0700 Subject: [PATCH 45/63] Remove lombok from core --- core/pom.xml | 9 +-------- .../src/main/java/feign/core/codec/PredicateEncoder.java | 7 +++++-- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/core/pom.xml b/core/pom.xml index 1595a695a..f0d5d18ee 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -89,14 +89,7 @@ ${mockito.version} test - - - - org.projectlombok - lombok - ${lombok.version} - provided - + diff --git a/core/src/main/java/feign/core/codec/PredicateEncoder.java b/core/src/main/java/feign/core/codec/PredicateEncoder.java index 5e8c531ff..cfb472a6e 100644 --- a/core/src/main/java/feign/core/codec/PredicateEncoder.java +++ b/core/src/main/java/feign/core/codec/PredicateEncoder.java @@ -20,13 +20,16 @@ import feign.codec.Encoder; import feign.codec.EncoderPredicate; import java.lang.reflect.Type; -import lombok.RequiredArgsConstructor; -@RequiredArgsConstructor public class PredicateEncoder implements Encoder { private final Encoder delegate; private final EncoderPredicate predicate; + public PredicateEncoder(Encoder delegate, EncoderPredicate predicate) { + this.delegate = delegate; + this.predicate = predicate; + } + @Override public boolean encode(Object object, Type bodyType, RequestTemplate template) throws EncodeException { From 6710ffc4391f51ff852189a1fa57515beacae82d Mon Sep 17 00:00:00 2001 From: kevin Date: Mon, 27 Jul 2026 15:27:02 -0700 Subject: [PATCH 46/63] Fix formatting --- core/src/main/java/feign/core/codec/PredicateEncoder.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/feign/core/codec/PredicateEncoder.java b/core/src/main/java/feign/core/codec/PredicateEncoder.java index cfb472a6e..f07b4fee1 100644 --- a/core/src/main/java/feign/core/codec/PredicateEncoder.java +++ b/core/src/main/java/feign/core/codec/PredicateEncoder.java @@ -26,8 +26,8 @@ public class PredicateEncoder implements Encoder { private final EncoderPredicate predicate; public PredicateEncoder(Encoder delegate, EncoderPredicate predicate) { - this.delegate = delegate; - this.predicate = predicate; + this.delegate = delegate; + this.predicate = predicate; } @Override From 5922082e7d994306e32cebc4f7c96cb501a18868 Mon Sep 17 00:00:00 2001 From: kevin Date: Mon, 27 Jul 2026 15:49:10 -0700 Subject: [PATCH 47/63] Update changelog for changes in MultiEncoder, link to new pull request --- CHANGELOG.md | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ada9b2fd8..2187fbedf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,13 +4,10 @@ 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) + the encoder does not handle the type. The `MultiEncoder.of()` factory method composes multiple encoders + into a `MultiEncoder` that tries each delegate's `encode()` in order. Existing custom `Encoder` implementations + must update the `encode()` return type from `void` to `boolean` and throw EncodeException for true encoding failures. + (https://github.com/OpenFeign/feign/pull/3485) ### Version 13.12 From 5440c15265076d1a676a45c0518c5e4dd9d7b0de Mon Sep 17 00:00:00 2001 From: kevin Date: Tue, 28 Jul 2026 16:32:28 -0700 Subject: [PATCH 48/63] Remove library version changes --- pom.xml | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/pom.xml b/pom.xml index 505f0bc6b..528d41761 100644 --- a/pom.xml +++ b/pom.xml @@ -167,16 +167,16 @@ 5.4.0 33.6.0-jre - 2.1.1 + 2.1.0 2.14.0 1.15.2 2.0.18 - 20260719 + 20260522 4.1.0 - 6.1.2 - 2.22.1 - 3.2.1 + 6.1.0 + 2.22.0 + 3.2.0 3.27.7 5.23.0 2.0.61.android8 @@ -206,9 +206,9 @@ 3.2.0 1.2.8 4.0.0 - 6.44.0 - 3.42.0 - 3.40.0 + 6.42.0 + 3.40.0 + 3.38.0 0.26.1 1.0 @@ -221,9 +221,9 @@ 1.15.0 0.23.0 26.0 - 4.5.3 + 4.5.2 4.5.14 - 5.6.2 + 5.6.1 1.13.0 1.5.18 3.1.0 @@ -255,7 +255,7 @@ 2025.1.2 5.0.2 7.0.8 - 2.4.2.Final + 2.4.1.Final 1.18.0 @@ -1326,4 +1326,4 @@ - + \ No newline at end of file From c8301764ba7a207baee3a8f74737662f38f05a5b Mon Sep 17 00:00:00 2001 From: kevin Date: Tue, 28 Jul 2026 16:35:35 -0700 Subject: [PATCH 49/63] private constructor --- core/src/main/java/feign/core/codec/MultiEncoder.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/feign/core/codec/MultiEncoder.java b/core/src/main/java/feign/core/codec/MultiEncoder.java index 053079e44..1b99e55b2 100644 --- a/core/src/main/java/feign/core/codec/MultiEncoder.java +++ b/core/src/main/java/feign/core/codec/MultiEncoder.java @@ -62,7 +62,7 @@ static Encoder of(List encoders) { * @param delegates the list of delegates to use for encoding. Both list and its elements must not * be {@code null}. */ - MultiEncoder(List delegates) { + private MultiEncoder(List delegates) { this.delegates = Objects.requireNonNull(delegates, "delegates cannot be null"); } From b3e884dbe92e8f170a7cc332fff35ee21e7a291b Mon Sep 17 00:00:00 2001 From: kevin Date: Tue, 28 Jul 2026 16:37:29 -0700 Subject: [PATCH 50/63] Add javadoc --- core/src/main/java/feign/core/codec/PredicateEncoder.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/core/src/main/java/feign/core/codec/PredicateEncoder.java b/core/src/main/java/feign/core/codec/PredicateEncoder.java index f07b4fee1..01e14b9e5 100644 --- a/core/src/main/java/feign/core/codec/PredicateEncoder.java +++ b/core/src/main/java/feign/core/codec/PredicateEncoder.java @@ -21,7 +21,15 @@ import feign.codec.EncoderPredicate; import java.lang.reflect.Type; +/** + * An encoder that wraps another (delegate) encoder but blocks encoding if a predicate isn't met. + * For example, this is useful for restricting the usage of a given Encoder to a given content type header. + */ public class PredicateEncoder implements Encoder { + + /** + * The delegate that the encoding request will be passed to if the predicate is met. + */ private final Encoder delegate; private final EncoderPredicate predicate; From a6f05c47aff5d7a7334c688cab407d3528529107 Mon Sep 17 00:00:00 2001 From: kevin Date: Tue, 28 Jul 2026 16:48:18 -0700 Subject: [PATCH 51/63] Use Mockito more --- .../core/codec/PredicateEncoderTest.java | 33 +++++++++++-------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/core/src/test/java/feign/core/codec/PredicateEncoderTest.java b/core/src/test/java/feign/core/codec/PredicateEncoderTest.java index 57a0d2780..87945d18d 100644 --- a/core/src/test/java/feign/core/codec/PredicateEncoderTest.java +++ b/core/src/test/java/feign/core/codec/PredicateEncoderTest.java @@ -17,36 +17,37 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; -import feign.RequestTemplate; -import feign.codec.Encoder; -import feign.codec.EncoderPredicate; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import feign.RequestTemplate; +import feign.codec.Encoder; +import feign.codec.EncoderPredicate; + @ExtendWith(MockitoExtension.class) class PredicateEncoderTest { - String object = "Hello, World!"; - Class bodyType = String.class; - RequestTemplate requestTemplate = mock(RequestTemplate.class); - @Mock private Encoder delegate; - private final EncoderPredicate truePredicate = (o, t, tt) -> true; - private final EncoderPredicate falsePredicate = (o, t, tt) -> false; + @Mock private EncoderPredicate encoderPredicate; + @InjectMocks private PredicateEncoder predicateEncoder; @Test void shouldEncode() { + String object = "Hello, World!"; + Class bodyType = String.class; + RequestTemplate requestTemplate = mock(RequestTemplate.class); + + when(encoderPredicate.test(object, bodyType, requestTemplate)).thenReturn(true); when(delegate.encode(object, bodyType, requestTemplate)).thenReturn(true); - PredicateEncoder predicateEncoder = new PredicateEncoder(delegate, truePredicate); - assertThat(predicateEncoder.encode(object, bodyType, requestTemplate)).isTrue(); verify(delegate).encode(object, bodyType, requestTemplate); @@ -54,10 +55,14 @@ void shouldEncode() { @Test void shouldNotEncode() { - PredicateEncoder predicateEncoder = new PredicateEncoder(delegate, falsePredicate); + var object = "Hello, World!"; + var bodyType = String.class; + var requestTemplate = mock(RequestTemplate.class); + + when(encoderPredicate.test(object, bodyType, requestTemplate)).thenReturn(false); assertThat(predicateEncoder.encode(object, bodyType, requestTemplate)).isFalse(); - verify(delegate, never()).encode(object, bodyType, requestTemplate); + verifyNoInteractions(delegate); } } From da348f54f8f3e0849adb329818bf53ccc0769604 Mon Sep 17 00:00:00 2001 From: kevin Date: Tue, 28 Jul 2026 16:48:25 -0700 Subject: [PATCH 52/63] Make class public --- core/src/main/java/feign/core/codec/MultiEncoder.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/feign/core/codec/MultiEncoder.java b/core/src/main/java/feign/core/codec/MultiEncoder.java index 1b99e55b2..6a15426c6 100644 --- a/core/src/main/java/feign/core/codec/MultiEncoder.java +++ b/core/src/main/java/feign/core/codec/MultiEncoder.java @@ -29,7 +29,7 @@ * * @since 14 */ -class MultiEncoder implements Encoder { +public class MultiEncoder implements Encoder { private final List delegates; /** From 49b7d91aee623b5f440c82822b64d50ea3696462 Mon Sep 17 00:00:00 2001 From: kevin Date: Tue, 28 Jul 2026 17:00:33 -0700 Subject: [PATCH 53/63] Fix toString --- core/src/main/java/feign/core/codec/MultiEncoder.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/feign/core/codec/MultiEncoder.java b/core/src/main/java/feign/core/codec/MultiEncoder.java index 6a15426c6..d6c89228e 100644 --- a/core/src/main/java/feign/core/codec/MultiEncoder.java +++ b/core/src/main/java/feign/core/codec/MultiEncoder.java @@ -92,6 +92,6 @@ public boolean encode(Object object, Type bodyType, RequestTemplate template) */ @Override public String toString() { - return "DelegatingEncoder{" + "delegates=" + delegates + '}'; + return "MultiEncoder{" + "delegates=" + delegates + '}'; } } From ee828e727e9419a97bb2e8938d878acf444ffecb Mon Sep 17 00:00:00 2001 From: kevin Date: Tue, 28 Jul 2026 17:01:00 -0700 Subject: [PATCH 54/63] Swap constructor argument order (predicte first, then delegate) --- core/src/main/java/feign/core/codec/PredicateEncoder.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/feign/core/codec/PredicateEncoder.java b/core/src/main/java/feign/core/codec/PredicateEncoder.java index 01e14b9e5..b2a852561 100644 --- a/core/src/main/java/feign/core/codec/PredicateEncoder.java +++ b/core/src/main/java/feign/core/codec/PredicateEncoder.java @@ -33,7 +33,7 @@ public class PredicateEncoder implements Encoder { private final Encoder delegate; private final EncoderPredicate predicate; - public PredicateEncoder(Encoder delegate, EncoderPredicate predicate) { + public PredicateEncoder(EncoderPredicate predicate, Encoder delegate) { this.delegate = delegate; this.predicate = predicate; } From 9106c0b780fae0f1f44aa2b7f9ffcd12620dc287 Mon Sep 17 00:00:00 2001 From: kevin Date: Tue, 28 Jul 2026 17:09:04 -0700 Subject: [PATCH 55/63] Clean up migration document --- MIGRATION-v14.md | 59 ++++++++++++++++++++---------------------------- 1 file changed, 25 insertions(+), 34 deletions(-) diff --git a/MIGRATION-v14.md b/MIGRATION-v14.md index 8f0133a1e..d854b498b 100644 --- a/MIGRATION-v14.md +++ b/MIGRATION-v14.md @@ -384,7 +384,7 @@ public class MyEncoder implements Encoder { ``` 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 +from `encode()`. If your encoder returns `false`, the `MultiEncoder` (see section 19) will try the next encoder. If no encoder returns `true`, an `EncodeException` is thrown. --- @@ -415,9 +415,9 @@ new feign.core.codec.DefaultEncoder() --- -### 17. Composing multiple encoders with `Encoder.of()` (https://github.com/OpenFeign/feign/pull/3485) +### 17. Composing multiple encoders with `MultiEncoder.of()` (https://github.com/OpenFeign/feign/pull/3485) -Use `Encoder.of(...)` to compose multiple encoders into a single `DelegatingEncoder`, which +Use `MultiEncoder.of(...)` to compose multiple encoders into a single `MultiEncoder`, which delegates to the first encoder whose `encode()` returns `true`. **Before:** @@ -432,7 +432,7 @@ Feign.builder() ```java Feign.builder() - .encoder(Encoder.of( + .encoder(MultiEncoder.of( new FormEncoder(), new JacksonEncoder(), new JAXBEncoder(factory) @@ -448,40 +448,11 @@ Feign.builder() .target(MyApi.class, "https://api.example.com"); ``` -The `Encoder.of()` factory wraps the supplied encoders in a `DelegatingEncoder`, which tries +The `MultiEncoder.of()` factory returns a `MultiEncoder`, which tries each encoder's `encode()` and uses the first one that returns `true`. --- -### 18. Multi-encoder support — `DelegatingEncoder` (https://github.com/OpenFeign/feign/pull/3485) - -You can now compose multiple encoders via `Encoder.of()`, and Feign will pick the right one at -request time based on the return value of `encode()`. This is especially useful for APIs that mix -JSON, XML, and other content types: - -**Before:** - -```java -// Only one encoder — had to manually choose or wrap -Feign.builder() - .encoder(new FormEncoder(new JacksonEncoder())) - .target(MyApi.class, "https://api.example.com"); -``` - -**After:** - -```java -Feign.builder() - .encoder(Encoder.of( - new FormEncoder(), // handles multipart/form-urlencoded by Content-Type - new JacksonEncoder(), // implements JsonEncoder marker interface - new JAXBEncoder(factory) // handles XML - )) - .target(MyApi.class, "https://api.example.com"); -``` - ---- - ## Implementing a Custom Streaming Body If you want to stream a body (e.g., from a file or `InputStream`), implement `Request.Body` directly. Because @@ -547,6 +518,26 @@ public class FileBody implements Request.Body { --- +## Selecting Encoder Based on a Predicate + +TODO: Once we have specialized EncoderPredicate factory methods (i.e. for xml content types), update this example to use those factory methods + +The new `PredicateEncoder` and `EncoderPredicate` classes can be used in conjunction with `MultiEncoder` to fine tune which Encoder +handles different types of encode requests. + +```java +Feign.builder() + .encoder(MultiEncoder.of( + new DefaultEncoder(), + new PredicateEncoder((obj, type, templ) -> templ.headers().get("Content-Type").contains("application/xml"), new JAXBEncoder(factory), // handle xml requests + new JacksonEncoder() // handle everything else + )) + .target(MyApi.class, "https://api.example.com"); + +``` + +--- + ## Spring Cloud OpenFeign Compatibility `RequestTemplate#body(byte[], Charset)` is kept `@Deprecated` for backward compatibility with From 983a2447ccd612d43aa880fc841a4d9b9940bc86 Mon Sep 17 00:00:00 2001 From: kevin Date: Tue, 28 Jul 2026 17:22:33 -0700 Subject: [PATCH 56/63] Fix formatting --- .../feign/core/codec/PredicateEncoder.java | 10 +++++----- .../core/codec/PredicateEncoderTest.java | 19 +++++++++---------- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/core/src/main/java/feign/core/codec/PredicateEncoder.java b/core/src/main/java/feign/core/codec/PredicateEncoder.java index b2a852561..4f5929f26 100644 --- a/core/src/main/java/feign/core/codec/PredicateEncoder.java +++ b/core/src/main/java/feign/core/codec/PredicateEncoder.java @@ -23,14 +23,14 @@ /** * An encoder that wraps another (delegate) encoder but blocks encoding if a predicate isn't met. - * For example, this is useful for restricting the usage of a given Encoder to a given content type header. + * For example, this is useful for restricting the usage of a given Encoder to a given content type + * header. */ public class PredicateEncoder implements Encoder { - - /** - * The delegate that the encoding request will be passed to if the predicate is met. - */ + + /** The delegate that the encoding request will be passed to if the predicate is met. */ private final Encoder delegate; + private final EncoderPredicate predicate; public PredicateEncoder(EncoderPredicate predicate, Encoder delegate) { diff --git a/core/src/test/java/feign/core/codec/PredicateEncoderTest.java b/core/src/test/java/feign/core/codec/PredicateEncoderTest.java index 87945d18d..aa4f7e871 100644 --- a/core/src/test/java/feign/core/codec/PredicateEncoderTest.java +++ b/core/src/test/java/feign/core/codec/PredicateEncoderTest.java @@ -21,16 +21,15 @@ import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; +import feign.RequestTemplate; +import feign.codec.Encoder; +import feign.codec.EncoderPredicate; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import feign.RequestTemplate; -import feign.codec.Encoder; -import feign.codec.EncoderPredicate; - @ExtendWith(MockitoExtension.class) class PredicateEncoderTest { @@ -41,11 +40,11 @@ class PredicateEncoderTest { @Test void shouldEncode() { - String object = "Hello, World!"; - Class bodyType = String.class; - RequestTemplate requestTemplate = mock(RequestTemplate.class); - - when(encoderPredicate.test(object, bodyType, requestTemplate)).thenReturn(true); + String object = "Hello, World!"; + Class bodyType = String.class; + RequestTemplate requestTemplate = mock(RequestTemplate.class); + + when(encoderPredicate.test(object, bodyType, requestTemplate)).thenReturn(true); when(delegate.encode(object, bodyType, requestTemplate)).thenReturn(true); assertThat(predicateEncoder.encode(object, bodyType, requestTemplate)).isTrue(); @@ -58,7 +57,7 @@ void shouldNotEncode() { var object = "Hello, World!"; var bodyType = String.class; var requestTemplate = mock(RequestTemplate.class); - + when(encoderPredicate.test(object, bodyType, requestTemplate)).thenReturn(false); assertThat(predicateEncoder.encode(object, bodyType, requestTemplate)).isFalse(); From 6bdb435bbcb9449497c001f2cb05e64c7ffd4915 Mon Sep 17 00:00:00 2001 From: kevin Date: Mon, 10 Aug 2026 17:45:37 -0700 Subject: [PATCH 57/63] Set maven-jar-plugin.version back to 3.5.0 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 528d41761..ae3652972 100644 --- a/pom.xml +++ b/pom.xml @@ -188,7 +188,7 @@ 3.4.0 3.12.0 5.0.0 - 3.4.2 + 3.5.0 3.3.1 6.0.2 0.1.1 From af582ce171dbe6334f7c592793d7c03581c1bb6f Mon Sep 17 00:00:00 2001 From: kevin Date: Mon, 10 Aug 2026 17:52:06 -0700 Subject: [PATCH 58/63] Static factory should be public --- core/src/main/java/feign/core/codec/MultiEncoder.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/feign/core/codec/MultiEncoder.java b/core/src/main/java/feign/core/codec/MultiEncoder.java index d6c89228e..f29048322 100644 --- a/core/src/main/java/feign/core/codec/MultiEncoder.java +++ b/core/src/main/java/feign/core/codec/MultiEncoder.java @@ -40,7 +40,7 @@ public class MultiEncoder implements Encoder { * @return a delegating encoder * @since 14 */ - static Encoder of(Encoder... encoders) { + static public Encoder of(Encoder... encoders) { return of(Arrays.asList(encoders)); } @@ -52,7 +52,7 @@ static Encoder of(Encoder... encoders) { * @return a delegating encoder * @since 14 */ - static Encoder of(List encoders) { + static public Encoder of(List encoders) { return new MultiEncoder(encoders); } From 60984fb7f4be4253882bcaba9430a0e1e89458ef Mon Sep 17 00:00:00 2001 From: kevin Date: Mon, 10 Aug 2026 17:53:11 -0700 Subject: [PATCH 59/63] Remove reference to canEncode() method. Improve 14. example so it shows the previous use of EncodeException to indicate "can not handle type" and how this should be handled by returning false now. --- MIGRATION-v14.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/MIGRATION-v14.md b/MIGRATION-v14.md index d854b498b..ceb41f25e 100644 --- a/MIGRATION-v14.md +++ b/MIGRATION-v14.md @@ -358,7 +358,7 @@ VertxFeign.builder() ### 14. `Encoder.encode()` now returns `boolean` (https://github.com/OpenFeign/feign/pull/3485) `Encoder.encode()` now returns `boolean` instead of `void`. Return `true` when the encoder -handles the object, `false` otherwise. This replaces the separate `canEncode()` method. +handles the object, `false` otherwise. **Before:** @@ -366,6 +366,7 @@ handles the object, `false` otherwise. This replaces the separate `canEncode()` public class MyEncoder implements Encoder { @Override public void encode(Object object, Type bodyType, RequestTemplate template) { + if(bodyType != String.class) throw new EncodeException("Encoding not supported"); template.body(Request.Body.of(serialize(object))); } } @@ -378,7 +379,7 @@ 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 + return true; // or return false if the encoder does not handle this type. Do NOT throw EncodeException unless a problem actually happens during encoding. } } ``` From 85c98a9f8a7c2677117f8778581984ff26afe4e9 Mon Sep 17 00:00:00 2001 From: kevin Date: Mon, 10 Aug 2026 18:02:23 -0700 Subject: [PATCH 60/63] Rename PredicateEncoder to PredicatingEncoder --- MIGRATION-v14.md | 4 ++-- .../{PredicateEncoder.java => PredicatingEncoder.java} | 4 ++-- ...dicateEncoderTest.java => PredicatingEncoderTest.java} | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) rename core/src/main/java/feign/core/codec/{PredicateEncoder.java => PredicatingEncoder.java} (92%) rename core/src/test/java/feign/core/codec/{PredicateEncoderTest.java => PredicatingEncoderTest.java} (88%) diff --git a/MIGRATION-v14.md b/MIGRATION-v14.md index ceb41f25e..7885192cf 100644 --- a/MIGRATION-v14.md +++ b/MIGRATION-v14.md @@ -523,14 +523,14 @@ public class FileBody implements Request.Body { TODO: Once we have specialized EncoderPredicate factory methods (i.e. for xml content types), update this example to use those factory methods -The new `PredicateEncoder` and `EncoderPredicate` classes can be used in conjunction with `MultiEncoder` to fine tune which Encoder +The new `PredicatingEncoder` and `EncoderPredicate` classes can be used in conjunction with `MultiEncoder` to fine tune which Encoder handles different types of encode requests. ```java Feign.builder() .encoder(MultiEncoder.of( new DefaultEncoder(), - new PredicateEncoder((obj, type, templ) -> templ.headers().get("Content-Type").contains("application/xml"), new JAXBEncoder(factory), // handle xml requests + new PredicatingEncoder((obj, type, templ) -> templ.headers().get("Content-Type").contains("application/xml"), new JAXBEncoder(factory), // handle xml requests new JacksonEncoder() // handle everything else )) .target(MyApi.class, "https://api.example.com"); diff --git a/core/src/main/java/feign/core/codec/PredicateEncoder.java b/core/src/main/java/feign/core/codec/PredicatingEncoder.java similarity index 92% rename from core/src/main/java/feign/core/codec/PredicateEncoder.java rename to core/src/main/java/feign/core/codec/PredicatingEncoder.java index 4f5929f26..e97999046 100644 --- a/core/src/main/java/feign/core/codec/PredicateEncoder.java +++ b/core/src/main/java/feign/core/codec/PredicatingEncoder.java @@ -26,14 +26,14 @@ * For example, this is useful for restricting the usage of a given Encoder to a given content type * header. */ -public class PredicateEncoder implements Encoder { +public class PredicatingEncoder implements Encoder { /** The delegate that the encoding request will be passed to if the predicate is met. */ private final Encoder delegate; private final EncoderPredicate predicate; - public PredicateEncoder(EncoderPredicate predicate, Encoder delegate) { + public PredicatingEncoder(EncoderPredicate predicate, Encoder delegate) { this.delegate = delegate; this.predicate = predicate; } diff --git a/core/src/test/java/feign/core/codec/PredicateEncoderTest.java b/core/src/test/java/feign/core/codec/PredicatingEncoderTest.java similarity index 88% rename from core/src/test/java/feign/core/codec/PredicateEncoderTest.java rename to core/src/test/java/feign/core/codec/PredicatingEncoderTest.java index aa4f7e871..472e67231 100644 --- a/core/src/test/java/feign/core/codec/PredicateEncoderTest.java +++ b/core/src/test/java/feign/core/codec/PredicatingEncoderTest.java @@ -31,11 +31,11 @@ import org.mockito.junit.jupiter.MockitoExtension; @ExtendWith(MockitoExtension.class) -class PredicateEncoderTest { +class PredicatingEncoderTest { @Mock private Encoder delegate; @Mock private EncoderPredicate encoderPredicate; - @InjectMocks private PredicateEncoder predicateEncoder; + @InjectMocks private PredicatingEncoder predicatingEncoder; @Test void shouldEncode() { @@ -47,7 +47,7 @@ void shouldEncode() { when(encoderPredicate.test(object, bodyType, requestTemplate)).thenReturn(true); when(delegate.encode(object, bodyType, requestTemplate)).thenReturn(true); - assertThat(predicateEncoder.encode(object, bodyType, requestTemplate)).isTrue(); + assertThat(predicatingEncoder.encode(object, bodyType, requestTemplate)).isTrue(); verify(delegate).encode(object, bodyType, requestTemplate); } @@ -60,7 +60,7 @@ void shouldNotEncode() { when(encoderPredicate.test(object, bodyType, requestTemplate)).thenReturn(false); - assertThat(predicateEncoder.encode(object, bodyType, requestTemplate)).isFalse(); + assertThat(predicatingEncoder.encode(object, bodyType, requestTemplate)).isFalse(); verifyNoInteractions(delegate); } From f1f07b04b30bc687187c36ee22d0c7ecbdc35f58 Mon Sep 17 00:00:00 2001 From: kevin Date: Mon, 10 Aug 2026 18:13:14 -0700 Subject: [PATCH 61/63] fix code formatting --- core/src/main/java/feign/core/codec/MultiEncoder.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/feign/core/codec/MultiEncoder.java b/core/src/main/java/feign/core/codec/MultiEncoder.java index f29048322..5a3283d76 100644 --- a/core/src/main/java/feign/core/codec/MultiEncoder.java +++ b/core/src/main/java/feign/core/codec/MultiEncoder.java @@ -40,7 +40,7 @@ public class MultiEncoder implements Encoder { * @return a delegating encoder * @since 14 */ - static public Encoder of(Encoder... encoders) { + public static Encoder of(Encoder... encoders) { return of(Arrays.asList(encoders)); } @@ -52,7 +52,7 @@ static public Encoder of(Encoder... encoders) { * @return a delegating encoder * @since 14 */ - static public Encoder of(List encoders) { + public static Encoder of(List encoders) { return new MultiEncoder(encoders); } From 2b715d00f5bce7189e10a82479059acac0399862 Mon Sep 17 00:00:00 2001 From: kevin Date: Tue, 11 Aug 2026 15:18:11 -0700 Subject: [PATCH 62/63] Fix/improve javadocs --- .../java/feign/core/codec/MultiEncoder.java | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/core/src/main/java/feign/core/codec/MultiEncoder.java b/core/src/main/java/feign/core/codec/MultiEncoder.java index 5a3283d76..a95b2cb6c 100644 --- a/core/src/main/java/feign/core/codec/MultiEncoder.java +++ b/core/src/main/java/feign/core/codec/MultiEncoder.java @@ -33,11 +33,11 @@ public class MultiEncoder implements Encoder { private final List delegates; /** - * 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)}. + * Creates an 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 + * @param encoders the list of encoders that will be tried in sequence + * @return the multi-encoder * @since 14 */ public static Encoder of(Encoder... encoders) { @@ -45,11 +45,11 @@ public static Encoder of(Encoder... 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)}. + * Creates an 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 + * @return the multi-encoder * @since 14 */ public static Encoder of(List encoders) { @@ -68,12 +68,14 @@ private MultiEncoder(List delegates) { /** * Encodes the given object using the first delegate that can encode it. If no delegate can encode - * the object, an {@link EncodeException} is thrown. + * the object, returns false. * * @param object {@inheritDoc} * @param bodyType {@inheritDoc} * @param template {@inheritDoc} * @throws EncodeException {@inheritDoc} + * @return true if encoding happened, false if no delegate was available that could handle the + * encode request */ @Override public boolean encode(Object object, Type bodyType, RequestTemplate template) From 9134d7dd2139db2a07b7743122ac5b87c9fead7f Mon Sep 17 00:00:00 2001 From: kevin Date: Tue, 11 Aug 2026 15:18:24 -0700 Subject: [PATCH 63/63] Revert unnecessary version bumps --- .mvn/extensions.xml | 4 ++-- benchmark/pom.xml | 2 +- kotlin/pom.xml | 2 +- vertx/feign-vertx/pom.xml | 2 +- vertx/feign-vertx4-test/pom.xml | 2 +- vertx/feign-vertx5-test/pom.xml | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.mvn/extensions.xml b/.mvn/extensions.xml index 2ce7b7f8a..bfbb33c26 100644 --- a/.mvn/extensions.xml +++ b/.mvn/extensions.xml @@ -19,11 +19,11 @@ com.gradle develocity-maven-extension - 2.5.0 + 2.4.2 com.gradle common-custom-user-data-maven-extension - 2.3.0 + 2.2.0 diff --git a/benchmark/pom.xml b/benchmark/pom.xml index 9b50a24cb..30a88ef29 100644 --- a/benchmark/pom.xml +++ b/benchmark/pom.xml @@ -32,7 +32,7 @@ 1.37 0.5.3 1.3.8 - 4.2.16.Final + 4.2.15.Final true diff --git a/kotlin/pom.xml b/kotlin/pom.xml index 01e622ae5..92329f7c8 100644 --- a/kotlin/pom.xml +++ b/kotlin/pom.xml @@ -30,7 +30,7 @@ Feign Kotlin - 2.4.10 + 2.4.0 1.11.0 diff --git a/vertx/feign-vertx/pom.xml b/vertx/feign-vertx/pom.xml index ed8146528..f63715516 100644 --- a/vertx/feign-vertx/pom.xml +++ b/vertx/feign-vertx/pom.xml @@ -31,7 +31,7 @@ 11 - 2.22.1 + 2.22.0 diff --git a/vertx/feign-vertx4-test/pom.xml b/vertx/feign-vertx4-test/pom.xml index e2adcc182..4bb2c0047 100644 --- a/vertx/feign-vertx4-test/pom.xml +++ b/vertx/feign-vertx4-test/pom.xml @@ -30,7 +30,7 @@ Tests with Vertx 4.x. - 4.5.30 + 4.5.28 diff --git a/vertx/feign-vertx5-test/pom.xml b/vertx/feign-vertx5-test/pom.xml index f24c15a1a..c0431a64d 100644 --- a/vertx/feign-vertx5-test/pom.xml +++ b/vertx/feign-vertx5-test/pom.xml @@ -30,7 +30,7 @@ Tests with Vertx 5.x. - 5.1.5 + 5.1.3