diff --git a/.circleci/config.yml b/.circleci/config.yml
new file mode 100644
index 0000000000..414b063a86
--- /dev/null
+++ b/.circleci/config.yml
@@ -0,0 +1,255 @@
+#
+# Copyright 2012-2020 The Feign Authors
+#
+# 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.
+#
+
+orbs:
+ android: circleci/android@1.0.3
+
+# common executors
+executors:
+ java:
+ parameters:
+ version:
+ description: 'jdk version to use'
+ default: '8'
+ type: string
+ docker:
+ - image: circleci/openjdk:<>
+ android:
+ parameters:
+ version:
+ description: 'jdk version to use'
+ default: '8'
+ type: string
+ docker:
+ - image: circleci/openjdk:<>
+# common commands
+commands:
+ resolve-dependencies:
+ description: 'Download and prepare all dependencies'
+ steps:
+ - run:
+ name: 'Resolving Dependencies'
+ command: |
+ mvn dependency:resolve-plugins go-offline:resolve-dependencies -DskipTests=true -B
+ verify-formatting:
+ steps:
+ - run:
+ name: 'Verify formatting'
+ command: |
+ scripts/no-git-changes.sh
+ configure-gpg:
+ steps:
+ - run:
+ name: 'Configure GPG keys'
+ command: |
+ echo -e "$GPG_KEY" | gpg --batch --no-tty --import --yes
+ nexus-deploy:
+ steps:
+ - run:
+ name: 'Deploy Core Modules Sonatype'
+ command: |
+ mvn -nsu -s .circleci/settings.xml -P release -pl -:feign-benchmark -DskipTests=true deploy
+ nexus-deploy-jdk11:
+ steps:
+ - run:
+ name: 'Build JDK 11 Release modules locally'
+ command: |
+ mvn -B -nsu -s .circleci/settings.xml -P java11 -pl :feign-java11,:feign-jakarta -am -DskipTests=true install
+ - run:
+ name: 'Deploy JDK 11 Modules to Sonatype'
+ command: |
+ mvn -B -nsu -s .circleci/settings.xml -P release,java11 -pl :feign-java11,:feign-jakarta -DskipTests=true deploy
+# our job defaults
+defaults: &defaults
+ working_directory: ~/feign
+ environment:
+ # Customize the JVM maximum heap limit
+ MAVEN_OPTS: -Xmx3200m
+
+# branch filters
+master-only: &master-only
+ branches:
+ only: master
+
+tags-only: &tags-only
+ branches:
+ ignore: /.*/
+ tags:
+ only: /.*/
+
+all-branches: &all-branches
+ branches:
+ ignore: master
+ tags:
+ ignore: /.*/
+
+version: 2.1
+
+jobs:
+ test:
+ parameters:
+ jdk:
+ description: 'jdk version to use'
+ default: '8'
+ type: string
+ executor:
+ name: java
+ version: <>
+ <<: *defaults
+ steps:
+ - checkout
+ - restore_cache:
+ keys:
+ - feign-dependencies-{{ checksum "pom.xml" }}
+ - feign-dependencies-
+ - resolve-dependencies
+ - save_cache:
+ paths:
+ - ~/.m2
+ key: feign-dependencies-{{ checksum "pom.xml" }}
+ - run:
+ name: 'Test'
+ command: |
+ mvn -B test
+ - verify-formatting
+ android-test:
+ # These next lines define the Android machine image executor: https://circleci.com/docs/2.0/executor-types/
+ executor:
+ name: android/android-machine
+
+ steps:
+ # Checkout the code as the first step.
+ - checkout
+
+ # The next step will run the unit tests
+ - android/run-tests:
+ test-command: ./gradlew lint testDebug --continue
+
+ deploy:
+ parameters:
+ jdk:
+ description: 'jdk version to use'
+ default: '8'
+ type: string
+ executor:
+ name: java
+ version: <>
+ <<: *defaults
+ steps:
+ - checkout
+ - restore_cache:
+ keys:
+ - feign-dependencies-{{ checksum "pom.xml" }}
+ - feign-dependencies-
+ - resolve-dependencies
+ - configure-gpg
+ - nexus-deploy
+
+ deploy-jdk11:
+ parameters:
+ jdk:
+ description: 'jdk version to use'
+ default: '11'
+ type: string
+ executor:
+ name: java
+ version: <>
+ <<: *defaults
+ steps:
+ - checkout
+ - restore_cache:
+ keys:
+ - feign-dependencies-{{ checksum "pom.xml" }}
+ - feign-dependencies-
+ - resolve-dependencies
+ - configure-gpg
+ - nexus-deploy-jdk11
+
+workflows:
+ version: 2
+ build:
+ jobs:
+ - test:
+ jdk: '8'
+ name: 'jdk 8'
+ filters:
+ <<: *all-branches
+ - test:
+ jdk: '11'
+ name: 'jdk 11'
+ filters:
+ <<: *all-branches
+ - test:
+ jdk: '17-buster'
+ name: 'jdk 17'
+ filters:
+ <<: *all-branches
+ - test:
+ name: 'android test'
+
+
+ snapshot:
+ jobs:
+ - test:
+ jdk: '8'
+ name: 'jdk 8'
+ filters:
+ <<: *master-only
+ - test:
+ jdk: '11'
+ name: 'jdk 11'
+ filters:
+ <<: *master-only
+ - test:
+ jdk: '17-buster'
+ name: 'jdk 17'
+ filters:
+ <<: *master-only
+ - deploy:
+ jdk: '8'
+ name: 'deploy snapshot'
+ requires:
+ - 'jdk 8'
+ - 'jdk 11'
+ - 'jdk 17'
+ context: Sonatype
+ filters:
+ <<: *master-only
+ - deploy-jdk11:
+ jdk: '11'
+ name: 'deploy jdk11 snapshot modules'
+ requires:
+ - 'jdk 11'
+ - 'deploy snapshot'
+ context: Sonatype
+ filters:
+ <<: *master-only
+
+ release:
+ jobs:
+ - deploy:
+ jdk: '8'
+ name: 'release to maven central'
+ context: Sonatype
+ filters:
+ <<: *tags-only
+ - deploy-jdk11:
+ jdk: '11'
+ name: 'release jdk11 artifacts to maven central'
+ requires:
+ - 'release to maven central'
+ context: Sonatype
+ filters:
+ <<: *tags-only
+
diff --git a/.circleci/settings.xml b/.circleci/settings.xml
new file mode 100644
index 0000000000..b3b4740ad1
--- /dev/null
+++ b/.circleci/settings.xml
@@ -0,0 +1,39 @@
+
+
+
+
+ ossrh
+ ${env.SONATYPE_USER}
+ ${env.SONATYPE_PASSWORD}
+
+
+
+
+ ossrh
+
+ true
+
+
+ ${env.GPG_PASSPHRASE}
+
+
+
+
+
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000000..5b063201e4
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,11 @@
+# To get started with Dependabot version updates, you'll need to specify which
+# package ecosystems to update and where the package manifests are located.
+# Please see the documentation for all configuration options:
+# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
+
+version: 2
+updates:
+ - package-ecosystem: "maven" # See documentation for possible values
+ directory: "/" # Location of package manifests
+ schedule:
+ interval: "daily"
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000000..b4a09015cc
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,71 @@
+# Compiled source #
+###################
+*.com
+*.class
+*.dll
+*.exe
+*.o
+*.so
+
+# Packages #
+############
+# it's better to unpack these files and commit the raw source
+# git has its own built in compression methods
+*.7z
+*.dmg
+*.gz
+*.iso
+*.jar
+*.rar
+*.tar
+*.zip
+
+# Logs and databases #
+######################
+*.log
+
+# OS generated files #
+######################
+.DS_Store*
+ehthumbs.db
+Icon?
+Thumbs.db
+
+# Editor Files #
+################
+*~
+*.swp
+
+# Build output directies
+/target
+**/test-output
+**/target
+**/bin
+build
+*/build
+.m2
+
+# IntelliJ specific files/directories
+out
+.idea
+*.ipr
+*.iws
+*.iml
+atlassian-ide-plugin.xml
+
+# Eclipse specific files/directories
+.classpath
+.project
+.settings
+.metadata
+.factorypath
+.generated
+
+# NetBeans specific files/directories
+.nbattrs
+
+# encrypted values
+*.asc
+
+# maven versions
+*.versionsBackup
diff --git a/.mvn/jvm.config b/.mvn/jvm.config
new file mode 100644
index 0000000000..0e7dabeff6
--- /dev/null
+++ b/.mvn/jvm.config
@@ -0,0 +1 @@
+-Xmx1024m -XX:CICompilerCount=1 -XX:TieredStopAtLevel=1 -Djava.security.egd=file:/dev/./urandom
\ No newline at end of file
diff --git a/.mvn/maven.config b/.mvn/maven.config
new file mode 100644
index 0000000000..8b13789179
--- /dev/null
+++ b/.mvn/maven.config
@@ -0,0 +1 @@
+
diff --git a/.mvn/wrapper/maven-wrapper.jar b/.mvn/wrapper/maven-wrapper.jar
new file mode 100644
index 0000000000..c6feb8bb6f
Binary files /dev/null and b/.mvn/wrapper/maven-wrapper.jar differ
diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties
new file mode 100644
index 0000000000..c9023edfe7
--- /dev/null
+++ b/.mvn/wrapper/maven-wrapper.properties
@@ -0,0 +1 @@
+distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.5.4/apache-maven-3.5.4-bin.zip
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000000..7f3c52d2fe
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,328 @@
+### Version 11.9
+
+* `OkHttpClient` now implements `AsyncClient`
+
+### Version 10.9
+
+* Configurable to disable streaming mode for Default client by verils (#1182)
+* Overriding query parameter name by boggard (#1184)
+* Internal feign metrics by velo:
+* Dropwizard metrics 5 (#1181)
+* Micrometer (#1188)
+
+### Version 10.8
+
+* async feign variant supporting CompleteableFutures by motinis (#1174)
+* deterministic iterations for Feign mocks by contextshuffling (#1165)
+* Async client for apache http 5 by velo (#1179)
+
+### Version 10.7
+
+* Fix for vunerabilities reported by snky (#1121)
+* Makes iterator compatible with Java iterator expected behavior (#1117)
+* Bump reactive dependencies (#1105)
+* Deprecated `encoded` and add comment (#1108)
+
+### Version 10.6
+* Remove java8 module (#1086)
+* Add composed Spring annotations support (#1090)
+* Generate mocked clients for tests from feign interfaces (#1092)
+
+### Version 10.5
+* Add Apache Http 5 Client (#1065)
+* Updating Apache HttpClient to 4.5.10 (#1080) (#1081)
+* Spring4 contract (#1069)
+* Declarative contracts (#1060)
+
+### Version 10.4
+* Adding support for JDK Proxy (#1045)
+* Add Google HTTP Client support (#1057)
+
+### Version 10.3
+* Upgrade dependencies with security vunerabilities (#997 #1010 #1011 #1024 #1025 #1031 #1032)
+* Parse Retry-After header responses that include decimal points (#980)
+* Fine-grained HTTP error exceptions with client and server errors (#854)
+* Adds support for per request timeout options (#970)
+* Unwrap RetryableException and throw cause (#737)
+* JacksonEncoder avoids intermediate String request body (#989)
+* Respect decode404 flag and decode 404 response body (#1012)
+* Maintain user-given order for header values (#1009)
+
+### Version 10.1
+* Refactoring RequestTemplate to RFC6570 (#778)
+* Allow JAXB context caching in factory (#761)
+* Reactive Wrapper Support (#795)
+* Introduced native http2 client using Java 11 (#806)
+* Unwrap RetryableException and throw cause (#737)
+* Supports PATCH without a body paramter (#824)
+* Feign-Ribbon integration now depends on Ribbon 2.3.0, updated from Ribbon 2.1.1 (#826)
+
+### Version 10.0
+* Feign baseline is now JDK 8
+ - Feign is now being built and tested with OpenJDK 11 as well. Releases and code base will use JDK 8, we are just testing compatibility with JDK 11.
+* Removed @Deprecated methods marked for removal on feign 10.
+* `RetryException` includes the `Method` used for the offending `Request`.
+* `Response` objects now contain the `Request` used.
+
+### Version 9.6
+* Feign builder now supports flag `doNotCloseAfterDecode` to support lazy iteration of responses.
+* Adds `JacksonIteratorDecoder` and `StreamDecoder` to decode responses as `java.util.Iterator` or `java.util.stream.Stream`.
+
+### Version 9.5.1
+* When specified, Content-Type header is now included on OkHttp requests lacking a body.
+* Sets empty HttpEntity if apache request body is null.
+
+### Version 9.5
+* Introduces `feign-java8` with support for `java.util.Optional`
+* Adds `Feign.Builder.mapAndDecode()` to allow response preprocessing before decoding it.
+
+### Version 9.4.1
+* 404 responses are no longer swallowed for `void` return types.
+
+### Version 9.4
+* Adds Builder class to JAXBDecoder for disabling namespace-awareness (defaults to true).
+
+### Version 9.3
+* Adds `FallbackFactory`, allowing access to the cause of a Hystrix fallback
+* Adds support for encoded parameters via `@Param(encoded = true)`
+
+### Version 9.2
+* Adds Hystrix `SetterFactory` to customize group and command keys
+* Supports context path when using Ribbon `LoadBalancingTarget`
+* Adds builder methods for the Response object
+* Deprecates Response factory methods
+* Adds nullable Request field to the Response object
+
+### Version 9.1
+* Allows query parameters to match on a substring. Ex `q=body:{body}`
+
+### Version 9.0
+* Migrates to maven from gradle
+* Changes maven groupId to `io.github.openfeign`
+
+### Version 8.18
+* Adds support for expansion of @Param lists
+* Content-Length response bodies with lengths greater than Integer.MAX_VALUE report null length
+ * Previously the OkhttpClient would throw an exception, and ApacheHttpClient
+ would report a wrong, possibly negative value
+* Adds support for encoded query parameters in `@QueryMap` via `@QueryMap(encoded = true)`
+* Keys in `Response.headers` are now lower-cased. This map is now case-insensitive with regards to keys,
+ and iterates in lexicographic order.
+ * This is a step towards supporting http2, as header names in http1 are treated as case-insensitive
+ and http2 down-cases header names.
+
+### Version 8.17
+* Adds support to RxJava Completable via `HystrixFeign` builder with fallback support
+* Upgraded hystrix-core to 1.4.26
+* Upgrades dependency version for OkHttp/MockWebServer 3.2.0
+
+### Version 8.16
+* Adds `@HeaderMap` annotation to support dynamic header fields and values
+* Add support for default and static methods on interfaces
+
+### Version 8.15
+* Adds `@QueryMap` annotation to support dynamic query parameters
+* Supports runtime injection of `Param.Expander` via `MethodMetadata.indexToExpander`
+* Adds fallback support for HystrixCommand, Observable, and Single results
+* Supports PUT without a body parameter
+* Supports substitutions in `@Headers` like in `@Body`. (#326)
+ * **Note:** You might need to URL-encode literal values of `{` or `%` in your existing code.
+
+### Version 8.14
+* Add support for RxJava Observable and Single return types via the `HystrixFeign` builder.
+* Adds fallback implementation configuration to the `HystrixFeign` builder
+* Bumps dependency versions, most notably Gson 2.5 and OkHttp 2.7
+
+### Version 8.13
+* Never expands >8kb responses into memory
+
+### Version 8.12
+* Adds `Feign.Builder.decode404()` to reduce boilerplate for empty semantics.
+
+### Version 8.11
+* Adds support for Hystrix via a `HystrixFeign` builder.
+
+### Version 8.10
+* Adds HTTP status to FeignException for easier response handling
+* Reads class-level @Produces/@Consumes JAX-RS annotations
+* Supports POST without a body parameter
+
+### Version 8.9
+* Skips error handling when return type is `Response`
+
+### Version 8.8
+* Adds jackson-jaxb codec
+* Bumps dependency versions for integrations
+ * OkHttp/MockWebServer 2.5.0
+ * Jackson 2.6.1
+ * Apache Http Client 4.5
+ * JMH 1.10.5
+
+### Version 8.7
+* Bumps dependency versions for integrations
+ * OkHttp/MockWebServer 2.4.0
+ * Gson 2.3.1
+ * Jackson 2.6.0
+ * Ribbon 2.1.0
+ * SLF4J 1.7.12
+
+### Version 8.6
+* Adds base api support via single-inheritance interfaces
+
+### Version 7.5/8.5
+* Added possibility to leave slash encoded in path parameters
+
+### Version 8.4
+* Correct Retryer bug that prevented it from retrying requests after the first 5 retry attempts.
+ * **Note:** If you have a custom `feign.Retryer` implementation you now must now implement `public Retryer clone()`.
+ It is suggested that you simply return a new instance of your Retryer class.
+
+### Version 8.3
+* Adds client implementation for Apache Http Client
+
+### Version 8.2
+* Allows customized request construction by exposing `Request.create()`
+* Adds JMH benchmark module
+* Enforces source compatibility with animal-sniffer
+
+### Version 8.1
+* Allows `@Headers` to be applied to a type
+
+### Version 8.0
+* Removes Dagger 1.x Dependency
+* Removes support for parameters annotated with `javax.inject.@Named`. Use `feign.@Param` instead.
+* Makes body parameter type explicit.
+
+### Version 7.4
+* Allows `@Headers` to be applied to a type
+
+### Version 7.3
+* Adds Request.Options support to RibbonClient
+* Adds LBClientFactory to enable caching of Ribbon LBClients
+* Updates to Ribbon 2.0-RC13
+* Updates to Jackson 2.5.1
+* Supports query parameters without values
+
+### Version 7.2
+* Adds `Feign.Builder.build()`
+* Opens constructor for Gson and Jackson codecs which accepts type adapters
+* Adds EmptyTarget for interfaces who exclusively declare URI methods
+* Reformats code according to [Google Java Style](https://google-styleguide.googlecode.com/svn/trunk/javaguide.html)
+
+### Version 7.1
+* Introduces feign.@Param to annotate template parameters. Users must migrate from `javax.inject.@Named` to `feign.@Param` before updating to Feign 8.0.
+ * Supports custom expansion via `@Param(value = "name", expander = CustomExpander.class)`
+* Adds OkHttp integration
+* Allows multiple headers with the same name.
+* Ensures Accept headers default to `*/*`
+
+### Version 7.0
+* Expose reflective dispatch hook: InvocationHandlerFactory
+* Add JAXB integration
+* Add SLF4J integration
+* Upgrade to Dagger 1.2.2.
+ * **Note:** Dagger-generated code prior to version 1.2.0 is incompatible with Dagger 1.2.0 and beyond. Dagger users should upgrade Dagger to at least version 1.2.0, and recompile any dependency-injected classes.
+
+### Version 6.1.3
+* Updates to Ribbon 2.0-RC5
+
+### Version 6.1.1
+* Fix for #85
+
+### Version 6.1.0
+* Add [SLF4J](http://www.slf4j.org/) integration
+
+### Version 6.0.1
+* Fix for BasicAuthRequestInterceptor when username and/or password are long.
+
+### Version 6.0
+* Support binary request and response bodies.
+* Don't throw http status code exceptions when return type is `Response`.
+
+### Version 5.4.0
+* Add `BasicAuthRequestInterceptor`
+* Add Jackson integration
+
+### Version 5.3.0
+* Split `GsonCodec` into `GsonEncoder` and `GsonDecoder`, which are easy to use with `Feign.Builder`
+* Deprecate `GsonCodec`
+* Update to Ribbon 0.2.3
+
+### Version 5.2.0
+* Support usage of `GsonCodec` via `Feign.Builder`
+
+### Version 5.1.0
+* Correctly handle IOExceptions wrapped by Ribbon.
+* Miscellaneous findbugs fixes.
+
+### Version 5.0.1
+* `Decoder.decode()` is no longer called for `Response` or `void` types.
+
+### Version 5.0
+* Remove support for Observable methods.
+* Use single non-generic Decoder/Encoder instead of sets of type-specific Decoders/Encoders.
+* Decoders/Encoders are now more flexible, having access to the Response/RequestTemplate respectively.
+* Moved SaxDecoder into `feign-sax` dependency.
+ * SaxDecoder now decodes multiple types.
+ * Remove pattern decoders in favor of SaxDecoder.
+* Added Feign.Builder to simplify client customizations without using Dagger.
+* Gson type adapters can be registered as Dagger set bindings.
+* `Feign.create(...)` now requires specifying an encoder and decoder.
+
+### Version 4.4.1
+* Fix NullPointerException on calling equals and hashCode.
+
+### Version 4.4
+* Support overriding default HostnameVerifier.
+* Support GZIP content encoding for request bodies.
+* Support Iterable args for query parameters.
+* Support urls which have query parameters.
+
+### Version 4.3
+* Add ability to configure zero or more RequestInterceptors.
+* Remove `overrides = true` on codec modules.
+
+### Version 4.2/3.3
+* Document and enforce JAX-RS annotation processing from server POV
+* Skip query template parameters when corresponding java arg is null
+
+### Version 4.1/3.2
+* update to dagger 1.1
+* Add wikipedia search example
+* Allow `@Path` on types in feign-jaxrs
+
+### Version 4.0
+* Support RxJava-style Observers.
+ * Return type can be `Observable` for an async equiv of `Iterable`.
+ * `Observer` replaces `IncrementalCallback` and is passed to `Observable.subscribe()`.
+ * On `Subscription.unsubscribe()`, `Observer.onNext()` will stop being called.
+
+### Version 3.1
+* Log when an http request is retried or a response fails due to an IOException.
+
+### Version 3.0
+* Added support for asynchronous callbacks via `IncrementalCallback` and `IncrementalDecoder.TextStream`.
+* Wire is now Logger, with configurable Logger.Level.
+* Added `feign-gson` codec, used via `new GsonModule()`
+* changed codec to be similar to [WebSocket JSR 356](http://docs.oracle.com/javaee/7/api/javax/websocket/package-summary.html)
+ * Decoder is now `Decoder.TextStream`
+ * BodyEncoder is now `Encoder.Text`
+ * FormEncoder is now `Encoder.Text
+ * Feign standard decoders do not have built in support for this flag. If you are using this flag,
+ * you MUST also use a custom Decoder, and be sure to close all resources appropriately somewhere
+ * in the Decoder (you can use {@link Util#ensureClosed} for convenience).
+ *
+ * @since 9.6
+ *
+ */
+ public B doNotCloseAfterDecode() {
+ this.closeAfterDecode = false;
+ return thisB;
+ }
+
+ public B queryMapEncoder(QueryMapEncoder queryMapEncoder) {
+ this.queryMapEncoder = queryMapEncoder;
+ return thisB;
+ }
+
+ /**
+ * Allows to map the response before passing it to the decoder.
+ */
+ public B mapAndDecode(ResponseMapper mapper, Decoder decoder) {
+ this.decoder = new ResponseMappingDecoder(mapper, decoder);
+ return thisB;
+ }
+
+ /**
+ * This flag indicates that the {@link #decoder(Decoder) decoder} should process responses with
+ * 404 status, specifically returning null or empty instead of throwing {@link FeignException}.
+ *
+ *
+ * All first-party (ex gson) decoders return well-known empty values defined by
+ * {@link Util#emptyValueOf}. To customize further, wrap an existing {@link #decoder(Decoder)
+ * decoder} or make your own.
+ *
+ *
+ * This flag only works with 404, as opposed to all or arbitrary status codes. This was an
+ * explicit decision: 404 -> empty is safe, common and doesn't complicate redirection, retry or
+ * fallback policy. If your server returns a different status for not-found, correct via a custom
+ * {@link #client(Client) client}.
+ *
+ * @since 11.9
+ */
+ public B dismiss404() {
+ this.dismiss404 = true;
+ return thisB;
+ }
+
+
+ /**
+ * This flag indicates that the {@link #decoder(Decoder) decoder} should process responses with
+ * 404 status, specifically returning null or empty instead of throwing {@link FeignException}.
+ *
+ *
+ * All first-party (ex gson) decoders return well-known empty values defined by
+ * {@link Util#emptyValueOf}. To customize further, wrap an existing {@link #decoder(Decoder)
+ * decoder} or make your own.
+ *
+ *
+ * This flag only works with 404, as opposed to all or arbitrary status codes. This was an
+ * explicit decision: 404 -> empty is safe, common and doesn't complicate redirection, retry or
+ * fallback policy. If your server returns a different status for not-found, correct via a custom
+ * {@link #client(Client) client}.
+ *
+ * @since 8.12
+ * @deprecated
+ */
+ @Deprecated
+ public B decode404() {
+ this.dismiss404 = true;
+ return thisB;
+ }
+
+
+ public B errorDecoder(ErrorDecoder errorDecoder) {
+ this.errorDecoder = errorDecoder;
+ return thisB;
+ }
+
+ public B options(Options options) {
+ this.options = options;
+ return thisB;
+ }
+
+ /**
+ * Adds a single request interceptor to the builder.
+ */
+ public B requestInterceptor(RequestInterceptor requestInterceptor) {
+ this.requestInterceptors.add(requestInterceptor);
+ return thisB;
+ }
+
+ /**
+ * Sets the full set of request interceptors for the builder, overwriting any previous
+ * interceptors.
+ */
+ public B requestInterceptors(Iterable requestInterceptors) {
+ this.requestInterceptors.clear();
+ for (RequestInterceptor requestInterceptor : requestInterceptors) {
+ this.requestInterceptors.add(requestInterceptor);
+ }
+ return thisB;
+ }
+
+ /**
+ * Adds a single response interceptor to the builder.
+ */
+ public B responseInterceptor(ResponseInterceptor responseInterceptor) {
+ this.responseInterceptor = responseInterceptor;
+ return thisB;
+ }
+
+
+ /**
+ * Allows you to override how reflective dispatch works inside of Feign.
+ */
+ public B invocationHandlerFactory(InvocationHandlerFactory invocationHandlerFactory) {
+ this.invocationHandlerFactory = invocationHandlerFactory;
+ return thisB;
+ }
+
+ public B exceptionPropagationPolicy(ExceptionPropagationPolicy propagationPolicy) {
+ this.propagationPolicy = propagationPolicy;
+ return thisB;
+ }
+
+ public B addCapability(Capability capability) {
+ this.capabilities.add(capability);
+ return thisB;
+ }
+
+ protected B enrich() {
+ if (capabilities.isEmpty()) {
+ return thisB;
+ }
+
+ getFieldsToEnrich().forEach(field -> {
+ field.setAccessible(true);
+ try {
+ final Object originalValue = field.get(thisB);
+ final Object enriched;
+ if (originalValue instanceof List) {
+ Type ownerType = ((ParameterizedType) field.getGenericType()).getActualTypeArguments()[0];
+ enriched = ((List) originalValue).stream()
+ .map(value -> Capability.enrich(value, (Class>) ownerType, capabilities))
+ .collect(Collectors.toList());
+ } else {
+ enriched = Capability.enrich(originalValue, field.getType(), capabilities);
+ }
+ field.set(thisB, enriched);
+ } catch (IllegalArgumentException | IllegalAccessException e) {
+ throw new RuntimeException("Unable to enrich field " + field, e);
+ } finally {
+ field.setAccessible(false);
+ }
+ });
+
+ return thisB;
+ }
+
+ List getFieldsToEnrich() {
+ return Util.allFields(getClass())
+ .stream()
+ // exclude anything generated by compiler
+ .filter(field -> !field.isSynthetic())
+ // and capabilities itself
+ .filter(field -> !Objects.equals(field.getName(), "capabilities"))
+ // and thisB helper field
+ .filter(field -> !Objects.equals(field.getName(), "thisB"))
+ // skip primitive types
+ .filter(field -> !field.getType().isPrimitive())
+ // skip enumerations
+ .filter(field -> !field.getType().isEnum())
+ .collect(Collectors.toList());
+ }
+
+
+}
diff --git a/core/src/main/java/feign/Body.java b/core/src/main/java/feign/Body.java
new file mode 100644
index 0000000000..e7336fbb16
--- /dev/null
+++ b/core/src/main/java/feign/Body.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2012-2022 The Feign Authors
+ *
+ * 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 java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+import java.util.Map;
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+/**
+ * A possibly templated body of a PUT or POST command. variables wrapped in curly braces are
+ * expanded before the request is submitted.
+ * ex.
+ *
+ *
+ */
+public enum CollectionFormat {
+ /** Comma separated values, eg foo=bar,baz */
+ CSV(","),
+ /** Space separated values, eg foo=bar baz */
+ SSV(" "),
+ /** Tab separated values, eg foo=bar[tab]baz */
+ TSV("\t"),
+ /** Values separated with the pipe (|) character, eg foo=bar|baz */
+ PIPES("|"),
+ /** Parameter name repeated for each value, eg foo=bar&foo=baz */
+ // Using null as a special case since there is no single separator character
+ EXPLODED(null);
+
+ private final String separator;
+
+ CollectionFormat(String separator) {
+ this.separator = separator;
+ }
+
+ /**
+ * Joins the field and possibly multiple values with the given separator.
+ *
+ *
+ * Calling EXPLODED.join("foo", ["bar"]) will return "foo=bar".
+ *
+ * Null values are treated somewhat specially. With EXPLODED, the field is repeated without any
+ * "=" for backwards compatibility. With all other formats, null values are not included in the
+ * joined value list.
+ *
+ *
+ * @param field The field name corresponding to these values.
+ * @param values A collection of value strings for the given field.
+ * @param charset to encode the sequence
+ * @return The formatted char sequence of the field and joined values. If the value collection is
+ * empty, an empty char sequence will be returned.
+ */
+ public CharSequence join(String field, Collection values, Charset charset) {
+ StringBuilder builder = new StringBuilder();
+ int valueCount = 0;
+ for (String value : values) {
+ if (separator == null) {
+ // exploded
+ builder.append(valueCount++ == 0 ? "" : "&");
+ builder.append(UriUtils.encode(field, charset));
+ if (value != null) {
+ builder.append('=');
+ builder.append(value);
+ }
+ } else {
+ // delimited with a separator character
+ if (builder.length() == 0) {
+ builder.append(UriUtils.encode(field, charset));
+ }
+ if (value == null) {
+ continue;
+ }
+ builder.append(valueCount++ == 0 ? "=" : UriUtils.encode(separator, charset));
+ builder.append(value);
+ }
+ }
+ return builder;
+ }
+}
diff --git a/core/src/main/java/feign/Contract.java b/core/src/main/java/feign/Contract.java
new file mode 100644
index 0000000000..9f6a48800a
--- /dev/null
+++ b/core/src/main/java/feign/Contract.java
@@ -0,0 +1,336 @@
+/*
+ * Copyright 2012-2022 The Feign Authors
+ *
+ * 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 feign.Request.HttpMethod;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.lang.reflect.Parameter;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import static feign.Util.checkState;
+import static feign.Util.emptyToNull;
+
+/**
+ * Defines what annotations and values are valid on interfaces.
+ */
+public interface Contract {
+
+ /**
+ * Called to parse the methods in the class that are linked to HTTP requests.
+ *
+ * @param targetType {@link feign.Target#type() type} of the Feign interface.
+ */
+ List parseAndValidateMetadata(Class> targetType);
+
+ abstract class BaseContract implements Contract {
+
+ /**
+ * @param targetType {@link feign.Target#type() type} of the Feign interface.
+ * @see #parseAndValidateMetadata(Class)
+ */
+ @Override
+ public List parseAndValidateMetadata(Class> targetType) {
+ checkState(targetType.getTypeParameters().length == 0, "Parameterized types unsupported: %s",
+ targetType.getSimpleName());
+ checkState(targetType.getInterfaces().length <= 1, "Only single inheritance supported: %s",
+ targetType.getSimpleName());
+ final Map result = new LinkedHashMap();
+ for (final Method method : targetType.getMethods()) {
+ if (method.getDeclaringClass() == Object.class ||
+ (method.getModifiers() & Modifier.STATIC) != 0 ||
+ Util.isDefault(method)) {
+ continue;
+ }
+ final MethodMetadata metadata = parseAndValidateMetadata(targetType, method);
+ if (result.containsKey(metadata.configKey())) {
+ MethodMetadata existingMetadata = result.get(metadata.configKey());
+ Type existingReturnType = existingMetadata.returnType();
+ Type overridingReturnType = metadata.returnType();
+ Type resolvedType = Types.resolveReturnType(existingReturnType, overridingReturnType);
+ if (resolvedType.equals(overridingReturnType)) {
+ result.put(metadata.configKey(), metadata);
+ }
+ continue;
+ }
+ result.put(metadata.configKey(), metadata);
+ }
+ return new ArrayList<>(result.values());
+ }
+
+ /**
+ * @deprecated use {@link #parseAndValidateMetadata(Class, Method)} instead.
+ */
+ @Deprecated
+ public MethodMetadata parseAndValidateMetadata(Method method) {
+ return parseAndValidateMetadata(method.getDeclaringClass(), method);
+ }
+
+ /**
+ * Called indirectly by {@link #parseAndValidateMetadata(Class)}.
+ */
+ protected MethodMetadata parseAndValidateMetadata(Class> targetType, Method method) {
+ final MethodMetadata data = new MethodMetadata();
+ data.targetType(targetType);
+ data.method(method);
+ data.returnType(
+ Types.resolve(targetType, targetType, method.getGenericReturnType()));
+ data.configKey(Feign.configKey(targetType, method));
+ if (AlwaysEncodeBodyContract.class.isAssignableFrom(this.getClass())) {
+ data.alwaysEncodeBody(true);
+ }
+
+ if (targetType.getInterfaces().length == 1) {
+ processAnnotationOnClass(data, targetType.getInterfaces()[0]);
+ }
+ processAnnotationOnClass(data, targetType);
+
+
+ for (final Annotation methodAnnotation : method.getAnnotations()) {
+ processAnnotationOnMethod(data, methodAnnotation, method);
+ }
+ if (data.isIgnored()) {
+ return data;
+ }
+ checkState(data.template().method() != null,
+ "Method %s not annotated with HTTP method type (ex. GET, POST)%s",
+ data.configKey(), data.warnings());
+ final Class>[] parameterTypes = method.getParameterTypes();
+ final Type[] genericParameterTypes = method.getGenericParameterTypes();
+
+ final Annotation[][] parameterAnnotations = method.getParameterAnnotations();
+ final int count = parameterAnnotations.length;
+ for (int i = 0; i < count; i++) {
+ boolean isHttpAnnotation = false;
+ if (parameterAnnotations[i] != null) {
+ isHttpAnnotation = processAnnotationsOnParameter(data, parameterAnnotations[i], i);
+ }
+
+ if (isHttpAnnotation) {
+ data.ignoreParamater(i);
+ }
+
+ if (parameterTypes[i] == URI.class) {
+ data.urlIndex(i);
+ } else if (!isHttpAnnotation
+ && !Request.Options.class.isAssignableFrom(parameterTypes[i])) {
+ if (data.isAlreadyProcessed(i)) {
+ checkState(data.formParams().isEmpty() || data.bodyIndex() == null,
+ "Body parameters cannot be used with form parameters.%s", data.warnings());
+ } else if (!data.alwaysEncodeBody()) {
+ checkState(data.formParams().isEmpty(),
+ "Body parameters cannot be used with form parameters.%s", data.warnings());
+ checkState(data.bodyIndex() == null,
+ "Method has too many Body parameters: %s%s", method, data.warnings());
+ data.bodyIndex(i);
+ data.bodyType(
+ Types.resolve(targetType, targetType, genericParameterTypes[i]));
+ }
+ }
+ }
+
+ if (data.headerMapIndex() != null) {
+ // check header map parameter for map type
+ if (Map.class.isAssignableFrom(parameterTypes[data.headerMapIndex()])) {
+ checkMapKeys("HeaderMap", genericParameterTypes[data.headerMapIndex()]);
+ }
+ }
+
+ if (data.queryMapIndex() != null) {
+ if (Map.class.isAssignableFrom(parameterTypes[data.queryMapIndex()])) {
+ checkMapKeys("QueryMap", genericParameterTypes[data.queryMapIndex()]);
+ }
+ }
+
+ return data;
+ }
+
+ private static void checkMapString(String name, Class> type, Type genericType) {
+ checkState(Map.class.isAssignableFrom(type),
+ "%s parameter must be a Map: %s", name, type);
+ checkMapKeys(name, genericType);
+ }
+
+ private static void checkMapKeys(String name, Type genericType) {
+ Class> keyClass = null;
+
+ // assume our type parameterized
+ if (ParameterizedType.class.isAssignableFrom(genericType.getClass())) {
+ final Type[] parameterTypes = ((ParameterizedType) genericType).getActualTypeArguments();
+ keyClass = (Class>) parameterTypes[0];
+ } else if (genericType instanceof Class>) {
+ // raw class, type parameters cannot be inferred directly, but we can scan any extended
+ // interfaces looking for any explict types
+ final Type[] interfaces = ((Class>) genericType).getGenericInterfaces();
+ for (final Type extended : interfaces) {
+ if (ParameterizedType.class.isAssignableFrom(extended.getClass())) {
+ // use the first extended interface we find.
+ final Type[] parameterTypes = ((ParameterizedType) extended).getActualTypeArguments();
+ keyClass = (Class>) parameterTypes[0];
+ break;
+ }
+ }
+ }
+
+ if (keyClass != null) {
+ checkState(String.class.equals(keyClass),
+ "%s key must be a String: %s", name, keyClass.getSimpleName());
+ }
+ }
+
+ /**
+ * Called by parseAndValidateMetadata twice, first on the declaring class, then on the target
+ * type (unless they are the same).
+ *
+ * @param data metadata collected so far relating to the current java method.
+ * @param clz the class to process
+ */
+ protected abstract void processAnnotationOnClass(MethodMetadata data, Class> clz);
+
+ /**
+ * @param data metadata collected so far relating to the current java method.
+ * @param annotation annotations present on the current method annotation.
+ * @param method method currently being processed.
+ */
+ protected abstract void processAnnotationOnMethod(MethodMetadata data,
+ Annotation annotation,
+ Method method);
+
+ /**
+ * @param data metadata collected so far relating to the current java method.
+ * @param annotations annotations present on the current parameter annotation.
+ * @param paramIndex if you find a name in {@code annotations}, call
+ * {@link #nameParam(MethodMetadata, String, int)} with this as the last parameter.
+ * @return true if you called {@link #nameParam(MethodMetadata, String, int)} after finding an
+ * http-relevant annotation.
+ */
+ protected abstract boolean processAnnotationsOnParameter(MethodMetadata data,
+ Annotation[] annotations,
+ int paramIndex);
+
+ /**
+ * links a parameter name to its index in the method signature.
+ */
+ protected void nameParam(MethodMetadata data, String name, int i) {
+ final Collection names =
+ data.indexToName().containsKey(i) ? data.indexToName().get(i) : new ArrayList();
+ names.add(name);
+ data.indexToName().put(i, names);
+ }
+ }
+
+ class Default extends DeclarativeContract {
+
+ static final Pattern REQUEST_LINE_PATTERN = Pattern.compile("^([A-Z]+)[ ]*(.*)$");
+
+ public Default() {
+ super.registerClassAnnotation(Headers.class, (header, data) -> {
+ final String[] headersOnType = header.value();
+ checkState(headersOnType.length > 0, "Headers annotation was empty on type %s.",
+ data.configKey());
+ final Map> headers = toMap(headersOnType);
+ headers.putAll(data.template().headers());
+ data.template().headers(null); // to clear
+ data.template().headers(headers);
+ });
+ super.registerMethodAnnotation(RequestLine.class, (ann, data) -> {
+ final String requestLine = ann.value();
+ checkState(emptyToNull(requestLine) != null,
+ "RequestLine annotation was empty on method %s.", data.configKey());
+
+ final Matcher requestLineMatcher = REQUEST_LINE_PATTERN.matcher(requestLine);
+ if (!requestLineMatcher.find()) {
+ throw new IllegalStateException(String.format(
+ "RequestLine annotation didn't start with an HTTP verb on method %s",
+ data.configKey()));
+ } else {
+ data.template().method(HttpMethod.valueOf(requestLineMatcher.group(1)));
+ data.template().uri(requestLineMatcher.group(2));
+ }
+ data.template().decodeSlash(ann.decodeSlash());
+ data.template()
+ .collectionFormat(ann.collectionFormat());
+ });
+ super.registerMethodAnnotation(Body.class, (ann, data) -> {
+ final String body = ann.value();
+ checkState(emptyToNull(body) != null, "Body annotation was empty on method %s.",
+ data.configKey());
+ if (body.indexOf('{') == -1) {
+ data.template().body(body);
+ } else {
+ data.template().bodyTemplate(body);
+ }
+ });
+ super.registerMethodAnnotation(Headers.class, (header, data) -> {
+ final String[] headersOnMethod = header.value();
+ checkState(headersOnMethod.length > 0, "Headers annotation was empty on method %s.",
+ data.configKey());
+ data.template().headers(toMap(headersOnMethod));
+ });
+ super.registerParameterAnnotation(Param.class, (paramAnnotation, data, paramIndex) -> {
+ final String annotationName = paramAnnotation.value();
+ final Parameter parameter = data.method().getParameters()[paramIndex];
+ final String name;
+ if (emptyToNull(annotationName) == null && parameter.isNamePresent()) {
+ name = parameter.getName();
+ } else {
+ name = annotationName;
+ }
+ checkState(emptyToNull(name) != null, "Param annotation was empty on param %s.",
+ paramIndex);
+ nameParam(data, name, paramIndex);
+ final Class extends Param.Expander> expander = paramAnnotation.expander();
+ if (expander != Param.ToStringExpander.class) {
+ data.indexToExpanderClass().put(paramIndex, expander);
+ }
+ if (!data.template().hasRequestVariable(name)) {
+ data.formParams().add(name);
+ }
+ });
+ super.registerParameterAnnotation(QueryMap.class, (queryMap, data, paramIndex) -> {
+ checkState(data.queryMapIndex() == null,
+ "QueryMap annotation was present on multiple parameters.");
+ data.queryMapIndex(paramIndex);
+ });
+ super.registerParameterAnnotation(HeaderMap.class, (queryMap, data, paramIndex) -> {
+ checkState(data.headerMapIndex() == null,
+ "HeaderMap annotation was present on multiple parameters.");
+ data.headerMapIndex(paramIndex);
+ });
+ }
+
+ private static Map> toMap(String[] input) {
+ final Map> result =
+ new LinkedHashMap>(input.length);
+ for (final String header : input) {
+ final int colon = header.indexOf(':');
+ final String name = header.substring(0, colon);
+ if (!result.containsKey(name)) {
+ result.put(name, new ArrayList(1));
+ }
+ result.get(name).add(header.substring(colon + 1).trim());
+ }
+ return result;
+ }
+ }
+}
diff --git a/core/src/main/java/feign/DeclarativeContract.java b/core/src/main/java/feign/DeclarativeContract.java
new file mode 100644
index 0000000000..f105ea0bbb
--- /dev/null
+++ b/core/src/main/java/feign/DeclarativeContract.java
@@ -0,0 +1,263 @@
+/*
+ * Copyright 2012-2022 The Feign Authors
+ *
+ * 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 feign.Contract.BaseContract;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Method;
+import java.lang.reflect.Parameter;
+import java.util.*;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+/**
+ * {@link Contract} base implementation that works by declaring witch annotations should be
+ * processed and how each annotation modifies {@link MethodMetadata}
+ */
+public abstract class DeclarativeContract extends BaseContract {
+
+ private final List classAnnotationProcessors = new ArrayList<>();
+ private final List methodAnnotationProcessors = new ArrayList<>();
+ private final Map, DeclarativeContract.ParameterAnnotationProcessor> parameterAnnotationProcessors =
+ new HashMap<>();
+
+ @Override
+ public final List parseAndValidateMetadata(Class> targetType) {
+ // any implementations must register processors
+ return super.parseAndValidateMetadata(targetType);
+ }
+
+ /**
+ * Called by parseAndValidateMetadata twice, first on the declaring class, then on the target type
+ * (unless they are the same).
+ *
+ * @param data metadata collected so far relating to the current java method.
+ * @param targetType the class to process
+ */
+ @Override
+ protected final void processAnnotationOnClass(MethodMetadata data, Class> targetType) {
+ final List processors = Arrays.stream(targetType.getAnnotations())
+ .flatMap(annotation -> classAnnotationProcessors.stream()
+ .filter(processor -> processor.test(annotation)))
+ .collect(Collectors.toList());
+
+ if (!processors.isEmpty()) {
+ Arrays.stream(targetType.getAnnotations())
+ .forEach(annotation -> processors.stream()
+ .filter(processor -> processor.test(annotation))
+ .forEach(processor -> processor.process(annotation, data)));
+ } else {
+ if (targetType.getAnnotations().length == 0) {
+ data.addWarning(String.format(
+ "Class %s has no annotations, it may affect contract %s",
+ targetType.getSimpleName(),
+ getClass().getSimpleName()));
+ } else {
+ data.addWarning(String.format(
+ "Class %s has annotations %s that are not used by contract %s",
+ targetType.getSimpleName(),
+ Arrays.stream(targetType.getAnnotations())
+ .map(annotation -> annotation.annotationType()
+ .getSimpleName())
+ .collect(Collectors.toList()),
+ getClass().getSimpleName()));
+ }
+ }
+ }
+
+ /**
+ * @param data metadata collected so far relating to the current java method.
+ * @param annotation annotations present on the current method annotation.
+ * @param method method currently being processed.
+ */
+ @Override
+ protected final void processAnnotationOnMethod(MethodMetadata data,
+ Annotation annotation,
+ Method method) {
+ List processors = methodAnnotationProcessors.stream()
+ .filter(processor -> processor.test(annotation))
+ .collect(Collectors.toList());
+
+ if (!processors.isEmpty()) {
+ processors.forEach(processor -> processor.process(annotation, data));
+ } else {
+ data.addWarning(String.format(
+ "Method %s has an annotation %s that is not used by contract %s",
+ method.getName(),
+ annotation.annotationType()
+ .getSimpleName(),
+ getClass().getSimpleName()));
+ }
+ }
+
+
+ /**
+ * @param data metadata collected so far relating to the current java method.
+ * @param annotations annotations present on the current parameter annotation.
+ * @param paramIndex if you find a name in {@code annotations}, call
+ * {@link #nameParam(MethodMetadata, String, int)} with this as the last parameter.
+ * @return true if you called {@link #nameParam(MethodMetadata, String, int)} after finding an
+ * http-relevant annotation.
+ */
+ @Override
+ protected final boolean processAnnotationsOnParameter(MethodMetadata data,
+ Annotation[] annotations,
+ int paramIndex) {
+ List matchingAnnotations = Arrays.stream(annotations)
+ .filter(
+ annotation -> parameterAnnotationProcessors.containsKey(annotation.annotationType()))
+ .collect(Collectors.toList());
+
+ if (!matchingAnnotations.isEmpty()) {
+ matchingAnnotations.forEach(annotation -> parameterAnnotationProcessors
+ .getOrDefault(annotation.annotationType(), ParameterAnnotationProcessor.DO_NOTHING)
+ .process(annotation, data, paramIndex));
+
+ } else {
+ final Parameter parameter = data.method().getParameters()[paramIndex];
+ String parameterName = parameter.isNamePresent()
+ ? parameter.getName()
+ : parameter.getType().getSimpleName();
+ if (annotations.length == 0) {
+ data.addWarning(String.format(
+ "Parameter %s has no annotations, it may affect contract %s",
+ parameterName,
+ getClass().getSimpleName()));
+ } else {
+ data.addWarning(String.format(
+ "Parameter %s has annotations %s that are not used by contract %s",
+ parameterName,
+ Arrays.stream(annotations)
+ .map(annotation -> annotation.annotationType()
+ .getSimpleName())
+ .collect(Collectors.toList()),
+ getClass().getSimpleName()));
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Called while class annotations are being processed
+ *
+ * @param annotationType to be processed
+ * @param processor function that defines the annotations modifies {@link MethodMetadata}
+ */
+ protected void registerClassAnnotation(Class annotationType,
+ DeclarativeContract.AnnotationProcessor processor) {
+ registerClassAnnotation(
+ annotation -> annotation.annotationType().equals(annotationType),
+ processor);
+ }
+
+ /**
+ * Called while class annotations are being processed
+ *
+ * @param predicate to check if the annotation should be processed or not
+ * @param processor function that defines the annotations modifies {@link MethodMetadata}
+ */
+ protected void registerClassAnnotation(Predicate predicate,
+ DeclarativeContract.AnnotationProcessor processor) {
+ this.classAnnotationProcessors.add(new GuardedAnnotationProcessor(predicate, processor));
+ }
+
+ /**
+ * Called while method annotations are being processed
+ *
+ * @param annotationType to be processed
+ * @param processor function that defines the annotations modifies {@link MethodMetadata}
+ */
+ protected void registerMethodAnnotation(Class annotationType,
+ DeclarativeContract.AnnotationProcessor processor) {
+ registerMethodAnnotation(
+ annotation -> annotation.annotationType().equals(annotationType),
+ processor);
+ }
+
+ /**
+ * Called while method annotations are being processed
+ *
+ * @param predicate to check if the annotation should be processed or not
+ * @param processor function that defines the annotations modifies {@link MethodMetadata}
+ */
+ protected void registerMethodAnnotation(Predicate predicate,
+ DeclarativeContract.AnnotationProcessor processor) {
+ this.methodAnnotationProcessors.add(new GuardedAnnotationProcessor(predicate, processor));
+ }
+
+ /**
+ * Called while method parameter annotations are being processed
+ *
+ * @param annotation to be processed
+ * @param processor function that defines the annotations modifies {@link MethodMetadata}
+ */
+ protected void registerParameterAnnotation(Class annotation,
+ DeclarativeContract.ParameterAnnotationProcessor processor) {
+ this.parameterAnnotationProcessors.put((Class) annotation,
+ (DeclarativeContract.ParameterAnnotationProcessor) processor);
+ }
+
+ @FunctionalInterface
+ public interface AnnotationProcessor {
+
+ /**
+ * @param annotation present on the current element.
+ * @param metadata collected so far relating to the current java method.
+ */
+ void process(E annotation, MethodMetadata metadata);
+ }
+
+ @FunctionalInterface
+ public interface ParameterAnnotationProcessor {
+
+ DeclarativeContract.ParameterAnnotationProcessor DO_NOTHING = (ann, data, i) -> {
+ };
+
+ /**
+ * @param annotation present on the current parameter annotation.
+ * @param metadata metadata collected so far relating to the current java method.
+ * @param paramIndex if you find a name in {@code annotations}, call
+ * {@link #nameParam(MethodMetadata, String, int)} with this as the last parameter.
+ * @return true if you called {@link #nameParam(MethodMetadata, String, int)} after finding an
+ * http-relevant annotation.
+ */
+ void process(E annotation, MethodMetadata metadata, int paramIndex);
+ }
+
+ private class GuardedAnnotationProcessor
+ implements Predicate, DeclarativeContract.AnnotationProcessor {
+
+ private final Predicate predicate;
+ private final DeclarativeContract.AnnotationProcessor processor;
+
+ @SuppressWarnings({"rawtypes", "unchecked"})
+ private GuardedAnnotationProcessor(Predicate predicate,
+ DeclarativeContract.AnnotationProcessor processor) {
+ this.predicate = predicate;
+ this.processor = processor;
+ }
+
+ @Override
+ public void process(Annotation annotation, MethodMetadata metadata) {
+ processor.process(annotation, metadata);
+ }
+
+ @Override
+ public boolean test(Annotation t) {
+ return predicate.test(t);
+ }
+
+ }
+
+}
diff --git a/core/src/main/java/feign/DefaultMethodHandler.java b/core/src/main/java/feign/DefaultMethodHandler.java
new file mode 100644
index 0000000000..d7e35fd56d
--- /dev/null
+++ b/core/src/main/java/feign/DefaultMethodHandler.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright 2012-2022 The Feign Authors
+ *
+ * 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 feign.InvocationHandlerFactory.MethodHandler;
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.MethodHandles.Lookup;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+/**
+ * Handles default methods by directly invoking the default method code on the interface. The bindTo
+ * method must be called on the result before invoke is called.
+ */
+final class DefaultMethodHandler implements MethodHandler {
+ // Uses Java 7 MethodHandle based reflection. As default methods will only exist when
+ // run on a Java 8 JVM this will not affect use on legacy JVMs.
+ // When Feign upgrades to Java 7, remove the @IgnoreJRERequirement annotation.
+ private final MethodHandle unboundHandle;
+
+ // handle is effectively final after bindTo has been called...
+ private MethodHandle handle;
+
+ public DefaultMethodHandler(Method defaultMethod) {
+ Class> declaringClass = defaultMethod.getDeclaringClass();
+
+ try {
+ Lookup lookup = readLookup(declaringClass);
+ this.unboundHandle = lookup.unreflectSpecial(defaultMethod, declaringClass);
+ } catch (NoSuchFieldException | IllegalAccessException | IllegalArgumentException
+ | InvocationTargetException ex) {
+ throw new IllegalStateException(ex);
+ }
+
+ }
+
+ private Lookup readLookup(Class> declaringClass)
+ throws IllegalAccessException, InvocationTargetException, NoSuchFieldException {
+ try {
+ return safeReadLookup(declaringClass);
+ } catch (NoSuchMethodException e) {
+ try {
+ return androidLookup(declaringClass);
+ } catch (InstantiationException | NoSuchMethodException instantiationException) {
+ return legacyReadLookup();
+ }
+ }
+ }
+
+ public Lookup androidLookup(Class> declaringClass) throws InstantiationException,
+ InvocationTargetException, NoSuchMethodException, IllegalAccessException {
+ Lookup lookup;
+ try {
+ // Android 9+ double reflection
+ Class> classReference = Class.class;
+ Class>[] classType = new Class[] {Class.class};
+ Method reflectedGetDeclaredConstructor = classReference.getDeclaredMethod(
+ "getDeclaredConstructor",
+ Class[].class);
+ reflectedGetDeclaredConstructor.setAccessible(true);
+ Constructor> someHiddenMethod =
+ (Constructor>) reflectedGetDeclaredConstructor.invoke(Lookup.class, (Object) classType);
+ lookup = (Lookup) someHiddenMethod.newInstance(declaringClass);
+ } catch (IllegalAccessException ex0) {
+ // Android < 9 reflection
+ Constructor lookupConstructor = Lookup.class.getDeclaredConstructor(Class.class);
+ lookupConstructor.setAccessible(true);
+ lookup = lookupConstructor.newInstance(declaringClass);
+ }
+ return (lookup);
+ }
+
+
+ /**
+ * equivalent to:
+ *
+ *
+ *
+ * @param declaringClass
+ * @return
+ * @throws IllegalAccessException
+ * @throws InvocationTargetException
+ * @throws NoSuchMethodException
+ */
+ private Lookup safeReadLookup(Class> declaringClass)
+ throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
+ Lookup lookup = MethodHandles.lookup();
+
+ Object privateLookupIn =
+ MethodHandles.class.getMethod("privateLookupIn", Class.class, Lookup.class)
+ .invoke(null, declaringClass, lookup);
+ return (Lookup) privateLookupIn;
+ }
+
+ private Lookup legacyReadLookup() throws NoSuchFieldException, IllegalAccessException {
+ Field field = Lookup.class.getDeclaredField("IMPL_LOOKUP");
+ field.setAccessible(true);
+ Lookup lookup = (Lookup) field.get(null);
+ return lookup;
+ }
+
+ /**
+ * Bind this handler to a proxy object. After bound, DefaultMethodHandler#invoke will act as if it
+ * was called on the proxy object. Must be called once and only once for a given instance of
+ * DefaultMethodHandler
+ */
+ public void bindTo(Object proxy) {
+ if (handle != null) {
+ throw new IllegalStateException(
+ "Attempted to rebind a default method handler that was already bound");
+ }
+ handle = unboundHandle.bindTo(proxy);
+ }
+
+ /**
+ * Invoke this method. DefaultMethodHandler#bindTo must be called before the first time invoke is
+ * called.
+ */
+ @Override
+ public Object invoke(Object[] argv) throws Throwable {
+ if (handle == null) {
+ throw new IllegalStateException(
+ "Default method handler invoked before proxy has been bound.");
+ }
+ return handle.invokeWithArguments(argv);
+ }
+}
diff --git a/core/src/main/java/feign/ExceptionPropagationPolicy.java b/core/src/main/java/feign/ExceptionPropagationPolicy.java
new file mode 100644
index 0000000000..a706414af4
--- /dev/null
+++ b/core/src/main/java/feign/ExceptionPropagationPolicy.java
@@ -0,0 +1,18 @@
+/*
+ * Copyright 2012-2022 The Feign Authors
+ *
+ * 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;
+
+public enum ExceptionPropagationPolicy {
+ NONE, UNWRAP
+}
diff --git a/core/src/main/java/feign/Experimental.java b/core/src/main/java/feign/Experimental.java
new file mode 100644
index 0000000000..3205dce2cf
--- /dev/null
+++ b/core/src/main/java/feign/Experimental.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2012-2022 The Feign Authors
+ *
+ * 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 java.lang.annotation.*;
+import java.lang.annotation.Target;
+
+/**
+ * Indicates that a public API (public class, method or field) is subject to incompatible changes,
+ * or even removal, in a future release. An API bearing this annotation is exempt from any
+ * compatibility guarantees made by its containing library. Note that the presence of this
+ * annotation implies nothing about the quality or performance of the API in question, only the fact
+ * that it is not "API-frozen."
+ *
+ *
+ * It is generally safe for applications to depend on beta APIs, at the cost of some extra
+ * work during upgrades. However it is generally inadvisable for libraries (which get
+ * included on users' CLASSPATHs, outside the library developers' control) to do so.
+ *
+ * "Inspired" on guava @Beta
+ */
+@Retention(RetentionPolicy.CLASS)
+@Target({
+ ElementType.ANNOTATION_TYPE,
+ ElementType.CONSTRUCTOR,
+ ElementType.FIELD,
+ ElementType.METHOD,
+ ElementType.TYPE
+})
+@Documented
+public @interface Experimental {
+
+}
diff --git a/core/src/main/java/feign/Feign.java b/core/src/main/java/feign/Feign.java
new file mode 100644
index 0000000000..6134439ae8
--- /dev/null
+++ b/core/src/main/java/feign/Feign.java
@@ -0,0 +1,237 @@
+/*
+ * Copyright 2012-2022 The Feign Authors
+ *
+ * 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 feign.ReflectiveFeign.ParseHandlersByName;
+import feign.Request.Options;
+import feign.Target.HardCodedTarget;
+import feign.codec.Decoder;
+import feign.codec.Encoder;
+import feign.codec.ErrorDecoder;
+import java.io.IOException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Type;
+
+/**
+ * Feign's purpose is to ease development against http apis that feign restfulness.
+ * In implementation, Feign is a {@link Feign#newInstance factory} for generating {@link Target
+ * targeted} http apis.
+ */
+public abstract class Feign {
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ /**
+ * Configuration keys are formatted as unresolved see
+ * tags. This method exposes that format, in case you need to create the same value as
+ * {@link MethodMetadata#configKey()} for correlation purposes.
+ *
+ *
+ * Here are some sample encodings:
+ *
+ *
+ *
+ *
{@code
+ * Route53
+ * }: would match a class {@code
+ * route53.Route53
+ * }
+ *
{@code Route53#list()}: would match a method {@code route53.Route53#list()}
+ *
{@code Route53#listAt(Marker)}: would match a method {@code
+ * route53.Route53#listAt(Marker)}
+ *
{@code Route53#listByNameAndType(String, String)}: would match a method {@code
+ * route53.Route53#listAt(String, String)}
+ *
+ *
+ *
+ * Note that there is no whitespace expected in a key!
+ *
+ * @param targetType {@link feign.Target#type() type} of the Feign interface.
+ * @param method invoked method, present on {@code type} or its super.
+ * @see MethodMetadata#configKey()
+ */
+ public static String configKey(Class targetType, Method method) {
+ StringBuilder builder = new StringBuilder();
+ builder.append(targetType.getSimpleName());
+ builder.append('#').append(method.getName()).append('(');
+ for (Type param : method.getGenericParameterTypes()) {
+ param = Types.resolve(targetType, targetType, param);
+ builder.append(Types.getRawType(param).getSimpleName()).append(',');
+ }
+ if (method.getParameterTypes().length > 0) {
+ builder.deleteCharAt(builder.length() - 1);
+ }
+ return builder.append(')').toString();
+ }
+
+ /**
+ * @deprecated use {@link #configKey(Class, Method)} instead.
+ */
+ @Deprecated
+ public static String configKey(Method method) {
+ return configKey(method.getDeclaringClass(), method);
+ }
+
+ /**
+ * Returns a new instance of an HTTP API, defined by annotations in the {@link Feign Contract},
+ * for the specified {@code target}. You should cache this result.
+ */
+ public abstract T newInstance(Target target);
+
+ public static class Builder extends BaseBuilder {
+
+ private Client client = new Client.Default(null, null);
+ private boolean forceDecoding = false;
+
+ @Override
+ public Builder logLevel(Logger.Level logLevel) {
+ return super.logLevel(logLevel);
+ }
+
+ @Override
+ public Builder contract(Contract contract) {
+ return super.contract(contract);
+ }
+
+ public Builder client(Client client) {
+ this.client = client;
+
+ return this;
+ }
+
+ @Override
+ public Builder retryer(Retryer retryer) {
+ return super.retryer(retryer);
+ }
+
+ @Override
+ public Builder logger(Logger logger) {
+ return super.logger(logger);
+ }
+
+ @Override
+ public Builder encoder(Encoder encoder) {
+ return super.encoder(encoder);
+ }
+
+ @Override
+ public Builder decoder(Decoder decoder) {
+ return super.decoder(decoder);
+ }
+
+ @Override
+ public Builder queryMapEncoder(QueryMapEncoder queryMapEncoder) {
+ return super.queryMapEncoder(queryMapEncoder);
+ }
+
+ @Override
+ public Builder mapAndDecode(ResponseMapper mapper, Decoder decoder) {
+ return super.mapAndDecode(mapper, decoder);
+ }
+
+ @Deprecated
+ @Override
+ public Builder decode404() {
+ return super.decode404();
+ }
+
+ @Override
+ public Builder errorDecoder(ErrorDecoder errorDecoder) {
+ return super.errorDecoder(errorDecoder);
+ }
+
+ @Override
+ public Builder options(Options options) {
+ return super.options(options);
+ }
+
+ @Override
+ public Builder requestInterceptor(RequestInterceptor requestInterceptor) {
+ return super.requestInterceptor(requestInterceptor);
+ }
+
+ @Override
+ public Builder requestInterceptors(Iterable requestInterceptors) {
+ return super.requestInterceptors(requestInterceptors);
+ }
+
+ @Override
+ public Builder invocationHandlerFactory(InvocationHandlerFactory invocationHandlerFactory) {
+ return super.invocationHandlerFactory(invocationHandlerFactory);
+ }
+
+ @Override
+ public Builder doNotCloseAfterDecode() {
+ return super.doNotCloseAfterDecode();
+ }
+
+ @Override
+ public Builder exceptionPropagationPolicy(ExceptionPropagationPolicy propagationPolicy) {
+ return super.exceptionPropagationPolicy(propagationPolicy);
+ }
+
+ @Override
+ public Builder addCapability(Capability capability) {
+ return super.addCapability(capability);
+ }
+
+ /**
+ * Internal - used to indicate that the decoder should be immediately called
+ */
+ Builder forceDecoding() {
+ this.forceDecoding = true;
+ return this;
+ }
+
+ public T target(Class apiType, String url) {
+ return target(new HardCodedTarget<>(apiType, url));
+ }
+
+ public T target(Target target) {
+ return build().newInstance(target);
+ }
+
+ public Feign build() {
+ super.enrich();
+
+ SynchronousMethodHandler.Factory synchronousMethodHandlerFactory =
+ new SynchronousMethodHandler.Factory(client, retryer, requestInterceptors,
+ responseInterceptor, logger, logLevel, dismiss404, closeAfterDecode,
+ propagationPolicy, forceDecoding);
+ ParseHandlersByName handlersByName =
+ new ParseHandlersByName(contract, options, encoder, decoder, queryMapEncoder,
+ errorDecoder, synchronousMethodHandlerFactory);
+ return new ReflectiveFeign(handlersByName, invocationHandlerFactory, queryMapEncoder);
+ }
+ }
+
+ public static class ResponseMappingDecoder implements Decoder {
+
+ private final ResponseMapper mapper;
+ private final Decoder delegate;
+
+ public ResponseMappingDecoder(ResponseMapper mapper, Decoder decoder) {
+ this.mapper = mapper;
+ this.delegate = decoder;
+ }
+
+ @Override
+ public Object decode(Response response, Type type) throws IOException {
+ return delegate.decode(mapper.map(response, type), type);
+ }
+ }
+}
diff --git a/core/src/main/java/feign/FeignException.java b/core/src/main/java/feign/FeignException.java
new file mode 100644
index 0000000000..6d54d186fc
--- /dev/null
+++ b/core/src/main/java/feign/FeignException.java
@@ -0,0 +1,508 @@
+/*
+ * Copyright 2012-2022 The Feign Authors
+ *
+ * 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 java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.nio.Buffer;
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.charset.Charset;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Optional;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import static feign.Util.*;
+import static java.lang.String.format;
+
+/**
+ * Origin exception type for all Http Apis.
+ */
+public class FeignException extends RuntimeException {
+
+ private static final String EXCEPTION_MESSAGE_TEMPLATE_NULL_REQUEST =
+ "request should not be null";
+ private static final long serialVersionUID = 0;
+ private int status;
+ private byte[] responseBody;
+ private Map> responseHeaders;
+ private Request request;
+
+ protected FeignException(int status, String message, Throwable cause) {
+ super(message, cause);
+ this.status = status;
+ this.request = null;
+ }
+
+ protected FeignException(int status, String message, Throwable cause, byte[] responseBody,
+ Map> responseHeaders) {
+ super(message, cause);
+ this.status = status;
+ this.responseBody = responseBody;
+ this.responseHeaders = caseInsensitiveCopyOf(responseHeaders);
+ this.request = null;
+ }
+
+ protected FeignException(int status, String message) {
+ super(message);
+ this.status = status;
+ this.request = null;
+ }
+
+ protected FeignException(int status, String message, byte[] responseBody,
+ Map> responseHeaders) {
+ super(message);
+ this.status = status;
+ this.responseBody = responseBody;
+ this.responseHeaders = caseInsensitiveCopyOf(responseHeaders);
+ this.request = null;
+ }
+
+ protected FeignException(int status, String message, Request request, Throwable cause) {
+ super(message, cause);
+ this.status = status;
+ this.request = checkRequestNotNull(request);
+ }
+
+ protected FeignException(int status, String message, Request request, Throwable cause,
+ byte[] responseBody, Map> responseHeaders) {
+ super(message, cause);
+ this.status = status;
+ this.responseBody = responseBody;
+ this.responseHeaders = caseInsensitiveCopyOf(responseHeaders);
+ this.request = checkRequestNotNull(request);
+ }
+
+ protected FeignException(int status, String message, Request request) {
+ super(message);
+ this.status = status;
+ this.request = checkRequestNotNull(request);
+ }
+
+ protected FeignException(int status, String message, Request request, byte[] responseBody,
+ Map> responseHeaders) {
+ super(message);
+ this.status = status;
+ this.responseBody = responseBody;
+ this.responseHeaders = caseInsensitiveCopyOf(responseHeaders);
+ this.request = checkRequestNotNull(request);
+ }
+
+ private Request checkRequestNotNull(Request request) {
+ return checkNotNull(request, EXCEPTION_MESSAGE_TEMPLATE_NULL_REQUEST);
+ }
+
+ public int status() {
+ return this.status;
+ }
+
+ /**
+ * The Response Body, if present.
+ *
+ * @return the body of the response.
+ * @deprecated use {@link #responseBody()} instead.
+ */
+ @Deprecated
+ public byte[] content() {
+ return this.responseBody;
+ }
+
+ /**
+ * The Response body.
+ *
+ * @return an Optional wrapping the response body.
+ */
+ public Optional responseBody() {
+ if (this.responseBody == null) {
+ return Optional.empty();
+ }
+ return Optional.of(ByteBuffer.wrap(this.responseBody));
+ }
+
+ public Map> responseHeaders() {
+ if (this.responseHeaders == null) {
+ return Collections.emptyMap();
+ }
+ return responseHeaders;
+ }
+
+ public Request request() {
+ return this.request;
+ }
+
+ public boolean hasRequest() {
+ return (this.request != null);
+ }
+
+ public String contentUTF8() {
+ if (responseBody != null) {
+ return new String(responseBody, UTF_8);
+ } else {
+ return "";
+ }
+ }
+
+ static FeignException errorReading(Request request, Response response, IOException cause) {
+ return new FeignException(
+ response.status(),
+ format("%s reading %s %s", cause.getMessage(), request.httpMethod(), request.url()),
+ request,
+ cause,
+ request.body(),
+ request.headers());
+ }
+
+ public static FeignException errorStatus(String methodKey, Response response) {
+
+ byte[] body = {};
+ try {
+ if (response.body() != null) {
+ body = Util.toByteArray(response.body().asInputStream());
+ }
+ } catch (IOException ignored) { // NOPMD
+ }
+
+ String message = new FeignExceptionMessageBuilder()
+ .withResponse(response)
+ .withMethodKey(methodKey)
+ .withBody(body).build();
+
+ return errorStatus(response.status(), message, response.request(), body, response.headers());
+ }
+
+ private static FeignException errorStatus(int status,
+ String message,
+ Request request,
+ byte[] body,
+ Map> headers) {
+ if (isClientError(status)) {
+ return clientErrorStatus(status, message, request, body, headers);
+ }
+ if (isServerError(status)) {
+ return serverErrorStatus(status, message, request, body, headers);
+ }
+ return new FeignException(status, message, request, body, headers);
+ }
+
+ private static boolean isClientError(int status) {
+ return status >= 400 && status < 500;
+ }
+
+ private static FeignClientException clientErrorStatus(int status,
+ String message,
+ Request request,
+ byte[] body,
+ Map> headers) {
+ switch (status) {
+ case 400:
+ return new BadRequest(message, request, body, headers);
+ case 401:
+ return new Unauthorized(message, request, body, headers);
+ case 403:
+ return new Forbidden(message, request, body, headers);
+ case 404:
+ return new NotFound(message, request, body, headers);
+ case 405:
+ return new MethodNotAllowed(message, request, body, headers);
+ case 406:
+ return new NotAcceptable(message, request, body, headers);
+ case 409:
+ return new Conflict(message, request, body, headers);
+ case 410:
+ return new Gone(message, request, body, headers);
+ case 415:
+ return new UnsupportedMediaType(message, request, body, headers);
+ case 429:
+ return new TooManyRequests(message, request, body, headers);
+ case 422:
+ return new UnprocessableEntity(message, request, body, headers);
+ default:
+ return new FeignClientException(status, message, request, body, headers);
+ }
+ }
+
+ private static boolean isServerError(int status) {
+ return status >= 500 && status <= 599;
+ }
+
+ private static FeignServerException serverErrorStatus(int status,
+ String message,
+ Request request,
+ byte[] body,
+ Map> headers) {
+ switch (status) {
+ case 500:
+ return new InternalServerError(message, request, body, headers);
+ case 501:
+ return new NotImplemented(message, request, body, headers);
+ case 502:
+ return new BadGateway(message, request, body, headers);
+ case 503:
+ return new ServiceUnavailable(message, request, body, headers);
+ case 504:
+ return new GatewayTimeout(message, request, body, headers);
+ default:
+ return new FeignServerException(status, message, request, body, headers);
+ }
+ }
+
+ static FeignException errorExecuting(Request request, IOException cause) {
+ return new RetryableException(
+ -1,
+ format("%s executing %s %s", cause.getMessage(), request.httpMethod(), request.url()),
+ request.httpMethod(),
+ cause,
+ null, request);
+ }
+
+ public static class FeignClientException extends FeignException {
+ public FeignClientException(int status, String message, Request request, byte[] body,
+ Map> headers) {
+ super(status, message, request, body, headers);
+ }
+ }
+
+
+ public static class BadRequest extends FeignClientException {
+ public BadRequest(String message, Request request, byte[] body,
+ Map> headers) {
+ super(400, message, request, body, headers);
+ }
+ }
+
+
+ public static class Unauthorized extends FeignClientException {
+ public Unauthorized(String message, Request request, byte[] body,
+ Map> headers) {
+ super(401, message, request, body, headers);
+ }
+ }
+
+
+ public static class Forbidden extends FeignClientException {
+ public Forbidden(String message, Request request, byte[] body,
+ Map> headers) {
+ super(403, message, request, body, headers);
+ }
+ }
+
+
+ public static class NotFound extends FeignClientException {
+ public NotFound(String message, Request request, byte[] body,
+ Map> headers) {
+ super(404, message, request, body, headers);
+ }
+ }
+
+
+ public static class MethodNotAllowed extends FeignClientException {
+ public MethodNotAllowed(String message, Request request, byte[] body,
+ Map> headers) {
+ super(405, message, request, body, headers);
+ }
+ }
+
+
+ public static class NotAcceptable extends FeignClientException {
+ public NotAcceptable(String message, Request request, byte[] body,
+ Map> headers) {
+ super(406, message, request, body, headers);
+ }
+ }
+
+
+ public static class Conflict extends FeignClientException {
+ public Conflict(String message, Request request, byte[] body,
+ Map> headers) {
+ super(409, message, request, body, headers);
+ }
+ }
+
+
+ public static class Gone extends FeignClientException {
+ public Gone(String message, Request request, byte[] body,
+ Map> headers) {
+ super(410, message, request, body, headers);
+ }
+ }
+
+
+ public static class UnsupportedMediaType extends FeignClientException {
+ public UnsupportedMediaType(String message, Request request, byte[] body,
+ Map> headers) {
+ super(415, message, request, body, headers);
+ }
+ }
+
+
+ public static class TooManyRequests extends FeignClientException {
+ public TooManyRequests(String message, Request request, byte[] body,
+ Map> headers) {
+ super(429, message, request, body, headers);
+ }
+ }
+
+
+ public static class UnprocessableEntity extends FeignClientException {
+ public UnprocessableEntity(String message, Request request, byte[] body,
+ Map> headers) {
+ super(422, message, request, body, headers);
+ }
+ }
+
+
+ public static class FeignServerException extends FeignException {
+ public FeignServerException(int status, String message, Request request, byte[] body,
+ Map> headers) {
+ super(status, message, request, body, headers);
+ }
+ }
+
+
+ public static class InternalServerError extends FeignServerException {
+ public InternalServerError(String message, Request request, byte[] body,
+ Map> headers) {
+ super(500, message, request, body, headers);
+ }
+ }
+
+
+ public static class NotImplemented extends FeignServerException {
+ public NotImplemented(String message, Request request, byte[] body,
+ Map> headers) {
+ super(501, message, request, body, headers);
+ }
+ }
+
+
+ public static class BadGateway extends FeignServerException {
+ public BadGateway(String message, Request request, byte[] body,
+ Map> headers) {
+ super(502, message, request, body, headers);
+ }
+ }
+
+
+ public static class ServiceUnavailable extends FeignServerException {
+ public ServiceUnavailable(String message, Request request, byte[] body,
+ Map> headers) {
+ super(503, message, request, body, headers);
+ }
+ }
+
+
+ public static class GatewayTimeout extends FeignServerException {
+ public GatewayTimeout(String message, Request request, byte[] body,
+ Map> headers) {
+ super(504, message, request, body, headers);
+ }
+ }
+
+
+ private static class FeignExceptionMessageBuilder {
+
+ private static final int MAX_BODY_BYTES_LENGTH = 400;
+ private static final int MAX_BODY_CHARS_LENGTH = 200;
+
+ private Response response;
+
+ private byte[] body;
+ private String methodKey;
+
+ public FeignExceptionMessageBuilder withResponse(Response response) {
+ this.response = response;
+ return this;
+ }
+
+ public FeignExceptionMessageBuilder withBody(byte[] body) {
+ this.body = body;
+ return this;
+ }
+
+ public FeignExceptionMessageBuilder withMethodKey(String methodKey) {
+ this.methodKey = methodKey;
+ return this;
+ }
+
+ public String build() {
+ StringBuilder result = new StringBuilder();
+
+ if (response.reason() != null) {
+ result.append(format("[%d %s]", response.status(), response.reason()));
+ } else {
+ result.append(format("[%d]", response.status()));
+ }
+ result.append(format(" during [%s] to [%s] [%s]", response.request().httpMethod(),
+ response.request().url(), methodKey));
+
+ result.append(format(": [%s]", getBodyAsString(body, response.headers())));
+
+ return result.toString();
+ }
+
+ private static String getBodyAsString(byte[] body, Map> headers) {
+ Charset charset = getResponseCharset(headers);
+ if (charset == null) {
+ charset = Util.UTF_8;
+ }
+ return getResponseBody(body, charset);
+ }
+
+ private static String getResponseBody(byte[] body, Charset charset) {
+ if (body.length < MAX_BODY_BYTES_LENGTH) {
+ return new String(body, charset);
+ }
+ return getResponseBodyPreview(body, charset);
+ }
+
+ private static String getResponseBodyPreview(byte[] body, Charset charset) {
+ try {
+ Reader reader = new InputStreamReader(new ByteArrayInputStream(body), charset);
+ CharBuffer result = CharBuffer.allocate(MAX_BODY_CHARS_LENGTH);
+
+ reader.read(result);
+ reader.close();
+ ((Buffer) result).flip();
+ return result.toString() + "... (" + body.length + " bytes)";
+ } catch (IOException e) {
+ return e.toString() + ", failed to parse response";
+ }
+ }
+
+ private static Charset getResponseCharset(Map> headers) {
+
+ Collection strings = headers.get("content-type");
+ if (strings == null || strings.isEmpty()) {
+ return null;
+ }
+
+ Pattern pattern = Pattern.compile(".*charset=([^\\s|^;]+).*");
+ Matcher matcher = pattern.matcher(strings.iterator().next());
+ if (!matcher.lookingAt()) {
+ return null;
+ }
+
+ String group = matcher.group(1);
+ if (!Charset.isSupported(group)) {
+ return null;
+ }
+ return Charset.forName(group);
+
+ }
+ }
+}
diff --git a/core/src/main/java/feign/HeaderMap.java b/core/src/main/java/feign/HeaderMap.java
new file mode 100644
index 0000000000..51f3f1bd5d
--- /dev/null
+++ b/core/src/main/java/feign/HeaderMap.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2012-2022 The Feign Authors
+ *
+ * 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 java.lang.annotation.Retention;
+import java.util.List;
+import java.util.Map;
+import static java.lang.annotation.ElementType.PARAMETER;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+/**
+ * A template parameter that can be applied to a Map that contains header entries, where the keys
+ * are Strings that are the header field names and the values are the header field values. The
+ * headers specified by the map will be applied to the request after all other processing, and will
+ * take precedence over any previously specified header parameters.
+ * This parameter is useful in cases where different header fields and values need to be set on an
+ * API method on a per-request basis in a thread-safe manner and independently of Feign client
+ * construction. A concrete example of a case like this are custom metadata header fields (e.g. as
+ * "x-amz-meta-*" or "x-goog-meta-*") where the header field names are dynamic and the range of keys
+ * cannot be determined a priori. The {@link Headers} annotation does not allow this because the
+ * header fields that it defines are static (it is not possible to add or remove fields on a
+ * per-request basis), and doing this using a custom {@link Target} or {@link RequestInterceptor}
+ * can be cumbersome (it requires more code for per-method customization, it is difficult to
+ * implement in a thread-safe manner and it requires customization when the Feign client for the API
+ * is built).
+ *
+ *
+ *
+ * The annotated parameter must be an instance of {@link Map}, and the keys must be Strings. The
+ * header field value of a key will be the value of its toString method, except in the following
+ * cases:
+ *
+ *
+ *
if the value is null, the value will remain null (rather than converting to the String
+ * "null")
+ *
if the value is an {@link Iterable}, it is converted to a {@link List} of String objects
+ * where each value in the list is either null if the original value was null or the value's
+ * toString representation otherwise.
+ *
+ *
+ * Once this conversion is applied, the query keys and resulting String values follow the same
+ * contract as if they were set using {@link RequestTemplate#header(String, String...)}.
+ */
+@Retention(RUNTIME)
+@java.lang.annotation.Target(PARAMETER)
+public @interface HeaderMap {
+}
diff --git a/core/src/main/java/feign/Headers.java b/core/src/main/java/feign/Headers.java
new file mode 100644
index 0000000000..425ac1574a
--- /dev/null
+++ b/core/src/main/java/feign/Headers.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2012-2022 The Feign Authors
+ *
+ * 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 java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+/**
+ * Expands headers supplied in the {@code value}. Variables to the the right of the colon are
+ * expanded.
+ *
+ *
+ */
+@Target({METHOD, TYPE})
+@Retention(RUNTIME)
+public @interface Headers {
+
+ String[] value();
+}
diff --git a/core/src/main/java/feign/InvocationContext.java b/core/src/main/java/feign/InvocationContext.java
new file mode 100644
index 0000000000..f4d96e7990
--- /dev/null
+++ b/core/src/main/java/feign/InvocationContext.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2012-2022 The Feign Authors
+ *
+ * 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 feign.FeignException.errorReading;
+import feign.codec.DecodeException;
+import feign.codec.Decoder;
+import java.io.IOException;
+import java.lang.reflect.Type;
+
+public class InvocationContext {
+
+ private final Decoder decoder;
+ private final Type returnType;
+ private final Response response;
+
+ InvocationContext(Decoder decoder, Type returnType, Response response) {
+ this.decoder = decoder;
+ this.returnType = returnType;
+ this.response = response;
+ }
+
+ public Object proceed() {
+ try {
+ return decoder.decode(response, returnType);
+ } catch (final FeignException e) {
+ throw e;
+ } catch (final RuntimeException e) {
+ throw new DecodeException(response.status(), e.getMessage(), response.request(), e);
+ } catch (IOException e) {
+ throw errorReading(response.request(), response, e);
+ }
+ }
+
+ public Decoder decoder() {
+ return decoder;
+ }
+
+ public Type returnType() {
+ return returnType;
+ }
+
+ public Response response() {
+ return response;
+ }
+
+}
diff --git a/core/src/main/java/feign/InvocationHandlerFactory.java b/core/src/main/java/feign/InvocationHandlerFactory.java
new file mode 100644
index 0000000000..fbf13574bb
--- /dev/null
+++ b/core/src/main/java/feign/InvocationHandlerFactory.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2012-2022 The Feign Authors
+ *
+ * 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 java.lang.reflect.InvocationHandler;
+import java.lang.reflect.Method;
+import java.util.Map;
+
+/**
+ * Controls reflective method dispatch.
+ */
+public interface InvocationHandlerFactory {
+
+ InvocationHandler create(Target target, Map dispatch);
+
+ /**
+ * Like {@link InvocationHandler#invoke(Object, java.lang.reflect.Method, Object[])}, except for a
+ * single method.
+ */
+ interface MethodHandler {
+
+ Object invoke(Object[] argv) throws Throwable;
+ }
+
+ static final class Default implements InvocationHandlerFactory {
+
+ @Override
+ public InvocationHandler create(Target target, Map dispatch) {
+ return new ReflectiveFeign.FeignInvocationHandler(target, dispatch);
+ }
+ }
+}
diff --git a/core/src/main/java/feign/Logger.java b/core/src/main/java/feign/Logger.java
new file mode 100644
index 0000000000..99740e53c8
--- /dev/null
+++ b/core/src/main/java/feign/Logger.java
@@ -0,0 +1,303 @@
+/*
+ * Copyright 2012-2022 The Feign Authors
+ *
+ * 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 java.io.IOException;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.logging.FileHandler;
+import java.util.logging.LogRecord;
+import java.util.logging.SimpleFormatter;
+import static feign.Util.*;
+import static java.util.Objects.nonNull;
+
+/**
+ * Simple logging abstraction for debug messages. Adapted from {@code retrofit.RestAdapter.Log}.
+ */
+public abstract class Logger {
+
+ protected static String methodTag(String configKey) {
+ return '[' + configKey.substring(0, configKey.indexOf('(')) + "] ";
+ }
+
+ /**
+ * Override to log requests and responses using your own implementation. Messages will be http
+ * request and response text.
+ *
+ * @param configKey value of {@link Feign#configKey(Class, java.lang.reflect.Method)}
+ * @param format {@link java.util.Formatter format string}
+ * @param args arguments applied to {@code format}
+ */
+ protected abstract void log(String configKey, String format, Object... args);
+
+ /**
+ * Override to filter out request headers.
+ *
+ * @param header header name
+ * @return true to log a request header
+ */
+ protected boolean shouldLogRequestHeader(String header) {
+ return true;
+ }
+
+ /**
+ * Override to filter out response headers.
+ *
+ * @param header header name
+ * @return true to log a response header
+ */
+ protected boolean shouldLogResponseHeader(String header) {
+ return true;
+ }
+
+ protected void logRequest(String configKey, Level logLevel, Request request) {
+ String protocolVersion = resolveProtocolVersion(request.protocolVersion());
+ log(configKey, "---> %s %s %s", request.httpMethod().name(), request.url(),
+ protocolVersion);
+ if (logLevel.ordinal() >= Level.HEADERS.ordinal()) {
+
+ for (String field : request.headers().keySet()) {
+ if (shouldLogRequestHeader(field)) {
+ for (String value : valuesOrEmpty(request.headers(), field)) {
+ log(configKey, "%s: %s", field, value);
+ }
+ }
+ }
+
+ int bodyLength = 0;
+ if (request.body() != null) {
+ bodyLength = request.length();
+ if (logLevel.ordinal() >= Level.FULL.ordinal()) {
+ String bodyText =
+ request.charset() != null
+ ? new String(request.body(), request.charset())
+ : null;
+ log(configKey, ""); // CRLF
+ log(configKey, "%s", bodyText != null ? bodyText : "Binary data");
+ }
+ }
+ log(configKey, "---> END HTTP (%s-byte body)", bodyLength);
+ }
+ }
+
+ protected void logRetry(String configKey, Level logLevel) {
+ log(configKey, "---> RETRYING");
+ }
+
+ protected Response logAndRebufferResponse(String configKey,
+ Level logLevel,
+ Response response,
+ long elapsedTime)
+ throws IOException {
+ String protocolVersion = resolveProtocolVersion(response.protocolVersion());
+ String reason =
+ response.reason() != null && logLevel.compareTo(Level.NONE) > 0 ? " " + response.reason()
+ : "";
+ int status = response.status();
+ log(configKey, "<--- %s %s%s (%sms)", protocolVersion, status, reason, elapsedTime);
+ if (logLevel.ordinal() >= Level.HEADERS.ordinal()) {
+
+ for (String field : response.headers().keySet()) {
+ if (shouldLogResponseHeader(field)) {
+ for (String value : valuesOrEmpty(response.headers(), field)) {
+ log(configKey, "%s: %s", field, value);
+ }
+ }
+ }
+
+ int bodyLength = 0;
+ if (response.body() != null && !(status == 204 || status == 205)) {
+ // HTTP 204 No Content "...response MUST NOT include a message-body"
+ // HTTP 205 Reset Content "...response MUST NOT include an entity"
+ if (logLevel.ordinal() >= Level.FULL.ordinal()) {
+ log(configKey, ""); // CRLF
+ }
+ byte[] bodyData = Util.toByteArray(response.body().asInputStream());
+ bodyLength = bodyData.length;
+ if (logLevel.ordinal() >= Level.FULL.ordinal() && bodyLength > 0) {
+ log(configKey, "%s", decodeOrDefault(bodyData, UTF_8, "Binary data"));
+ }
+ log(configKey, "<--- END HTTP (%s-byte body)", bodyLength);
+ return response.toBuilder().body(bodyData).build();
+ } else {
+ log(configKey, "<--- END HTTP (%s-byte body)", bodyLength);
+ }
+ }
+ return response;
+ }
+
+ protected IOException logIOException(String configKey,
+ Level logLevel,
+ IOException ioe,
+ long elapsedTime) {
+ log(configKey, "<--- ERROR %s: %s (%sms)", ioe.getClass().getSimpleName(), ioe.getMessage(),
+ elapsedTime);
+ if (logLevel.ordinal() >= Level.FULL.ordinal()) {
+ StringWriter sw = new StringWriter();
+ ioe.printStackTrace(new PrintWriter(sw));
+ log(configKey, "%s", sw.toString());
+ log(configKey, "<--- END ERROR");
+ }
+ return ioe;
+ }
+
+ protected static String resolveProtocolVersion(Request.ProtocolVersion protocolVersion) {
+ if (nonNull(protocolVersion)) {
+ return protocolVersion.toString();
+ }
+ return "UNKNOWN";
+ }
+
+ /**
+ * Controls the level of logging.
+ */
+ public enum Level {
+ /**
+ * No logging.
+ */
+ NONE,
+ /**
+ * Log only the request method and URL and the response status code and execution time.
+ */
+ BASIC,
+ /**
+ * Log the basic information along with request and response headers.
+ */
+ HEADERS,
+ /**
+ * Log the headers, body, and metadata for both requests and responses.
+ */
+ FULL
+ }
+
+ /**
+ * Logs to System.err.
+ */
+ public static class ErrorLogger extends Logger {
+ @Override
+ protected void log(String configKey, String format, Object... args) {
+ System.err.printf(methodTag(configKey) + format + "%n", args);
+ }
+ }
+
+ /**
+ * Logs to the category {@link Logger} at {@link java.util.logging.Level#FINE}, if loggable.
+ */
+ public static class JavaLogger extends Logger {
+
+ final java.util.logging.Logger logger;
+
+ /**
+ * @deprecated Use {@link #JavaLogger(String)} or {@link #JavaLogger(Class)} instead.
+ *
+ * This constructor can be used to create just one logger. Example =
+ * {@code Logger.JavaLogger().appendToFile("logs/first.log")}
+ *
+ * If you create multiple loggers for multiple clients and provide different files
+ * to write log - you'll have unexpected behavior - all clients will write same log
+ * to each file.
+ *
+ * That's why this constructor will be removed in future.
+ */
+ @Deprecated
+ public JavaLogger() {
+ logger = java.util.logging.Logger.getLogger(Logger.class.getName());
+ }
+
+ /**
+ * Constructor for JavaLogger class
+ *
+ * @param loggerName a name for the logger. This should be a dot-separated name and should
+ * normally be based on the package name or class name of the subsystem, such as java.net
+ * or javax.swing
+ */
+ public JavaLogger(String loggerName) {
+ logger = java.util.logging.Logger.getLogger(loggerName);
+ }
+
+ /**
+ * Constructor for JavaLogger class
+ *
+ * @param clazz the returned logger will be named after clazz
+ */
+ public JavaLogger(Class> clazz) {
+ logger = java.util.logging.Logger.getLogger(clazz.getName());
+ }
+
+ @Override
+ protected void logRequest(String configKey, Level logLevel, Request request) {
+ if (logger.isLoggable(java.util.logging.Level.FINE)) {
+ super.logRequest(configKey, logLevel, request);
+ }
+ }
+
+ @Override
+ protected Response logAndRebufferResponse(String configKey,
+ Level logLevel,
+ Response response,
+ long elapsedTime)
+ throws IOException {
+ if (logger.isLoggable(java.util.logging.Level.FINE)) {
+ return super.logAndRebufferResponse(configKey, logLevel, response, elapsedTime);
+ }
+ return response;
+ }
+
+ @Override
+ protected void log(String configKey, String format, Object... args) {
+ if (logger.isLoggable(java.util.logging.Level.FINE)) {
+ logger.fine(String.format(methodTag(configKey) + format, args));
+ }
+ }
+
+ /**
+ * Helper that configures java.util.logging to sanely log messages at FINE level without
+ * additional formatting.
+ */
+ public JavaLogger appendToFile(String logfile) {
+ logger.setLevel(java.util.logging.Level.FINE);
+ try {
+ FileHandler handler = new FileHandler(logfile, true);
+ handler.setFormatter(new SimpleFormatter() {
+ @Override
+ public String format(LogRecord record) {
+ return String.format("%s%n", record.getMessage()); // NOPMD
+ }
+ });
+ logger.addHandler(handler);
+ } catch (IOException e) {
+ throw new IllegalStateException("Could not add file handler.", e);
+ }
+ return this;
+ }
+ }
+
+ public static class NoOpLogger extends Logger {
+
+ @Override
+ protected void logRequest(String configKey, Level logLevel, Request request) {}
+
+ @Override
+ protected Response logAndRebufferResponse(String configKey,
+ Level logLevel,
+ Response response,
+ long elapsedTime)
+ throws IOException {
+ return response;
+ }
+
+ @Override
+ protected void log(String configKey, String format, Object... args) {}
+ }
+}
diff --git a/core/src/main/java/feign/MethodInfo.java b/core/src/main/java/feign/MethodInfo.java
new file mode 100644
index 0000000000..608e03c2a8
--- /dev/null
+++ b/core/src/main/java/feign/MethodInfo.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2012-2022 The Feign Authors
+ *
+ * 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 java.lang.reflect.Method;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.util.concurrent.CompletableFuture;
+
+@Experimental
+class MethodInfo {
+ private final String configKey;
+ private final Type underlyingReturnType;
+ private final boolean asyncReturnType;
+
+ MethodInfo(String configKey, Type underlyingReturnType, boolean asyncReturnType) {
+ this.configKey = configKey;
+ this.underlyingReturnType = underlyingReturnType;
+ this.asyncReturnType = asyncReturnType;
+ }
+
+ MethodInfo(Class> targetType, Method method) {
+ this.configKey = Feign.configKey(targetType, method);
+
+ final Type type = Types.resolve(targetType, targetType, method.getGenericReturnType());
+
+ if (type instanceof ParameterizedType
+ && Types.getRawType(type).isAssignableFrom(CompletableFuture.class)) {
+ this.asyncReturnType = true;
+ this.underlyingReturnType = ((ParameterizedType) type).getActualTypeArguments()[0];
+ } else {
+ this.asyncReturnType = false;
+ this.underlyingReturnType = type;
+ }
+ }
+
+ String configKey() {
+ return configKey;
+ }
+
+ Type underlyingReturnType() {
+ return underlyingReturnType;
+ }
+
+ boolean isAsyncReturnType() {
+ return asyncReturnType;
+ }
+}
diff --git a/core/src/main/java/feign/MethodMetadata.java b/core/src/main/java/feign/MethodMetadata.java
new file mode 100644
index 0000000000..1962f459bd
--- /dev/null
+++ b/core/src/main/java/feign/MethodMetadata.java
@@ -0,0 +1,256 @@
+/*
+ * Copyright 2012-2022 The Feign Authors
+ *
+ * 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 java.io.Serializable;
+import java.lang.reflect.Method;
+import java.lang.reflect.Type;
+import java.util.*;
+import java.util.stream.Collectors;
+import feign.Param.Expander;
+
+public final class MethodMetadata implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+ private String configKey;
+ private transient Type returnType;
+ private Integer urlIndex;
+ private Integer bodyIndex;
+ private Integer headerMapIndex;
+ private Integer queryMapIndex;
+ private boolean alwaysEncodeBody;
+ private transient Type bodyType;
+ private final RequestTemplate template = new RequestTemplate();
+ private final List formParams = new ArrayList();
+ private final Map> indexToName =
+ new LinkedHashMap>();
+ private final Map> indexToExpanderClass =
+ new LinkedHashMap>();
+ private final Map indexToEncoded = new LinkedHashMap();
+ private transient Map indexToExpander;
+ private BitSet parameterToIgnore = new BitSet();
+ private boolean ignored;
+ private transient Class> targetType;
+ private transient Method method;
+ private transient final List warnings = new ArrayList<>();
+
+ MethodMetadata() {
+ template.methodMetadata(this);
+ }
+
+ /**
+ * Used as a reference to this method. For example, {@link Logger#log(String, String, Object...)
+ * logging} or {@link ReflectiveFeign reflective dispatch}.
+ *
+ * @see Feign#configKey(Class, java.lang.reflect.Method)
+ */
+ public String configKey() {
+ return configKey;
+ }
+
+ public MethodMetadata configKey(String configKey) {
+ this.configKey = configKey;
+ return this;
+ }
+
+ public Type returnType() {
+ return returnType;
+ }
+
+ public MethodMetadata returnType(Type returnType) {
+ this.returnType = returnType;
+ return this;
+ }
+
+ public Integer urlIndex() {
+ return urlIndex;
+ }
+
+ public MethodMetadata urlIndex(Integer urlIndex) {
+ this.urlIndex = urlIndex;
+ return this;
+ }
+
+ public Integer bodyIndex() {
+ return bodyIndex;
+ }
+
+ public MethodMetadata bodyIndex(Integer bodyIndex) {
+ this.bodyIndex = bodyIndex;
+ return this;
+ }
+
+ public Integer headerMapIndex() {
+ return headerMapIndex;
+ }
+
+ public MethodMetadata headerMapIndex(Integer headerMapIndex) {
+ this.headerMapIndex = headerMapIndex;
+ return this;
+ }
+
+ public Integer queryMapIndex() {
+ return queryMapIndex;
+ }
+
+ public MethodMetadata queryMapIndex(Integer queryMapIndex) {
+ this.queryMapIndex = queryMapIndex;
+ return this;
+ }
+
+ @Experimental
+ public boolean alwaysEncodeBody() {
+ return alwaysEncodeBody;
+ }
+
+ @Experimental
+ MethodMetadata alwaysEncodeBody(boolean alwaysEncodeBody) {
+ this.alwaysEncodeBody = alwaysEncodeBody;
+ return this;
+ }
+
+ /**
+ * Type corresponding to {@link #bodyIndex()}.
+ */
+ public Type bodyType() {
+ return bodyType;
+ }
+
+ public MethodMetadata bodyType(Type bodyType) {
+ this.bodyType = bodyType;
+ return this;
+ }
+
+ public RequestTemplate template() {
+ return template;
+ }
+
+ public List formParams() {
+ return formParams;
+ }
+
+ public Map> indexToName() {
+ return indexToName;
+ }
+
+ public Map indexToEncoded() {
+ return indexToEncoded;
+ }
+
+ /**
+ * If {@link #indexToExpander} is null, classes here will be instantiated by newInstance.
+ */
+ public Map> indexToExpanderClass() {
+ return indexToExpanderClass;
+ }
+
+ /**
+ * After {@link #indexToExpanderClass} is populated, this is set by contracts that support runtime
+ * injection.
+ */
+ public MethodMetadata indexToExpander(Map indexToExpander) {
+ this.indexToExpander = indexToExpander;
+ return this;
+ }
+
+ /**
+ * When not null, this value will be used instead of {@link #indexToExpander()}.
+ */
+ public Map indexToExpander() {
+ return indexToExpander;
+ }
+
+ /**
+ * @param i individual parameter that should be ignored
+ * @return this instance
+ */
+ public MethodMetadata ignoreParamater(int i) {
+ this.parameterToIgnore.set(i);
+ return this;
+ }
+
+ public BitSet parameterToIgnore() {
+ return parameterToIgnore;
+ }
+
+ public MethodMetadata parameterToIgnore(BitSet parameterToIgnore) {
+ this.parameterToIgnore = parameterToIgnore;
+ return this;
+ }
+
+ /**
+ * @param i individual parameter to check if should be ignored
+ * @return true when field should not be processed by feign
+ */
+ public boolean shouldIgnoreParamater(int i) {
+ return parameterToIgnore.get(i);
+ }
+
+ /**
+ * @param index
+ * @return true if the parameter {@code index} was already consumed by a any
+ * {@link MethodMetadata} holder
+ */
+ public boolean isAlreadyProcessed(Integer index) {
+ return index.equals(urlIndex)
+ || index.equals(bodyIndex)
+ || index.equals(headerMapIndex)
+ || index.equals(queryMapIndex)
+ || indexToName.containsKey(index)
+ || indexToExpanderClass.containsKey(index)
+ || indexToEncoded.containsKey(index)
+ || (indexToExpander != null && indexToExpander.containsKey(index))
+ || parameterToIgnore.get(index);
+ }
+
+ public void ignoreMethod() {
+ this.ignored = true;
+ }
+
+ public boolean isIgnored() {
+ return ignored;
+ }
+
+ @Experimental
+ public MethodMetadata targetType(Class> targetType) {
+ this.targetType = targetType;
+ return this;
+ }
+
+ @Experimental
+ public Class> targetType() {
+ return targetType;
+ }
+
+ @Experimental
+ public MethodMetadata method(Method method) {
+ this.method = method;
+ return this;
+ }
+
+ @Experimental
+ public Method method() {
+ return method;
+ }
+
+ public void addWarning(String warning) {
+ warnings.add(warning);
+ }
+
+ public String warnings() {
+ return warnings.stream()
+ .collect(Collectors.joining("\n- ", "\nWarnings:\n- ", ""));
+ }
+
+}
diff --git a/core/src/main/java/feign/Param.java b/core/src/main/java/feign/Param.java
new file mode 100644
index 0000000000..6789d9ed3a
--- /dev/null
+++ b/core/src/main/java/feign/Param.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2012-2022 The Feign Authors
+ *
+ * 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 java.lang.annotation.Retention;
+import static java.lang.annotation.ElementType.*;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+/**
+ * A named template parameter applied to {@link Headers}, {@linkplain RequestLine},
+ * {@linkplain Body}, POJO fields or beans properties when it expanding
+ */
+@Retention(RUNTIME)
+@java.lang.annotation.Target({PARAMETER, FIELD, METHOD})
+public @interface Param {
+
+ /**
+ * The name of the template parameter.
+ */
+ String value() default "";
+
+ /**
+ * How to expand the value of this parameter, if {@link ToStringExpander} isn't adequate.
+ */
+ Class extends Expander> expander() default ToStringExpander.class;
+
+ /**
+ * {@code encoded} has been maintained for backward compatibility and should be deprecated. We no
+ * longer need it as values that are already pct-encoded should be identified during expansion and
+ * passed through without any changes
+ *
+ * @see QueryMap#encoded
+ * @deprecated
+ */
+ boolean encoded() default false;
+
+ interface Expander {
+
+ /**
+ * Expands the value into a string. Does not accept or return null.
+ */
+ String expand(Object value);
+ }
+
+ final class ToStringExpander implements Expander {
+
+ @Override
+ public String expand(Object value) {
+ return value.toString();
+ }
+ }
+}
diff --git a/core/src/main/java/feign/QueryMap.java b/core/src/main/java/feign/QueryMap.java
new file mode 100644
index 0000000000..a2a5588d69
--- /dev/null
+++ b/core/src/main/java/feign/QueryMap.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2012-2022 The Feign Authors
+ *
+ * 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 java.lang.annotation.Retention;
+import java.util.List;
+import java.util.Map;
+import static java.lang.annotation.ElementType.PARAMETER;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+/**
+ * A template parameter that can be applied to a Map that contains query parameters, where the keys
+ * are Strings that are the parameter names and the values are the parameter values. The queries
+ * specified by the map will be applied to the request after all other processing, and will take
+ * precedence over any previously specified query parameters. It is not necessary to reference the
+ * parameter map as a variable.
+ *
+ *
+ *
+ *
+ * The annotated parameter must be an instance of {@link Map}, and the keys must be Strings. The
+ * query value of a key will be the value of its toString method, except in the following cases:
+ *
+ *
+ *
+ *
if the value is null, the value will remain null (rather than converting to the String
+ * "null")
+ *
if the value is an {@link Iterable}, it is converted to a {@link List} of String objects
+ * where each value in the list is either null if the original value was null or the value's
+ * toString representation otherwise.
+ *
+ *
+ * Once this conversion is applied, the query keys and resulting String values follow the same
+ * contract as if they were set using {@link RequestTemplate#query(String, String...)}.
+ */
+@Retention(RUNTIME)
+@java.lang.annotation.Target(PARAMETER)
+public @interface QueryMap {
+
+ /**
+ * Specifies whether parameter names and values are already encoded.
+ *
+ * Deprecation: there are two options
+ *
+ *
if name or value are already encoded we do nothing;
+ *
if name or value are not encoded we encode them.
+ *
+ *
+ * @see Param#encoded
+ * @deprecated
+ */
+ boolean encoded() default false;
+}
diff --git a/core/src/main/java/feign/QueryMapEncoder.java b/core/src/main/java/feign/QueryMapEncoder.java
new file mode 100644
index 0000000000..0eff49339b
--- /dev/null
+++ b/core/src/main/java/feign/QueryMapEncoder.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2012-2022 The Feign Authors
+ *
+ * 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 feign.querymap.FieldQueryMapEncoder;
+import feign.querymap.BeanQueryMapEncoder;
+import java.util.Map;
+
+/**
+ * A QueryMapEncoder encodes Objects into maps of query parameter names to values.
+ *
+ * @see FieldQueryMapEncoder
+ * @see BeanQueryMapEncoder
+ *
+ */
+public interface QueryMapEncoder {
+
+ /**
+ * Encodes the given object into a query map.
+ *
+ * @param object the object to encode
+ * @return the map represented by the object
+ */
+ Map encode(Object object);
+
+ /**
+ * @deprecated use {@link BeanQueryMapEncoder} instead. default encoder uses reflection to inspect
+ * provided objects Fields to expand the objects values into a query string. If you
+ * prefer that the query string be built using getter and setter methods, as defined
+ * in the Java Beans API, please use the {@link BeanQueryMapEncoder}
+ */
+ class Default extends FieldQueryMapEncoder {
+ }
+}
diff --git a/core/src/main/java/feign/ReflectiveAsyncFeign.java b/core/src/main/java/feign/ReflectiveAsyncFeign.java
new file mode 100644
index 0000000000..1fc4e1306c
--- /dev/null
+++ b/core/src/main/java/feign/ReflectiveAsyncFeign.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright 2012-2022 The Feign Authors
+ *
+ * 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 java.lang.reflect.InvocationHandler;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Proxy;
+import java.lang.reflect.Type;
+import java.lang.reflect.WildcardType;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ConcurrentHashMap;
+
+@Experimental
+public class ReflectiveAsyncFeign extends AsyncFeign {
+
+ private class AsyncFeignInvocationHandler implements InvocationHandler {
+
+ private final Map methodInfoLookup = new ConcurrentHashMap<>();
+
+ private final Class type;
+ private final T instance;
+ private final C context;
+
+ AsyncFeignInvocationHandler(Class type, T instance, C context) {
+ this.type = type;
+ this.instance = instance;
+ this.context = context;
+ }
+
+ @Override
+ public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
+ if ("equals".equals(method.getName()) && method.getParameterCount() == 1) {
+ try {
+ final Object otherHandler =
+ args.length > 0 && args[0] != null ? Proxy.getInvocationHandler(args[0])
+ : null;
+ return equals(otherHandler);
+ } catch (final IllegalArgumentException e) {
+ return false;
+ }
+ } else if ("hashCode".equals(method.getName()) && method.getParameterCount() == 0) {
+ return hashCode();
+ } else if ("toString".equals(method.getName()) && method.getParameterCount() == 0) {
+ return toString();
+ }
+
+ final MethodInfo methodInfo =
+ methodInfoLookup.computeIfAbsent(method, m -> new MethodInfo(type, m));
+
+ setInvocationContext(new AsyncInvocation<>(context, methodInfo));
+ try {
+ return method.invoke(instance, args);
+ } catch (final InvocationTargetException e) {
+ Throwable cause = e.getCause();
+ if (cause instanceof AsyncJoinException) {
+ cause = cause.getCause();
+ }
+ throw cause;
+ } finally {
+ clearInvocationContext();
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof AsyncFeignInvocationHandler) {
+ final AsyncFeignInvocationHandler> other = (AsyncFeignInvocationHandler>) obj;
+ return instance.equals(other.instance);
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return instance.hashCode();
+ }
+
+ @Override
+ public String toString() {
+ return instance.toString();
+ }
+ }
+
+ private ThreadLocal> activeContextHolder;
+
+ public ReflectiveAsyncFeign(Feign feign, AsyncContextSupplier defaultContextSupplier,
+ ThreadLocal> contextHolder) {
+ super(feign, defaultContextSupplier);
+ this.activeContextHolder = contextHolder;
+ }
+
+ protected void setInvocationContext(AsyncInvocation invocationContext) {
+ activeContextHolder.set(invocationContext);
+ }
+
+ protected void clearInvocationContext() {
+ activeContextHolder.remove();
+ }
+
+
+ private String getFullMethodName(Class> type, Type retType, Method m) {
+ return retType.getTypeName() + " " + type.toGenericString() + "." + m.getName();
+ }
+
+ @Override
+ protected T wrap(Class type, T instance, C context) {
+ if (!type.isInterface()) {
+ throw new IllegalArgumentException("Type must be an interface: " + type);
+ }
+
+ for (final Method m : type.getMethods()) {
+ final Class> retType = m.getReturnType();
+
+ if (!CompletableFuture.class.isAssignableFrom(retType)) {
+ continue; // synchronous case
+ }
+
+ if (retType != CompletableFuture.class) {
+ throw new IllegalArgumentException("Method return type is not CompleteableFuture: "
+ + getFullMethodName(type, retType, m));
+ }
+
+ final Type genRetType = m.getGenericReturnType();
+
+ if (!ParameterizedType.class.isInstance(genRetType)) {
+ throw new IllegalArgumentException("Method return type is not parameterized: "
+ + getFullMethodName(type, genRetType, m));
+ }
+
+ if (WildcardType.class
+ .isInstance(ParameterizedType.class.cast(genRetType).getActualTypeArguments()[0])) {
+ throw new IllegalArgumentException(
+ "Wildcards are not supported for return-type parameters: "
+ + getFullMethodName(type, genRetType, m));
+ }
+ }
+
+ return type.cast(Proxy.newProxyInstance(type.getClassLoader(), new Class>[] {type},
+ new AsyncFeignInvocationHandler<>(type, instance, context)));
+ }
+}
diff --git a/core/src/main/java/feign/ReflectiveFeign.java b/core/src/main/java/feign/ReflectiveFeign.java
new file mode 100644
index 0000000000..ae561cf488
--- /dev/null
+++ b/core/src/main/java/feign/ReflectiveFeign.java
@@ -0,0 +1,407 @@
+/*
+ * Copyright 2012-2022 The Feign Authors
+ *
+ * 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 feign.Util.checkArgument;
+import static feign.Util.checkNotNull;
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+import java.util.*;
+import java.util.Map.Entry;
+import feign.InvocationHandlerFactory.MethodHandler;
+import feign.Param.Expander;
+import feign.Request.Options;
+import feign.codec.*;
+import feign.template.UriUtils;
+
+public class ReflectiveFeign extends Feign {
+
+ private final ParseHandlersByName targetToHandlersByName;
+ private final InvocationHandlerFactory factory;
+ private final QueryMapEncoder queryMapEncoder;
+
+ ReflectiveFeign(ParseHandlersByName targetToHandlersByName, InvocationHandlerFactory factory,
+ QueryMapEncoder queryMapEncoder) {
+ this.targetToHandlersByName = targetToHandlersByName;
+ this.factory = factory;
+ this.queryMapEncoder = queryMapEncoder;
+ }
+
+ /**
+ * creates an api binding to the {@code target}. As this invokes reflection, care should be taken
+ * to cache the result.
+ */
+ @SuppressWarnings("unchecked")
+ @Override
+ public T newInstance(Target target) {
+ Map nameToHandler = targetToHandlersByName.apply(target);
+ Map methodToHandler = new LinkedHashMap();
+ List defaultMethodHandlers = new LinkedList();
+
+ for (Method method : target.type().getMethods()) {
+ if (method.getDeclaringClass() == Object.class) {
+ continue;
+ } else if (Util.isDefault(method)) {
+ DefaultMethodHandler handler = new DefaultMethodHandler(method);
+ defaultMethodHandlers.add(handler);
+ methodToHandler.put(method, handler);
+ } else {
+ methodToHandler.put(method, nameToHandler.get(Feign.configKey(target.type(), method)));
+ }
+ }
+ InvocationHandler handler = factory.create(target, methodToHandler);
+ T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(),
+ new Class>[] {target.type()}, handler);
+
+ for (DefaultMethodHandler defaultMethodHandler : defaultMethodHandlers) {
+ defaultMethodHandler.bindTo(proxy);
+ }
+ return proxy;
+ }
+
+ static class FeignInvocationHandler implements InvocationHandler {
+
+ private final Target target;
+ private final Map dispatch;
+
+ FeignInvocationHandler(Target target, Map dispatch) {
+ this.target = checkNotNull(target, "target");
+ this.dispatch = checkNotNull(dispatch, "dispatch for %s", target);
+ }
+
+ @Override
+ public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
+ if ("equals".equals(method.getName())) {
+ try {
+ Object otherHandler =
+ args.length > 0 && args[0] != null ? Proxy.getInvocationHandler(args[0]) : null;
+ return equals(otherHandler);
+ } catch (IllegalArgumentException e) {
+ return false;
+ }
+ } else if ("hashCode".equals(method.getName())) {
+ return hashCode();
+ } else if ("toString".equals(method.getName())) {
+ return toString();
+ }
+
+ return dispatch.get(method).invoke(args);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof FeignInvocationHandler) {
+ FeignInvocationHandler other = (FeignInvocationHandler) obj;
+ return target.equals(other.target);
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return target.hashCode();
+ }
+
+ @Override
+ public String toString() {
+ return target.toString();
+ }
+ }
+
+ static final class ParseHandlersByName {
+
+ private final Contract contract;
+ private final Options options;
+ private final Encoder encoder;
+ private final Decoder decoder;
+ private final ErrorDecoder errorDecoder;
+ private final QueryMapEncoder queryMapEncoder;
+ private final SynchronousMethodHandler.Factory factory;
+
+ ParseHandlersByName(
+ Contract contract,
+ Options options,
+ Encoder encoder,
+ Decoder decoder,
+ QueryMapEncoder queryMapEncoder,
+ ErrorDecoder errorDecoder,
+ SynchronousMethodHandler.Factory factory) {
+ this.contract = contract;
+ this.options = options;
+ this.factory = factory;
+ this.errorDecoder = errorDecoder;
+ this.queryMapEncoder = queryMapEncoder;
+ this.encoder = checkNotNull(encoder, "encoder");
+ this.decoder = checkNotNull(decoder, "decoder");
+ }
+
+ public Map apply(Target target) {
+ List metadata = contract.parseAndValidateMetadata(target.type());
+ Map result = new LinkedHashMap();
+ for (MethodMetadata md : metadata) {
+ BuildTemplateByResolvingArgs buildTemplate;
+ if (!md.formParams().isEmpty() && md.template().bodyTemplate() == null) {
+ buildTemplate =
+ new BuildFormEncodedTemplateFromArgs(md, encoder, queryMapEncoder, target);
+ } else if (md.bodyIndex() != null || md.alwaysEncodeBody()) {
+ buildTemplate = new BuildEncodedTemplateFromArgs(md, encoder, queryMapEncoder, target);
+ } else {
+ buildTemplate = new BuildTemplateByResolvingArgs(md, queryMapEncoder, target);
+ }
+ if (md.isIgnored()) {
+ result.put(md.configKey(), args -> {
+ throw new IllegalStateException(md.configKey() + " is not a method handled by feign");
+ });
+ } else {
+ result.put(md.configKey(),
+ factory.create(target, md, buildTemplate, options, decoder, errorDecoder));
+ }
+ }
+ return result;
+ }
+ }
+
+ private static class BuildTemplateByResolvingArgs implements RequestTemplate.Factory {
+
+ private final QueryMapEncoder queryMapEncoder;
+
+ protected final MethodMetadata metadata;
+ protected final Target> target;
+ private final Map indexToExpander = new LinkedHashMap();
+
+ private BuildTemplateByResolvingArgs(MethodMetadata metadata, QueryMapEncoder queryMapEncoder,
+ Target target) {
+ this.metadata = metadata;
+ this.target = target;
+ this.queryMapEncoder = queryMapEncoder;
+ if (metadata.indexToExpander() != null) {
+ indexToExpander.putAll(metadata.indexToExpander());
+ return;
+ }
+ if (metadata.indexToExpanderClass().isEmpty()) {
+ return;
+ }
+ for (Entry> indexToExpanderClass : metadata
+ .indexToExpanderClass().entrySet()) {
+ try {
+ indexToExpander
+ .put(indexToExpanderClass.getKey(), indexToExpanderClass.getValue().newInstance());
+ } catch (InstantiationException e) {
+ throw new IllegalStateException(e);
+ } catch (IllegalAccessException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+ }
+
+ @Override
+ public RequestTemplate create(Object[] argv) {
+ RequestTemplate mutable = RequestTemplate.from(metadata.template());
+ mutable.feignTarget(target);
+ if (metadata.urlIndex() != null) {
+ int urlIndex = metadata.urlIndex();
+ checkArgument(argv[urlIndex] != null, "URI parameter %s was null", urlIndex);
+ mutable.target(String.valueOf(argv[urlIndex]));
+ }
+ Map varBuilder = new LinkedHashMap();
+ for (Entry> entry : metadata.indexToName().entrySet()) {
+ int i = entry.getKey();
+ Object value = argv[entry.getKey()];
+ if (value != null) { // Null values are skipped.
+ if (indexToExpander.containsKey(i)) {
+ value = expandElements(indexToExpander.get(i), value);
+ }
+ for (String name : entry.getValue()) {
+ varBuilder.put(name, value);
+ }
+ }
+ }
+
+ RequestTemplate template = resolve(argv, mutable, varBuilder);
+ if (metadata.queryMapIndex() != null) {
+ // add query map parameters after initial resolve so that they take
+ // precedence over any predefined values
+ Object value = argv[metadata.queryMapIndex()];
+ Map queryMap = toQueryMap(value);
+ template = addQueryMapQueryParameters(queryMap, template);
+ }
+
+ if (metadata.headerMapIndex() != null) {
+ // add header map parameters for a resolution of the user pojo object
+ Object value = argv[metadata.headerMapIndex()];
+ Map headerMap = toQueryMap(value);
+ template = addHeaderMapHeaders(headerMap, template);
+ }
+
+ return template;
+ }
+
+ private Map toQueryMap(Object value) {
+ if (value instanceof Map) {
+ return (Map) value;
+ }
+ try {
+ return queryMapEncoder.encode(value);
+ } catch (EncodeException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ private Object expandElements(Expander expander, Object value) {
+ if (value instanceof Iterable) {
+ return expandIterable(expander, (Iterable) value);
+ }
+ return expander.expand(value);
+ }
+
+ private List expandIterable(Expander expander, Iterable value) {
+ List values = new ArrayList();
+ for (Object element : value) {
+ if (element != null) {
+ values.add(expander.expand(element));
+ }
+ }
+ return values;
+ }
+
+ @SuppressWarnings("unchecked")
+ private RequestTemplate addHeaderMapHeaders(Map headerMap,
+ RequestTemplate mutable) {
+ for (Entry currEntry : headerMap.entrySet()) {
+ Collection values = new ArrayList();
+
+ Object currValue = currEntry.getValue();
+ if (currValue instanceof Iterable>) {
+ Iterator> iter = ((Iterable>) currValue).iterator();
+ while (iter.hasNext()) {
+ Object nextObject = iter.next();
+ values.add(nextObject == null ? null : nextObject.toString());
+ }
+ } else {
+ values.add(currValue == null ? null : currValue.toString());
+ }
+
+ mutable.header(currEntry.getKey(), values);
+ }
+ return mutable;
+ }
+
+ @SuppressWarnings("unchecked")
+ private RequestTemplate addQueryMapQueryParameters(Map queryMap,
+ RequestTemplate mutable) {
+ for (Entry currEntry : queryMap.entrySet()) {
+ Collection values = new ArrayList();
+
+ Object currValue = currEntry.getValue();
+ if (currValue instanceof Iterable>) {
+ Iterator> iter = ((Iterable>) currValue).iterator();
+ while (iter.hasNext()) {
+ Object nextObject = iter.next();
+ values.add(nextObject == null ? null : UriUtils.encode(nextObject.toString()));
+ }
+ } else if (currValue instanceof Object[]) {
+ for (Object value : (Object[]) currValue) {
+ values.add(value == null ? null : UriUtils.encode(value.toString()));
+ }
+ } else {
+ if (currValue != null) {
+ values.add(UriUtils.encode(currValue.toString()));
+ }
+ }
+
+ if (values.size() > 0) {
+ mutable.query(UriUtils.encode(currEntry.getKey()), values);
+ }
+ }
+ return mutable;
+ }
+
+ protected RequestTemplate resolve(Object[] argv,
+ RequestTemplate mutable,
+ Map variables) {
+ return mutable.resolve(variables);
+ }
+ }
+
+ private static class BuildFormEncodedTemplateFromArgs extends BuildTemplateByResolvingArgs {
+
+ private final Encoder encoder;
+
+ private BuildFormEncodedTemplateFromArgs(MethodMetadata metadata, Encoder encoder,
+ QueryMapEncoder queryMapEncoder, Target target) {
+ super(metadata, queryMapEncoder, target);
+ this.encoder = encoder;
+ }
+
+ @Override
+ protected RequestTemplate resolve(Object[] argv,
+ RequestTemplate mutable,
+ Map variables) {
+ Map formVariables = new LinkedHashMap();
+ for (Entry entry : variables.entrySet()) {
+ if (metadata.formParams().contains(entry.getKey())) {
+ formVariables.put(entry.getKey(), entry.getValue());
+ }
+ }
+ try {
+ encoder.encode(formVariables, Encoder.MAP_STRING_WILDCARD, mutable);
+ } catch (EncodeException e) {
+ throw e;
+ } catch (RuntimeException e) {
+ throw new EncodeException(e.getMessage(), e);
+ }
+ return super.resolve(argv, mutable, variables);
+ }
+ }
+
+ private static class BuildEncodedTemplateFromArgs extends BuildTemplateByResolvingArgs {
+
+ private final Encoder encoder;
+
+ private BuildEncodedTemplateFromArgs(MethodMetadata metadata, Encoder encoder,
+ QueryMapEncoder queryMapEncoder, Target target) {
+ super(metadata, queryMapEncoder, target);
+ this.encoder = encoder;
+ }
+
+ @Override
+ protected RequestTemplate resolve(Object[] argv,
+ RequestTemplate mutable,
+ Map variables) {
+
+ boolean alwaysEncodeBody = mutable.methodMetadata().alwaysEncodeBody();
+
+ Object body = null;
+ if (!alwaysEncodeBody) {
+ body = argv[metadata.bodyIndex()];
+ checkArgument(body != null, "Body parameter %s was null", metadata.bodyIndex());
+ }
+
+ try {
+ if (alwaysEncodeBody) {
+ body = argv == null ? new Object[0] : argv;
+ encoder.encode(body, Object[].class, mutable);
+ } else {
+ encoder.encode(body, metadata.bodyType(), mutable);
+ }
+ } catch (EncodeException e) {
+ throw e;
+ } catch (RuntimeException e) {
+ throw new EncodeException(e.getMessage(), e);
+ }
+ return super.resolve(argv, mutable, variables);
+ }
+ }
+}
diff --git a/core/src/main/java/feign/Request.java b/core/src/main/java/feign/Request.java
new file mode 100644
index 0000000000..c9987d10f7
--- /dev/null
+++ b/core/src/main/java/feign/Request.java
@@ -0,0 +1,485 @@
+/*
+ * Copyright 2012-2022 The Feign Authors
+ *
+ * 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 java.io.Serializable;
+import java.net.HttpURLConnection;
+import java.nio.charset.Charset;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.TimeUnit;
+import static feign.Util.checkNotNull;
+import static feign.Util.valuesOrEmpty;
+
+/**
+ * An immutable request to an http server.
+ */
+public final class Request implements Serializable {
+
+ public enum HttpMethod {
+ GET, HEAD, POST, PUT, DELETE, CONNECT, OPTIONS, TRACE, PATCH
+ }
+
+ public enum ProtocolVersion {
+ HTTP_1_0("HTTP/1.0"), HTTP_1_1("HTTP/1.1"), HTTP_2("HTTP/2.0"), MOCK;
+
+ final String protocolVersion;
+
+ ProtocolVersion() {
+ protocolVersion = name();
+ }
+
+ ProtocolVersion(String protocolVersion) {
+ this.protocolVersion = protocolVersion;
+ }
+
+ @Override
+ public String toString() {
+ return protocolVersion;
+ }
+ }
+
+ /**
+ * No parameters can be null except {@code body} and {@code charset}. All parameters must be
+ * effectively immutable, via safe copies, not mutating or otherwise.
+ *
+ * @deprecated {@link #create(HttpMethod, String, Map, byte[], Charset)}
+ */
+ @Deprecated
+ public static Request create(String method,
+ String url,
+ Map> headers,
+ byte[] body,
+ Charset charset) {
+ checkNotNull(method, "httpMethod of %s", method);
+ final HttpMethod httpMethod = HttpMethod.valueOf(method.toUpperCase());
+ return create(httpMethod, url, headers, body, charset, null);
+ }
+
+ /**
+ * Builds a Request. All parameters must be effectively immutable, via safe copies.
+ *
+ * @param httpMethod for the request.
+ * @param url for the request.
+ * @param headers to include.
+ * @param body of the request, can be {@literal null}
+ * @param charset of the request, can be {@literal null}
+ * @return a Request
+ */
+ @Deprecated
+ public static Request create(HttpMethod httpMethod,
+ String url,
+ Map> headers,
+ byte[] body,
+ Charset charset) {
+ return create(httpMethod, url, headers, Body.create(body, charset), null);
+ }
+
+ /**
+ * Builds a Request. All parameters must be effectively immutable, via safe copies.
+ *
+ * @param httpMethod for the request.
+ * @param url for the request.
+ * @param headers to include.
+ * @param body of the request, can be {@literal null}
+ * @param charset of the request, can be {@literal null}
+ * @return a Request
+ */
+ public static Request create(HttpMethod httpMethod,
+ String url,
+ Map> headers,
+ byte[] body,
+ Charset charset,
+ RequestTemplate requestTemplate) {
+ return create(httpMethod, url, headers, Body.create(body, charset), requestTemplate);
+ }
+
+ /**
+ * Builds a Request. All parameters must be effectively immutable, via safe copies.
+ *
+ * @param httpMethod for the request.
+ * @param url for the request.
+ * @param headers to include.
+ * @param body of the request, can be {@literal null}
+ * @return a Request
+ */
+ public static Request create(HttpMethod httpMethod,
+ String url,
+ Map> headers,
+ Body body,
+ RequestTemplate requestTemplate) {
+ return new Request(httpMethod, url, headers, body, requestTemplate);
+ }
+
+ private final HttpMethod httpMethod;
+ private final String url;
+ private final Map> headers;
+ private final Body body;
+ private final RequestTemplate requestTemplate;
+ private final ProtocolVersion protocolVersion;
+
+ /**
+ * Creates a new Request.
+ *
+ * @param method of the request.
+ * @param url for the request.
+ * @param headers for the request.
+ * @param body for the request, optional.
+ * @param requestTemplate used to build the request.
+ */
+ Request(HttpMethod method,
+ String url,
+ Map> headers,
+ Body body,
+ RequestTemplate requestTemplate) {
+ this.httpMethod = checkNotNull(method, "httpMethod of %s", method.name());
+ this.url = checkNotNull(url, "url");
+ this.headers = checkNotNull(headers, "headers of %s %s", method, url);
+ this.body = body;
+ this.requestTemplate = requestTemplate;
+ protocolVersion = ProtocolVersion.HTTP_1_1;
+ }
+
+ /**
+ * Http Method for this request.
+ *
+ * @return the HttpMethod string
+ * @deprecated @see {@link #httpMethod()}
+ */
+ @Deprecated
+ public String method() {
+ return httpMethod.name();
+ }
+
+ /**
+ * Http Method for the request.
+ *
+ * @return the HttpMethod.
+ */
+ public HttpMethod httpMethod() {
+ return this.httpMethod;
+ }
+
+
+ /**
+ * URL for the request.
+ *
+ * @return URL as a String.
+ */
+ public String url() {
+ return url;
+ }
+
+ /**
+ * Request Headers.
+ *
+ * @return the request headers.
+ */
+ public Map> headers() {
+ return Collections.unmodifiableMap(headers);
+ }
+
+ /**
+ * Charset of the request.
+ *
+ * @return the current character set for the request, may be {@literal null} for binary data.
+ */
+ public Charset charset() {
+ return body.encoding;
+ }
+
+ /**
+ * If present, this is the replayable body to send to the server. In some cases, this may be
+ * interpretable as text.
+ *
+ * @see #charset()
+ */
+ public byte[] body() {
+ return body.data;
+ }
+
+ public boolean isBinary() {
+ return body.isBinary();
+ }
+
+ /**
+ * Request Length.
+ *
+ * @return size of the request body.
+ */
+ public int length() {
+ return this.body.length();
+ }
+
+ /**
+ * Request HTTP protocol version
+ *
+ * @return HTTP protocol version
+ */
+ public ProtocolVersion protocolVersion() {
+ return protocolVersion;
+ }
+
+ /**
+ * Request as an HTTP/1.1 request.
+ *
+ * @return the request.
+ */
+ @Override
+ public String toString() {
+ final StringBuilder builder = new StringBuilder();
+ builder.append(httpMethod).append(' ').append(url).append(" HTTP/1.1\n");
+ for (final String field : headers.keySet()) {
+ for (final String value : valuesOrEmpty(headers, field)) {
+ builder.append(field).append(": ").append(value).append('\n');
+ }
+ }
+ if (body != null) {
+ builder.append('\n').append(body.asString());
+ }
+ return builder.toString();
+ }
+
+ /**
+ * Controls the per-request settings currently required to be implemented by all {@link Client
+ * clients}
+ */
+ public static class Options {
+
+ private final long connectTimeout;
+ private final TimeUnit connectTimeoutUnit;
+ private final long readTimeout;
+ private final TimeUnit readTimeoutUnit;
+ private final boolean followRedirects;
+
+ /**
+ * Creates a new Options instance.
+ *
+ * @param connectTimeoutMillis connection timeout in milliseconds.
+ * @param readTimeoutMillis read timeout in milliseconds.
+ * @param followRedirects if the request should follow 3xx redirections.
+ *
+ * @deprecated please use {@link #Options(long, TimeUnit, long, TimeUnit, boolean)}
+ */
+ @Deprecated
+ public Options(int connectTimeoutMillis, int readTimeoutMillis, boolean followRedirects) {
+ this(connectTimeoutMillis, TimeUnit.MILLISECONDS,
+ readTimeoutMillis, TimeUnit.MILLISECONDS,
+ followRedirects);
+ }
+
+ /**
+ * Creates a new Options Instance.
+ *
+ * @param connectTimeout value.
+ * @param connectTimeoutUnit with the TimeUnit for the timeout value.
+ * @param readTimeout value.
+ * @param readTimeoutUnit with the TimeUnit for the timeout value.
+ * @param followRedirects if the request should follow 3xx redirections.
+ */
+ public Options(long connectTimeout, TimeUnit connectTimeoutUnit,
+ long readTimeout, TimeUnit readTimeoutUnit,
+ boolean followRedirects) {
+ super();
+ this.connectTimeout = connectTimeout;
+ this.connectTimeoutUnit = connectTimeoutUnit;
+ this.readTimeout = readTimeout;
+ this.readTimeoutUnit = readTimeoutUnit;
+ this.followRedirects = followRedirects;
+ }
+
+ /**
+ * Creates a new Options instance that follows redirects by default.
+ *
+ * @param connectTimeoutMillis connection timeout in milliseconds.
+ * @param readTimeoutMillis read timeout in milliseconds.
+ *
+ * @deprecated please use {@link #Options(long, TimeUnit, long, TimeUnit, boolean)}
+ */
+ @Deprecated
+ public Options(int connectTimeoutMillis, int readTimeoutMillis) {
+ this(connectTimeoutMillis, readTimeoutMillis, true);
+ }
+
+ /**
+ * Creates the new Options instance using the following defaults:
+ *
+ *
Connect Timeout: 10 seconds
+ *
Read Timeout: 60 seconds
+ *
Follow all 3xx redirects
+ *
+ */
+ public Options() {
+ this(10, TimeUnit.SECONDS, 60, TimeUnit.SECONDS, true);
+ }
+
+ /**
+ * Defaults to 10 seconds. {@code 0} implies no timeout.
+ *
+ * @see java.net.HttpURLConnection#getConnectTimeout()
+ */
+ public int connectTimeoutMillis() {
+ return (int) connectTimeoutUnit.toMillis(connectTimeout);
+ }
+
+ /**
+ * Defaults to 60 seconds. {@code 0} implies no timeout.
+ *
+ * @see java.net.HttpURLConnection#getReadTimeout()
+ */
+ public int readTimeoutMillis() {
+ return (int) readTimeoutUnit.toMillis(readTimeout);
+ }
+
+
+ /**
+ * Defaults to true. {@code false} tells the client to not follow the redirections.
+ *
+ * @see HttpURLConnection#getFollowRedirects()
+ */
+ public boolean isFollowRedirects() {
+ return followRedirects;
+ }
+
+ /**
+ * Connect Timeout Value.
+ *
+ * @return current timeout value.
+ */
+ public long connectTimeout() {
+ return connectTimeout;
+ }
+
+ /**
+ * TimeUnit for the Connection Timeout value.
+ *
+ * @return TimeUnit
+ */
+ public TimeUnit connectTimeoutUnit() {
+ return connectTimeoutUnit;
+ }
+
+ /**
+ * Read Timeout value.
+ *
+ * @return current read timeout value.
+ */
+ public long readTimeout() {
+ return readTimeout;
+ }
+
+ /**
+ * TimeUnit for the Read Timeout value.
+ *
+ * @return TimeUnit
+ */
+ public TimeUnit readTimeoutUnit() {
+ return readTimeoutUnit;
+ }
+
+ }
+
+ @Experimental
+ public RequestTemplate requestTemplate() {
+ return this.requestTemplate;
+ }
+
+ /**
+ * Request Body
+ *
+ * Considered experimental, will most likely be made internal going forward.
+ *
+ */
+ @Experimental
+ public static class Body implements Serializable {
+
+ private transient Charset encoding;
+
+ private byte[] data;
+
+ private Body() {
+ super();
+ }
+
+ private Body(byte[] data) {
+ this.data = data;
+ }
+
+ private Body(byte[] data, Charset encoding) {
+ this.data = data;
+ this.encoding = encoding;
+ }
+
+ public Optional getEncoding() {
+ return Optional.ofNullable(this.encoding);
+ }
+
+ public int length() {
+ /* calculate the content length based on the data provided */
+ return data != null ? data.length : 0;
+ }
+
+ public byte[] asBytes() {
+ return data;
+ }
+
+ public String asString() {
+ return !isBinary()
+ ? new String(data, encoding)
+ : "Binary data";
+ }
+
+ public boolean isBinary() {
+ return encoding == null || data == null;
+ }
+
+ public static Body create(String data) {
+ return new Body(data.getBytes());
+ }
+
+ public static Body create(String data, Charset charset) {
+ return new Body(data.getBytes(charset), charset);
+ }
+
+ public static Body create(byte[] data) {
+ return new Body(data);
+ }
+
+ public static Body create(byte[] data, Charset charset) {
+ return new Body(data, charset);
+ }
+
+ /**
+ * Creates a new Request Body with charset encoded data.
+ *
+ * @param data to be encoded.
+ * @param charset to encode the data with. if {@literal null}, then data will be considered
+ * binary and will not be encoded.
+ *
+ * @return a new Request.Body instance with the encoded data.
+ * @deprecated please use {@link Request.Body#create(byte[], Charset)}
+ */
+ @Deprecated
+ public static Body encoded(byte[] data, Charset charset) {
+ return create(data, charset);
+ }
+
+ public static Body empty() {
+ return new Body();
+ }
+
+ }
+}
diff --git a/core/src/main/java/feign/RequestInterceptor.java b/core/src/main/java/feign/RequestInterceptor.java
new file mode 100644
index 0000000000..bf6496be6c
--- /dev/null
+++ b/core/src/main/java/feign/RequestInterceptor.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2012-2022 The Feign Authors
+ *
+ * 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;
+
+/**
+ * Zero or more {@code RequestInterceptors} may be configured for purposes such as adding headers to
+ * all requests. No guarantees are given with regards to the order that interceptors are applied.
+ * Once interceptors are applied, {@link Target#apply(RequestTemplate)} is called to create the
+ * immutable http request sent via {@link Client#execute(Request, feign.Request.Options)}.
+ *
+ * For example:
+ *
+ *
+ *
+ *
+ *
+ * Configuration
+ *
+ * {@code RequestInterceptors} are configured via {@link Feign.Builder#requestInterceptors}.
+ *
+ * Implementation notes
+ *
+ * Do not add parameters, such as {@code /path/{foo}/bar } in your implementation of
+ * {@link #apply(RequestTemplate)}.
+ * Interceptors are applied after the template's parameters are
+ * {@link RequestTemplate#resolve(java.util.Map) resolved}. This is to ensure that you can implement
+ * signatures are interceptors.
+ *
+ *
+ * Relationship to Retrofit 1.x
+ *
+ * This class is similar to {@code RequestInterceptor.intercept()}, except that the implementation
+ * can read, remove, or otherwise mutate any part of the request template.
+ */
+public interface RequestInterceptor {
+
+ /**
+ * Called for every request. Add data using methods on the supplied {@link RequestTemplate}.
+ */
+ void apply(RequestTemplate template);
+}
diff --git a/core/src/main/java/feign/RequestLine.java b/core/src/main/java/feign/RequestLine.java
new file mode 100644
index 0000000000..3869f2fe17
--- /dev/null
+++ b/core/src/main/java/feign/RequestLine.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2012-2022 The Feign Authors
+ *
+ * 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 java.lang.annotation.Retention;
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+/**
+ * Expands the uri template supplied in the {@code value}, permitting path and query variables, or
+ * just the http method. Templates should conform to
+ * RFC 6570. Support is limited to Simple String
+ * expansion and Reserved Expansion (Level 1 and Level 2) expressions.
+ */
+@java.lang.annotation.Target(METHOD)
+@Retention(RUNTIME)
+public @interface RequestLine {
+
+ String value();
+
+ boolean decodeSlash() default true;
+
+ CollectionFormat collectionFormat() default CollectionFormat.EXPLODED;
+}
diff --git a/core/src/main/java/feign/RequestTemplate.java b/core/src/main/java/feign/RequestTemplate.java
new file mode 100644
index 0000000000..04fe443c8c
--- /dev/null
+++ b/core/src/main/java/feign/RequestTemplate.java
@@ -0,0 +1,1033 @@
+/*
+ * Copyright 2012-2022 The Feign Authors
+ *
+ * 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 feign.Request.HttpMethod;
+import feign.template.*;
+import java.io.Serializable;
+import java.net.URI;
+import java.nio.charset.Charset;
+import java.util.AbstractMap.SimpleImmutableEntry;
+import java.util.*;
+import java.util.Map.Entry;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+import static feign.Util.*;
+
+/**
+ * Request Builder for an HTTP Target.
+ *
+ * This class is a variation on a UriTemplate, where, in addition to the uri, Headers and Query
+ * information also support template expressions.
+ *
+ */
+@SuppressWarnings("UnusedReturnValue")
+public final class RequestTemplate implements Serializable {
+
+ private static final Pattern QUERY_STRING_PATTERN = Pattern.compile("(? queries = new LinkedHashMap<>();
+ private final Map headers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+ private String target;
+ private String fragment;
+ private boolean resolved = false;
+ private UriTemplate uriTemplate;
+ private BodyTemplate bodyTemplate;
+ private HttpMethod method;
+ private transient Charset charset = Util.UTF_8;
+ private Request.Body body = Request.Body.empty();
+ private boolean decodeSlash = true;
+ private CollectionFormat collectionFormat = CollectionFormat.EXPLODED;
+ private MethodMetadata methodMetadata;
+ private Target> feignTarget;
+
+ /**
+ * Create a new Request Template.
+ */
+ public RequestTemplate() {
+ super();
+ }
+
+ /**
+ * Create a new Request Template.
+ *
+ * @param fragment part of the request uri.
+ * @param target for the template.
+ * @param uriTemplate for the template.
+ * @param bodyTemplate for the template, may be {@literal null}
+ * @param method of the request.
+ * @param charset for the request.
+ * @param body of the request, may be {@literal null}
+ * @param decodeSlash if the request uri should encode slash characters.
+ * @param collectionFormat when expanding collection based variables.
+ * @param feignTarget this template is targeted for.
+ * @param methodMetadata containing a reference to the method this template is built from.
+ */
+ private RequestTemplate(String target,
+ String fragment,
+ UriTemplate uriTemplate,
+ BodyTemplate bodyTemplate,
+ HttpMethod method,
+ Charset charset,
+ Request.Body body,
+ boolean decodeSlash,
+ CollectionFormat collectionFormat,
+ MethodMetadata methodMetadata,
+ Target> feignTarget) {
+ this.target = target;
+ this.fragment = fragment;
+ this.uriTemplate = uriTemplate;
+ this.bodyTemplate = bodyTemplate;
+ this.method = method;
+ this.charset = charset;
+ this.body = body;
+ this.decodeSlash = decodeSlash;
+ this.collectionFormat =
+ (collectionFormat != null) ? collectionFormat : CollectionFormat.EXPLODED;
+ this.methodMetadata = methodMetadata;
+ this.feignTarget = feignTarget;
+ }
+
+ /**
+ * Create a Request Template from an existing Request Template.
+ *
+ * @param requestTemplate to copy from.
+ * @return a new Request Template.
+ */
+ public static RequestTemplate from(RequestTemplate requestTemplate) {
+ RequestTemplate template =
+ new RequestTemplate(
+ requestTemplate.target,
+ requestTemplate.fragment,
+ requestTemplate.uriTemplate,
+ requestTemplate.bodyTemplate,
+ requestTemplate.method,
+ requestTemplate.charset,
+ requestTemplate.body,
+ requestTemplate.decodeSlash,
+ requestTemplate.collectionFormat,
+ requestTemplate.methodMetadata,
+ requestTemplate.feignTarget);
+
+ if (!requestTemplate.queries().isEmpty()) {
+ template.queries.putAll(requestTemplate.queries);
+ }
+
+ if (!requestTemplate.headers().isEmpty()) {
+ template.headers.putAll(requestTemplate.headers);
+ }
+ return template;
+ }
+
+ /**
+ * Create a Request Template from an existing Request Template.
+ *
+ * @param toCopy template.
+ * @deprecated replaced by {@link RequestTemplate#from(RequestTemplate)}
+ */
+ @Deprecated
+ public RequestTemplate(RequestTemplate toCopy) {
+ checkNotNull(toCopy, "toCopy");
+ this.target = toCopy.target;
+ this.fragment = toCopy.fragment;
+ this.method = toCopy.method;
+ this.queries.putAll(toCopy.queries);
+ this.headers.putAll(toCopy.headers);
+ this.charset = toCopy.charset;
+ this.body = toCopy.body;
+ this.decodeSlash = toCopy.decodeSlash;
+ this.collectionFormat =
+ (toCopy.collectionFormat != null) ? toCopy.collectionFormat : CollectionFormat.EXPLODED;
+ this.uriTemplate = toCopy.uriTemplate;
+ this.bodyTemplate = toCopy.bodyTemplate;
+ this.resolved = false;
+ this.methodMetadata = toCopy.methodMetadata;
+ this.target = toCopy.target;
+ this.feignTarget = toCopy.feignTarget;
+ }
+
+ /**
+ * Resolve all expressions using the variable value substitutions provided. Variable values will
+ * be pct-encoded, if they are not already.
+ *
+ * @param variables containing the variable values to use when resolving expressions.
+ * @return a new Request Template with all of the variables resolved.
+ */
+ public RequestTemplate resolve(Map variables) {
+
+ StringBuilder uri = new StringBuilder();
+
+ /* create a new template form this one, but explicitly */
+ RequestTemplate resolved = RequestTemplate.from(this);
+
+ if (this.uriTemplate == null) {
+ /* create a new uri template using the default root */
+ this.uriTemplate = UriTemplate.create("", !this.decodeSlash, this.charset);
+ }
+
+ String expanded = this.uriTemplate.expand(variables);
+ if (expanded != null) {
+ uri.append(expanded);
+ }
+
+ /*
+ * for simplicity, combine the queries into the uri and use the resulting uri to seed the
+ * resolved template.
+ */
+ if (!this.queries.isEmpty()) {
+ /*
+ * since we only want to keep resolved query values, reset any queries on the resolved copy
+ */
+ resolved.queries(Collections.emptyMap());
+ StringBuilder query = new StringBuilder();
+ Iterator queryTemplates = this.queries.values().iterator();
+
+ while (queryTemplates.hasNext()) {
+ QueryTemplate queryTemplate = queryTemplates.next();
+ String queryExpanded = queryTemplate.expand(variables);
+ if (Util.isNotBlank(queryExpanded)) {
+ query.append(queryExpanded);
+ if (queryTemplates.hasNext()) {
+ query.append("&");
+ }
+ }
+ }
+
+ String queryString = query.toString();
+ if (!queryString.isEmpty()) {
+ Matcher queryMatcher = QUERY_STRING_PATTERN.matcher(uri);
+ if (queryMatcher.find()) {
+ /* the uri already has a query, so any additional queries should be appended */
+ uri.append("&");
+ } else {
+ uri.append("?");
+ }
+ uri.append(queryString);
+ }
+ }
+
+ /* add the uri to result */
+ resolved.uri(uri.toString());
+
+ /* headers */
+ if (!this.headers.isEmpty()) {
+ /*
+ * same as the query string, we only want to keep resolved values, so clear the header map on
+ * the resolved instance
+ */
+ resolved.headers(Collections.emptyMap());
+ for (HeaderTemplate headerTemplate : this.headers.values()) {
+ /* resolve the header */
+ String header = headerTemplate.expand(variables);
+ if (!header.isEmpty()) {
+ /* append the header as a new literal as the value has already been expanded. */
+ resolved.header(headerTemplate.getName(), header);
+ }
+ }
+ }
+
+ if (this.bodyTemplate != null) {
+ resolved.body(this.bodyTemplate.expand(variables));
+ }
+
+ /* mark the new template resolved */
+ resolved.resolved = true;
+ return resolved;
+ }
+
+ /**
+ * Resolves all expressions, using the variables provided. Values not present in the {@code
+ * alreadyEncoded} map are pct-encoded.
+ *
+ * @param unencoded variable values to substitute.
+ * @param alreadyEncoded variable names.
+ * @return a resolved Request Template
+ * @deprecated use {@link RequestTemplate#resolve(Map)}. Values already encoded are recognized as
+ * such and skipped.
+ */
+ @SuppressWarnings("unused")
+ @Deprecated
+ RequestTemplate resolve(Map unencoded, Map alreadyEncoded) {
+ return this.resolve(unencoded);
+ }
+
+ /**
+ * Creates a {@link Request} from this template. The template must be resolved before calling this
+ * method, or an {@link IllegalStateException} will be thrown.
+ *
+ * @return a new Request instance.
+ * @throws IllegalStateException if this template has not been resolved.
+ */
+ public Request request() {
+ if (!this.resolved) {
+ throw new IllegalStateException("template has not been resolved.");
+ }
+ return Request.create(this.method, this.url(), this.headers(), this.body, this);
+ }
+
+ /**
+ * Set the Http Method.
+ *
+ * @param method to use.
+ * @return a RequestTemplate for chaining.
+ * @deprecated see {@link RequestTemplate#method(HttpMethod)}
+ */
+ @Deprecated
+ public RequestTemplate method(String method) {
+ checkNotNull(method, "method");
+ try {
+ this.method = HttpMethod.valueOf(method);
+ } catch (IllegalArgumentException iae) {
+ throw new IllegalArgumentException("Invalid HTTP Method: " + method);
+ }
+ return this;
+ }
+
+ /**
+ * Set the Http Method.
+ *
+ * @param method to use.
+ * @return a RequestTemplate for chaining.
+ */
+ public RequestTemplate method(HttpMethod method) {
+ checkNotNull(method, "method");
+ this.method = method;
+ return this;
+ }
+
+ /**
+ * The Request Http Method.
+ *
+ * @return Http Method.
+ */
+ public String method() {
+ return (method != null) ? method.name() : null;
+ }
+
+ /**
+ * Set whether do encode slash {@literal /} characters when resolving this template.
+ *
+ * @param decodeSlash if slash literals should not be encoded.
+ * @return a RequestTemplate for chaining.
+ */
+ public RequestTemplate decodeSlash(boolean decodeSlash) {
+ this.decodeSlash = decodeSlash;
+ this.uriTemplate =
+ UriTemplate.create(this.uriTemplate.toString(), !this.decodeSlash, this.charset);
+ if (!this.queries.isEmpty()) {
+ this.queries.replaceAll((key, queryTemplate) -> QueryTemplate.create(
+ /* replace the current template with new ones honoring the decode value */
+ queryTemplate.getName(), queryTemplate.getValues(), charset, collectionFormat,
+ decodeSlash));
+
+ }
+ return this;
+ }
+
+ /**
+ * If slash {@literal /} characters are not encoded when resolving.
+ *
+ * @return true if slash literals are not encoded, false otherwise.
+ */
+ public boolean decodeSlash() {
+ return decodeSlash;
+ }
+
+ /**
+ * The Collection Format to use when resolving variables that represent {@link Iterable}s or
+ * {@link Collection}s
+ *
+ * @param collectionFormat to use.
+ * @return a RequestTemplate for chaining.
+ */
+ public RequestTemplate collectionFormat(CollectionFormat collectionFormat) {
+ this.collectionFormat = collectionFormat;
+ return this;
+ }
+
+ /**
+ * The Collection Format that will be used when resolving {@link Iterable} and {@link Collection}
+ * variables.
+ *
+ * @return the collection format set
+ */
+ @SuppressWarnings("unused")
+ public CollectionFormat collectionFormat() {
+ return collectionFormat;
+ }
+
+ /**
+ * Append the value to the template.
+ *
+ * This method is poorly named and is used primarily to store the relative uri for the request. It
+ * has been replaced by {@link RequestTemplate#uri(String)} and will be removed in a future
+ * release.
+ *
+ *
+ * @param value to append.
+ * @return a RequestTemplate for chaining.
+ * @deprecated see {@link RequestTemplate#uri(String, boolean)}
+ */
+ @Deprecated
+ public RequestTemplate append(CharSequence value) {
+ /* proxy to url */
+ if (this.uriTemplate != null) {
+ return this.uri(value.toString(), true);
+ }
+ return this.uri(value.toString());
+ }
+
+ /**
+ * Insert the value at the specified point in the template uri.
+ *
+ * This method is poorly named has undocumented behavior. When the value contains a fully
+ * qualified http request url, the value is always inserted at the beginning of the uri.
+ *
+ *
+ * Due to this, use of this method is not recommended and remains for backward compatibility. It
+ * has been replaced by {@link RequestTemplate#target(String)} and will be removed in a future
+ * release.
+ *
+ *
+ * @param pos in the uri to place the value.
+ * @param value to insert.
+ * @return a RequestTemplate for chaining.
+ * @deprecated see {@link RequestTemplate#target(String)}
+ */
+ @SuppressWarnings("unused")
+ @Deprecated
+ public RequestTemplate insert(int pos, CharSequence value) {
+ return target(value.toString());
+ }
+
+ /**
+ * Set the Uri for the request, replacing the existing uri if set.
+ *
+ * @param uri to use, must be a relative uri.
+ * @return a RequestTemplate for chaining.
+ */
+ public RequestTemplate uri(String uri) {
+ return this.uri(uri, false);
+ }
+
+ /**
+ * Set the uri for the request.
+ *
+ * @param uri to use, must be a relative uri.
+ * @param append if the uri should be appended, if the uri is already set.
+ * @return a RequestTemplate for chaining.
+ */
+ public RequestTemplate uri(String uri, boolean append) {
+ /* validate and ensure that the url is always a relative one */
+ if (UriUtils.isAbsolute(uri)) {
+ throw new IllegalArgumentException("url values must be not be absolute.");
+ }
+
+ if (uri == null) {
+ uri = "/";
+ } else if ((!uri.isEmpty() && !uri.startsWith("/") && !uri.startsWith("{")
+ && !uri.startsWith("?") && !uri.startsWith(";"))) {
+ /* if the start of the url is a literal, it must begin with a slash. */
+ uri = "/" + uri;
+ }
+
+ /*
+ * templates may provide query parameters. since we want to manage those explicity, we will need
+ * to extract those out, leaving the uriTemplate with only the path to deal with.
+ */
+ Matcher queryMatcher = QUERY_STRING_PATTERN.matcher(uri);
+ if (queryMatcher.find()) {
+ String queryString = uri.substring(queryMatcher.start() + 1);
+
+ /* parse the query string */
+ this.extractQueryTemplates(queryString, append);
+
+ /* reduce the uri to the path */
+ uri = uri.substring(0, queryMatcher.start());
+ }
+
+ int fragmentIndex = uri.indexOf('#');
+ if (fragmentIndex > -1) {
+ fragment = uri.substring(fragmentIndex);
+ uri = uri.substring(0, fragmentIndex);
+ }
+
+ /* replace the uri template */
+ if (append && this.uriTemplate != null) {
+ this.uriTemplate = UriTemplate.append(this.uriTemplate, uri);
+ } else {
+ this.uriTemplate = UriTemplate.create(uri, !this.decodeSlash, this.charset);
+ }
+ return this;
+ }
+
+ /**
+ * Set the target host for this request.
+ *
+ * @param target host for this request. Must be an absolute target.
+ * @return a RequestTemplate for chaining.
+ */
+ public RequestTemplate target(String target) {
+ /* target can be empty */
+ if (Util.isBlank(target)) {
+ return this;
+ }
+
+ /* verify that the target contains the scheme, host and port */
+ if (!UriUtils.isAbsolute(target)) {
+ throw new IllegalArgumentException("target values must be absolute.");
+ }
+ if (target.endsWith("/")) {
+ target = target.substring(0, target.length() - 1);
+ }
+ try {
+ /* parse the target */
+ URI targetUri = URI.create(target);
+
+ if (Util.isNotBlank(targetUri.getRawQuery())) {
+ /*
+ * target has a query string, we need to make sure that they are recorded as queries
+ */
+ this.extractQueryTemplates(targetUri.getRawQuery(), true);
+ }
+
+ /* strip the query string */
+ this.target = targetUri.getScheme() + "://" + targetUri.getAuthority() + targetUri.getPath();
+ if (targetUri.getFragment() != null) {
+ this.fragment = "#" + targetUri.getFragment();
+ }
+ } catch (IllegalArgumentException iae) {
+ /* the uri provided is not a valid one, we can't continue */
+ throw new IllegalArgumentException("Target is not a valid URI.", iae);
+ }
+ return this;
+ }
+
+ /**
+ * The URL for the request. If the template has not been resolved, the url will represent a uri
+ * template.
+ *
+ * @return the url
+ */
+ public String url() {
+
+ /* build the fully qualified url with all query parameters */
+ StringBuilder url = new StringBuilder(this.path());
+ if (!this.queries.isEmpty()) {
+ url.append(this.queryLine());
+ }
+ if (fragment != null) {
+ url.append(fragment);
+ }
+
+ return url.toString();
+ }
+
+ /**
+ * The Uri Path.
+ *
+ * @return the uri path.
+ */
+ public String path() {
+ /* build the fully qualified url with all query parameters */
+ StringBuilder path = new StringBuilder();
+ if (this.target != null) {
+ path.append(this.target);
+ }
+ if (this.uriTemplate != null) {
+ path.append(this.uriTemplate.toString());
+ }
+ if (path.length() == 0) {
+ /* no path indicates the root uri */
+ path.append("/");
+ }
+ return path.toString();
+
+ }
+
+ /**
+ * List all of the template variable expressions for this template.
+ *
+ * @return a list of template variable names
+ */
+ public List variables() {
+ /* combine the variables from the uri, query, header, and body templates */
+ List variables = new ArrayList<>(this.uriTemplate.getVariables());
+
+ /* queries */
+ for (QueryTemplate queryTemplate : this.queries.values()) {
+ variables.addAll(queryTemplate.getVariables());
+ }
+
+ /* headers */
+ for (HeaderTemplate headerTemplate : this.headers.values()) {
+ variables.addAll(headerTemplate.getVariables());
+ }
+
+ /* body */
+ if (this.bodyTemplate != null) {
+ variables.addAll(this.bodyTemplate.getVariables());
+ }
+
+ return variables;
+ }
+
+ /**
+ * @see RequestTemplate#query(String, Iterable)
+ */
+ public RequestTemplate query(String name, String... values) {
+ if (values == null) {
+ return query(name, Collections.emptyList());
+ }
+ return query(name, Arrays.asList(values));
+ }
+
+
+ /**
+ * Specify a Query String parameter, with the specified values. Values can be literals or template
+ * expressions.
+ *
+ * @param name of the parameter.
+ * @param values for this parameter.
+ * @return a RequestTemplate for chaining.
+ */
+ public RequestTemplate query(String name, Iterable values) {
+ return appendQuery(name, values, this.collectionFormat);
+ }
+
+ /**
+ * Specify a Query String parameter, with the specified values. Values can be literals or template
+ * expressions.
+ *
+ * @param name of the parameter.
+ * @param values for this parameter.
+ * @param collectionFormat to use when resolving collection based expressions.
+ * @return a Request Template for chaining.
+ */
+ public RequestTemplate query(String name,
+ Iterable values,
+ CollectionFormat collectionFormat) {
+ return appendQuery(name, values, collectionFormat);
+ }
+
+ /**
+ * Appends the query name and values.
+ *
+ * @param name of the parameter.
+ * @param values for the parameter, may be expressions.
+ * @param collectionFormat to use when resolving collection based query variables.
+ * @return a RequestTemplate for chaining.
+ */
+ private RequestTemplate appendQuery(String name,
+ Iterable values,
+ CollectionFormat collectionFormat) {
+ if (!values.iterator().hasNext()) {
+ /* empty value, clear the existing values */
+ this.queries.remove(name);
+ return this;
+ }
+
+ /* create a new query template out of the information here */
+ this.queries.compute(name, (key, queryTemplate) -> {
+ if (queryTemplate == null) {
+ return QueryTemplate.create(name, values, this.charset, collectionFormat, this.decodeSlash);
+ } else {
+ return QueryTemplate.append(queryTemplate, values, collectionFormat, this.decodeSlash);
+ }
+ });
+ return this;
+ }
+
+ /**
+ * Sets the Query Parameters.
+ *
+ * @param queries to use for this request.
+ * @return a RequestTemplate for chaining.
+ */
+ @SuppressWarnings("unused")
+ public RequestTemplate queries(Map> queries) {
+ if (queries == null || queries.isEmpty()) {
+ this.queries.clear();
+ } else {
+ queries.forEach(this::query);
+ }
+ return this;
+ }
+
+
+ /**
+ * Return an immutable Map of all Query Parameters and their values.
+ *
+ * @return registered Query Parameters.
+ */
+ public Map> queries() {
+ Map> queryMap = new LinkedHashMap<>();
+ this.queries.forEach((key, queryTemplate) -> {
+ List values = new ArrayList<>(queryTemplate.getValues());
+
+ /* add the expanded collection, but lock it */
+ queryMap.put(key, Collections.unmodifiableList(values));
+ });
+
+ return Collections.unmodifiableMap(queryMap);
+ }
+
+ /**
+ * @see RequestTemplate#header(String, Iterable)
+ */
+ public RequestTemplate header(String name, String... values) {
+ return header(name, Arrays.asList(values));
+ }
+
+ /**
+ * Specify a Header, with the specified values. Values can be literals or template expressions.
+ *
+ * @param name of the header.
+ * @param values for this header.
+ * @return a RequestTemplate for chaining.
+ */
+ public RequestTemplate header(String name, Iterable values) {
+ if (name == null || name.isEmpty()) {
+ throw new IllegalArgumentException("name is required.");
+ }
+ if (values == null) {
+ values = Collections.emptyList();
+ }
+
+ return appendHeader(name, values);
+ }
+
+ /**
+ * Clear on reader from {@link RequestTemplate}
+ *
+ * @param name of the header.
+ * @return a RequestTemplate for chaining.
+ */
+ public RequestTemplate removeHeader(String name) {
+ if (name == null || name.isEmpty()) {
+ throw new IllegalArgumentException("name is required.");
+ }
+ this.headers.remove(name);
+ return this;
+ }
+
+ /**
+ * Create a Header Template.
+ *
+ * @param name of the header
+ * @param values for the header, may be expressions.
+ * @return a RequestTemplate for chaining.
+ */
+ private RequestTemplate appendHeader(String name, Iterable values) {
+ if (!values.iterator().hasNext()) {
+ /* empty value, clear the existing values */
+ this.headers.remove(name);
+ return this;
+ }
+ if (name.equals("Content-Type")) {
+ // a client can only produce content of one single type, so always override Content-Type and
+ // only add a single type
+ this.headers.remove(name);
+ this.headers.put(name,
+ HeaderTemplate.create(name, Collections.singletonList(values.iterator().next())));
+ return this;
+ }
+ this.headers.compute(name, (headerName, headerTemplate) -> {
+ if (headerTemplate == null) {
+ return HeaderTemplate.create(headerName, values);
+ } else {
+ return HeaderTemplate.append(headerTemplate, values);
+ }
+ });
+ return this;
+ }
+
+ /**
+ * Headers for this Request.
+ *
+ * @param headers to use.
+ * @return a RequestTemplate for chaining.
+ */
+ public RequestTemplate headers(Map> headers) {
+ if (headers != null && !headers.isEmpty()) {
+ headers.forEach(this::header);
+ } else {
+ this.headers.clear();
+ }
+ return this;
+ }
+
+ /**
+ * Returns an immutable copy of the Headers for this request.
+ *
+ * @return the currently applied headers.
+ */
+ public Map> headers() {
+ Map> headerMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+ this.headers.forEach((key, headerTemplate) -> {
+ List values = new ArrayList<>(headerTemplate.getValues());
+
+ /* add the expanded collection, but only if it has values */
+ if (!values.isEmpty()) {
+ headerMap.put(key, Collections.unmodifiableList(values));
+ }
+ });
+ return Collections.unmodifiableMap(headerMap);
+ }
+
+ /**
+ * Sets the Body and Charset for this request.
+ *
+ * @param data to send, can be null.
+ * @param charset of the encoded data.
+ * @return a RequestTemplate for chaining.
+ */
+ public RequestTemplate body(byte[] data, Charset charset) {
+ this.body(Request.Body.create(data, charset));
+ return this;
+ }
+
+ /**
+ * Set the Body for this request. Charset is assumed to be UTF_8. Data must be encoded.
+ *
+ * @param bodyText to send.
+ * @return a RequestTemplate for chaining.
+ */
+ public RequestTemplate body(String bodyText) {
+ this.body(Request.Body.create(bodyText.getBytes(this.charset), this.charset));
+ return this;
+ }
+
+ /**
+ * Set the Body for this request.
+ *
+ * @param body to send.
+ * @return a RequestTemplate for chaining.
+ * @deprecated use {@link #body(byte[], Charset)} instead.
+ */
+ @Deprecated
+ public RequestTemplate body(Request.Body body) {
+ this.body = body;
+
+ /* body template must be cleared to prevent double processing */
+ this.bodyTemplate = null;
+
+ header(CONTENT_LENGTH, Collections.emptyList());
+ if (body.length() > 0) {
+ header(CONTENT_LENGTH, String.valueOf(body.length()));
+ }
+
+ return this;
+ }
+
+ /**
+ * Charset of the Request Body, if known.
+ *
+ * @return the currently applied Charset.
+ */
+ public Charset requestCharset() {
+ if (this.body != null) {
+ return this.body.getEncoding()
+ .orElse(this.charset);
+ }
+ return this.charset;
+ }
+
+ /**
+ * The Request Body.
+ *
+ * @return the request body.
+ */
+ public byte[] body() {
+ return body.asBytes();
+ }
+
+ /**
+ * The Request.Body internal object.
+ *
+ * @return the internal Request.Body.
+ * @deprecated this abstraction is leaky and will be removed in later releases.
+ */
+ @Deprecated
+ public Request.Body requestBody() {
+ return this.body;
+ }
+
+
+ /**
+ * Specify the Body Template to use. Can contain literals and expressions.
+ *
+ * @param bodyTemplate to use.
+ * @return a RequestTemplate for chaining.
+ */
+ public RequestTemplate bodyTemplate(String bodyTemplate) {
+ this.bodyTemplate = BodyTemplate.create(bodyTemplate, this.charset);
+ return this;
+ }
+
+ /**
+ * Specify the Body Template to use. Can contain literals and expressions.
+ *
+ * @param bodyTemplate to use.
+ * @return a RequestTemplate for chaining.
+ */
+ public RequestTemplate bodyTemplate(String bodyTemplate, Charset charset) {
+ this.bodyTemplate = BodyTemplate.create(bodyTemplate, charset);
+ this.charset = charset;
+ return this;
+ }
+
+ /**
+ * Body Template to resolve.
+ *
+ * @return the unresolved body template.
+ */
+ public String bodyTemplate() {
+ if (this.bodyTemplate != null) {
+ return this.bodyTemplate.toString();
+ }
+ return null;
+ }
+
+ @Override
+ public String toString() {
+ return request().toString();
+ }
+
+ /**
+ * Return if the variable exists on the uri, query, or headers, in this template.
+ *
+ * @param variable to look for.
+ * @return true if the variable exists, false otherwise.
+ */
+ public boolean hasRequestVariable(String variable) {
+ return this.getRequestVariables().contains(variable);
+ }
+
+ /**
+ * Retrieve all uri, header, and query template variables.
+ *
+ * @return a List of all the variable names.
+ */
+ public Collection getRequestVariables() {
+ final Collection variables = new LinkedHashSet<>(this.uriTemplate.getVariables());
+ this.queries.values().forEach(queryTemplate -> variables.addAll(queryTemplate.getVariables()));
+ this.headers.values()
+ .forEach(headerTemplate -> variables.addAll(headerTemplate.getVariables()));
+ return variables;
+ }
+
+ /**
+ * If this template has been resolved.
+ *
+ * @return true if the template has been resolved, false otherwise.
+ */
+ @SuppressWarnings("unused")
+ public boolean resolved() {
+ return this.resolved;
+ }
+
+ /**
+ * The Query String for the template. Expressions are not resolved.
+ *
+ * @return the Query String.
+ */
+ public String queryLine() {
+ StringBuilder queryString = new StringBuilder();
+
+ if (!this.queries.isEmpty()) {
+ Iterator iterator = this.queries.values().iterator();
+ while (iterator.hasNext()) {
+ QueryTemplate queryTemplate = iterator.next();
+ String query = queryTemplate.toString();
+ if (query != null && !query.isEmpty()) {
+ queryString.append(query);
+ if (iterator.hasNext()) {
+ queryString.append("&");
+ }
+ }
+ }
+ }
+ /* remove any trailing ampersands */
+ String result = queryString.toString();
+ if (result.endsWith("&")) {
+ result = result.substring(0, result.length() - 1);
+ }
+
+ if (!result.isEmpty()) {
+ result = "?" + result;
+ }
+
+ return result;
+ }
+
+ private void extractQueryTemplates(String queryString, boolean append) {
+ /* split the query string up into name value pairs */
+ Map> queryParameters =
+ Arrays.stream(queryString.split("&"))
+ .map(this::splitQueryParameter)
+ .collect(Collectors.groupingBy(
+ SimpleImmutableEntry::getKey,
+ LinkedHashMap::new,
+ Collectors.mapping(Entry::getValue, Collectors.toList())));
+
+ /* add them to this template */
+ if (!append) {
+ /* clear the queries and use the new ones */
+ this.queries.clear();
+ }
+ queryParameters.forEach(this::query);
+ }
+
+ private SimpleImmutableEntry splitQueryParameter(String pair) {
+ int eq = pair.indexOf("=");
+ final String name = (eq > 0) ? pair.substring(0, eq) : pair;
+ final String value = (eq > 0 && eq < pair.length()) ? pair.substring(eq + 1) : null;
+ return new SimpleImmutableEntry<>(name, value);
+ }
+
+ @Experimental
+ public RequestTemplate methodMetadata(MethodMetadata methodMetadata) {
+ this.methodMetadata = methodMetadata;
+ return this;
+ }
+
+ @Experimental
+ public RequestTemplate feignTarget(Target> feignTarget) {
+ this.feignTarget = feignTarget;
+ return this;
+ }
+
+ @Experimental
+ public MethodMetadata methodMetadata() {
+ return methodMetadata;
+ }
+
+ @Experimental
+ public Target> feignTarget() {
+ return feignTarget;
+ }
+
+ /**
+ * Factory for creating RequestTemplate.
+ */
+ interface Factory {
+
+ /**
+ * create a request template using args passed to a method invocation.
+ */
+ RequestTemplate create(Object[] argv);
+ }
+
+}
diff --git a/core/src/main/java/feign/Response.java b/core/src/main/java/feign/Response.java
new file mode 100644
index 0000000000..84ad31d998
--- /dev/null
+++ b/core/src/main/java/feign/Response.java
@@ -0,0 +1,382 @@
+/*
+ * Copyright 2012-2022 The Feign Authors
+ *
+ * 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 java.io.*;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.util.*;
+import feign.Request.ProtocolVersion;
+import static feign.Util.*;
+
+/**
+ * An immutable response to an http invocation which only returns string content.
+ */
+public final class Response implements Closeable {
+
+ private final int status;
+ private final String reason;
+ private final Map> headers;
+ private final Body body;
+ private final Request request;
+ private final ProtocolVersion protocolVersion;
+
+ private Response(Builder builder) {
+ checkState(builder.request != null, "original request is required");
+ this.status = builder.status;
+ this.request = builder.request;
+ this.reason = builder.reason; // nullable
+ this.headers = caseInsensitiveCopyOf(builder.headers);
+ this.body = builder.body; // nullable
+ this.protocolVersion = builder.protocolVersion;
+ }
+
+ public Builder toBuilder() {
+ return new Builder(this);
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ public static final class Builder {
+ int status;
+ String reason;
+ Map> headers;
+ Body body;
+ Request request;
+ private RequestTemplate requestTemplate;
+ private ProtocolVersion protocolVersion = ProtocolVersion.HTTP_1_1;
+
+ Builder() {}
+
+ Builder(Response source) {
+ this.status = source.status;
+ this.reason = source.reason;
+ this.headers = source.headers;
+ this.body = source.body;
+ this.request = source.request;
+ this.protocolVersion = source.protocolVersion;
+ }
+
+ /** @see Response#status */
+ public Builder status(int status) {
+ this.status = status;
+ return this;
+ }
+
+ /** @see Response#reason */
+ public Builder reason(String reason) {
+ this.reason = reason;
+ return this;
+ }
+
+ /** @see Response#headers */
+ public Builder headers(Map> headers) {
+ this.headers = headers;
+ return this;
+ }
+
+ /** @see Response#body */
+ public Builder body(Body body) {
+ this.body = body;
+ return this;
+ }
+
+ /** @see Response#body */
+ public Builder body(InputStream inputStream, Integer length) {
+ this.body = InputStreamBody.orNull(inputStream, length);
+ return this;
+ }
+
+ /** @see Response#body */
+ public Builder body(byte[] data) {
+ this.body = ByteArrayBody.orNull(data);
+ return this;
+ }
+
+ /** @see Response#body */
+ public Builder body(String text, Charset charset) {
+ this.body = ByteArrayBody.orNull(text, charset);
+ return this;
+ }
+
+ /**
+ * @see Response#request
+ */
+ public Builder request(Request request) {
+ checkNotNull(request, "request is required");
+ this.request = request;
+ return this;
+ }
+
+ /**
+ * HTTP protocol version
+ */
+ public Builder protocolVersion(ProtocolVersion protocolVersion) {
+ this.protocolVersion = protocolVersion;
+ return this;
+ }
+
+ /**
+ * The Request Template used for the original request.
+ *
+ * @param requestTemplate used.
+ * @return builder reference.
+ */
+ @Experimental
+ public Builder requestTemplate(RequestTemplate requestTemplate) {
+ this.requestTemplate = requestTemplate;
+ return this;
+ }
+
+ public Response build() {
+ return new Response(this);
+ }
+ }
+
+ /**
+ * status code. ex {@code 200}
+ *
+ * See rfc2616
+ */
+ public int status() {
+ return status;
+ }
+
+ /**
+ * Nullable and not set when using http/2
+ *
+ * See https://github.com/http2/http2-spec/issues/202
+ */
+ public String reason() {
+ return reason;
+ }
+
+ /**
+ * Returns a case-insensitive mapping of header names to their values.
+ */
+ public Map> headers() {
+ return headers;
+ }
+
+ /**
+ * if present, the response had a body
+ */
+ public Body body() {
+ return body;
+ }
+
+ /**
+ * the request that generated this response
+ */
+ public Request request() {
+ return request;
+ }
+
+ /**
+ * the HTTP protocol version
+ *
+ * @return HTTP protocol version or empty if a client does not provide it
+ */
+ public ProtocolVersion protocolVersion() {
+ return protocolVersion;
+ }
+
+ public Charset charset() {
+
+ Collection contentTypeHeaders = headers().get("Content-Type");
+
+ if (contentTypeHeaders != null) {
+ for (String contentTypeHeader : contentTypeHeaders) {
+ String[] contentTypeParmeters = contentTypeHeader.split(";");
+ if (contentTypeParmeters.length > 1) {
+ String[] charsetParts = contentTypeParmeters[1].split("=");
+ if (charsetParts.length == 2 && "charset".equalsIgnoreCase(charsetParts[0].trim())) {
+ return Charset.forName(charsetParts[1]);
+ }
+ }
+ }
+ }
+
+ return Util.UTF_8;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder builder = new StringBuilder("HTTP/1.1 ").append(status);
+ if (reason != null)
+ builder.append(' ').append(reason);
+ builder.append('\n');
+ for (String field : headers.keySet()) {
+ for (String value : valuesOrEmpty(headers, field)) {
+ builder.append(field).append(": ").append(value).append('\n');
+ }
+ }
+ if (body != null)
+ builder.append('\n').append(body);
+ return builder.toString();
+ }
+
+ @Override
+ public void close() {
+ Util.ensureClosed(body);
+ }
+
+ public interface Body extends Closeable {
+
+ /**
+ * length in bytes, if known. Null if unknown or greater than {@link Integer#MAX_VALUE}.
+ *
+ *
+ *
+ *
+ * Note
+ * This is an integer as most implementations cannot do bodies greater than 2GB.
+ */
+ Integer length();
+
+ /**
+ * True if {@link #asInputStream()} and {@link #asReader()} can be called more than once.
+ */
+ boolean isRepeatable();
+
+ /**
+ * It is the responsibility of the caller to close the stream.
+ */
+ InputStream asInputStream() throws IOException;
+
+ /**
+ * It is the responsibility of the caller to close the stream.
+ *
+ * @deprecated favor {@link Body#asReader(Charset)}
+ */
+ @Deprecated
+ default Reader asReader() throws IOException {
+ return asReader(StandardCharsets.UTF_8);
+ }
+
+ /**
+ * It is the responsibility of the caller to close the stream.
+ */
+ Reader asReader(Charset charset) throws IOException;
+ }
+
+ private static final class InputStreamBody implements Response.Body {
+
+ private final InputStream inputStream;
+ private final Integer length;
+
+ private InputStreamBody(InputStream inputStream, Integer length) {
+ this.inputStream = inputStream;
+ this.length = length;
+ }
+
+ private static Body orNull(InputStream inputStream, Integer length) {
+ if (inputStream == null) {
+ return null;
+ }
+ return new InputStreamBody(inputStream, length);
+ }
+
+ @Override
+ public Integer length() {
+ return length;
+ }
+
+ @Override
+ public boolean isRepeatable() {
+ return false;
+ }
+
+ @Override
+ public InputStream asInputStream() {
+ return inputStream;
+ }
+
+ @SuppressWarnings("deprecation")
+ @Override
+ public Reader asReader() {
+ return new InputStreamReader(inputStream, UTF_8);
+ }
+
+ @Override
+ public Reader asReader(Charset charset) throws IOException {
+ checkNotNull(charset, "charset should not be null");
+ return new InputStreamReader(inputStream, charset);
+ }
+
+ @Override
+ public void close() throws IOException {
+ inputStream.close();
+ }
+
+ }
+
+ private static final class ByteArrayBody implements Response.Body {
+
+ private final byte[] data;
+
+ public ByteArrayBody(byte[] data) {
+ this.data = data;
+ }
+
+ private static Body orNull(byte[] data) {
+ if (data == null) {
+ return null;
+ }
+ return new ByteArrayBody(data);
+ }
+
+ private static Body orNull(String text, Charset charset) {
+ if (text == null) {
+ return null;
+ }
+ checkNotNull(charset, "charset");
+ return new ByteArrayBody(text.getBytes(charset));
+ }
+
+ @Override
+ public Integer length() {
+ return data.length;
+ }
+
+ @Override
+ public boolean isRepeatable() {
+ return true;
+ }
+
+ @Override
+ public InputStream asInputStream() throws IOException {
+ return new ByteArrayInputStream(data);
+ }
+
+ @SuppressWarnings("deprecation")
+ @Override
+ public Reader asReader() throws IOException {
+ return new InputStreamReader(asInputStream(), UTF_8);
+ }
+
+ @Override
+ public Reader asReader(Charset charset) throws IOException {
+ checkNotNull(charset, "charset should not be null");
+ return new InputStreamReader(asInputStream(), charset);
+ }
+
+ @Override
+ public void close() throws IOException {}
+
+ }
+
+}
diff --git a/core/src/main/java/feign/ResponseInterceptor.java b/core/src/main/java/feign/ResponseInterceptor.java
new file mode 100644
index 0000000000..d89127322f
--- /dev/null
+++ b/core/src/main/java/feign/ResponseInterceptor.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2012-2022 The Feign Authors
+ *
+ * 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 java.io.IOException;
+import java.util.function.Function;
+
+/**
+ * Zero or One {@code ResponseInterceptor} may be configured for purposes such as verify or modify
+ * headers of response, verify the business status of decoded object. Once interceptors are applied,
+ * {@link ResponseInterceptor#aroundDecode(Response, Function)} is called around decode method
+ * called
+ */
+public interface ResponseInterceptor {
+
+ ResponseInterceptor DEFAULT = InvocationContext::proceed;
+
+ /**
+ * Called for response around decode, must either manually invoke
+ * {@link InvocationContext#proceed} or manually create a new response object
+ *
+ * @param invocationContext information surrounding the response been decoded
+ * @return decoded response
+ */
+ Object aroundDecode(InvocationContext invocationContext) throws IOException;
+
+}
diff --git a/core/src/main/java/feign/ResponseMapper.java b/core/src/main/java/feign/ResponseMapper.java
new file mode 100644
index 0000000000..cbc249929c
--- /dev/null
+++ b/core/src/main/java/feign/ResponseMapper.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2012-2022 The Feign Authors
+ *
+ * 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 java.lang.reflect.Type;
+
+/**
+ * Map function to apply to the response before decoding it.
+ *
+ *