diff --git a/.circleci/config.yml b/.circleci/config.yml
new file mode 100644
index 000000000..d1c8c0ac5
--- /dev/null
+++ b/.circleci/config.yml
@@ -0,0 +1,140 @@
+#
+# 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.
+#
+
+# common executors
+executors:
+ java:
+ docker:
+ - image: velo/toolchains-4-ci-builds:with-21
+
+# common commands
+commands:
+ resolve-dependencies:
+ description: 'Download and prepare all dependencies'
+ steps:
+ - run:
+ name: 'Resolving Dependencies'
+ command: |
+ ./mvnw -ntp 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: |
+ ./mvnw -ntp -nsu -s .circleci/settings.xml -P release -pl -:feign-benchmark -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:
+ executor:
+ name: java
+ <<: *defaults
+ steps:
+ - checkout
+ - restore_cache:
+ keys:
+ - feign-dependencies-v2-{{ checksum "pom.xml" }}
+ - feign-dependencies-v2-
+ - resolve-dependencies
+ - save_cache:
+ paths:
+ - ~/.m2/repository
+ key: feign-dependencies-v2-{{ checksum "pom.xml" }}
+ - run:
+ name: 'Test'
+ command: |
+ ./mvnw -ntp -B verify
+ - verify-formatting
+
+ deploy:
+ executor:
+ name: java
+ <<: *defaults
+ steps:
+ - checkout
+ - restore_cache:
+ keys:
+ - feign-dependencies-v2-{{ checksum "pom.xml" }}
+ - feign-dependencies-v2-
+ - resolve-dependencies
+ - configure-gpg
+ - nexus-deploy
+
+workflows:
+ version: 2
+ build:
+ jobs:
+ - test:
+ name: 'pr-build'
+ filters:
+ <<: *all-branches
+
+ snapshot:
+ jobs:
+ - test:
+ name: 'snapshot'
+ filters:
+ <<: *master-only
+ - deploy:
+ name: 'deploy snapshot'
+ requires:
+ - 'snapshot'
+ context: Sonatype
+ filters:
+ <<: *master-only
+
+ release:
+ jobs:
+ - deploy:
+ name: 'release to maven central'
+ context: Sonatype
+ filters:
+ <<: *tags-only
diff --git a/.circleci/settings.xml b/.circleci/settings.xml
new file mode 100644
index 000000000..b3b4740ad
--- /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/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 000000000..6741fdc93
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1,2 @@
+github: [velo]
+patreon: velo132
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 000000000..8bffe2fee
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,12 @@
+# 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"
+ directory: "/"
+ schedule:
+ interval: "weekly"
+ open-pull-requests-limit: 100
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
new file mode 100644
index 000000000..e442a7ff4
--- /dev/null
+++ b/.github/workflows/codeql-analysis.yml
@@ -0,0 +1,101 @@
+# ----------------------------------------------------------------------------
+# Copyright 2012-2014 The Feign Authors
+#
+# The Netty Project licenses this file to you 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:
+#
+# https://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.
+# ----------------------------------------------------------------------------
+# For most projects, this workflow file will not need changing; you simply need
+# to commit it to your repository.
+#
+# You may wish to alter this file to override the set of languages analyzed,
+# or to provide custom queries or build logic.
+name: "CodeQL"
+
+on:
+ push:
+ branches: ["master"]
+ pull_request:
+ # The branches below must be a subset of the branches above
+ branches: ["master"]
+ schedule:
+ - cron: '0 13 * * 3'
+
+permissions:
+ contents: read
+
+jobs:
+ analyze:
+ permissions:
+ actions: read # for github/codeql-action/init to get workflow details
+ contents: read # for actions/checkout to fetch code
+ security-events: write # for github/codeql-action/analyze to upload SARIF results
+ name: Analyze
+ runs-on: ubuntu-latest
+
+ strategy:
+ fail-fast: false
+ matrix:
+ # Override automatic language detection by changing the below list
+ # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python']
+ language: ['java']
+ # Learn more...
+ # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ # Cache .m2/repository
+ - name: Cache local Maven repository
+ uses: actions/cache@v3
+ continue-on-error: true
+ with:
+ path: ~/.m2/repository
+ key: ${{ runner.os }}-maven-${{ matrix.language }} ${{ hashFiles('**/pom.xml') }}
+ restore-keys: |
+ ${{ runner.os }}-maven-${{ matrix.language }}
+ ${{ runner.os }}-maven-
+
+ # Initializes the CodeQL tools for scanning.
+ - name: Initialize CodeQL
+ uses: github/codeql-action/init@v2
+ with:
+ languages: ${{ matrix.language }}
+ # If you wish to specify custom queries, you can do so here or in a config file.
+ # By default, queries listed here will override any specified in a config file.
+ # Prefix the list here with "+" to use these queries and those in the config file.
+ # queries: ./path/to/local/query, your-org/your-repo/queries@main
+
+ # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
+ # If this step fails, then you should remove it and run the build manually (see below)
+ # - name: Autobuild
+ # uses: github/codeql-action/autobuild@v2
+
+ # âšī¸ Command-line programs to run using the OS shell.
+ # đ https://git.io/JvXDl
+
+ # âī¸ If the Autobuild fails above, remove it and uncomment the following three lines
+ # and modify them (or add more) to build your code if your project
+ # uses a compiled language
+ - name: Setup Java JDK
+ uses: actions/setup-java@v4.0.0
+ with:
+ distribution: 'temurin'
+ java-version: '21'
+
+ - name: Compile project
+ run: ./mvnw -B -ntp clean package -Pquickbuild -Dtoolchain.skip=true
+ env:
+ DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
+
+ - name: Perform CodeQL Analysis
+ uses: github/codeql-action/analyze@v2
diff --git a/.github/workflows/comment-pr.yml b/.github/workflows/comment-pr.yml
new file mode 100644
index 000000000..4aac84804
--- /dev/null
+++ b/.github/workflows/comment-pr.yml
@@ -0,0 +1,56 @@
+# Description: This workflow is triggered when the `receive-pr` workflow completes to post suggestions on the PR.
+# Since this pull request has write permissions on the target repo, we should **NOT** execute any untrusted code.
+# https://securitylab.github.com/research/github-actions-preventing-pwn-requests/
+---
+name: comment-pr
+
+on:
+ workflow_run:
+ workflows: ["receive-pr"]
+ types:
+ - completed
+
+jobs:
+ post-suggestions:
+ # https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#running-a-workflow-based-on-the-conclusion-of-another-workflow
+ if: ${{ github.event.workflow_run.conclusion == 'success' }}
+ runs-on: ubuntu-latest
+ env:
+ # https://docs.github.com/en/actions/reference/authentication-in-a-workflow#permissions-for-the-github_token
+ ACCESS_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ timeout-minutes: 10
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ ref: ${{github.event.workflow_run.head_branch}}
+ repository: ${{github.event.workflow_run.head_repository.full_name}}
+
+ # Download the patch
+ - uses: actions/download-artifact@v4
+ with:
+ name: patch
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ run-id: ${{ github.event.workflow_run.id }}
+ - name: Apply patch
+ run: |
+ git apply git-diff.patch --allow-empty
+ rm git-diff.patch
+
+ # Download the PR number
+ - uses: actions/download-artifact@v4
+ with:
+ name: pr_number
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ run-id: ${{ github.event.workflow_run.id }}
+ - name: Read pr_number.txt
+ run: |
+ PR_NUMBER=$(cat pr_number.txt)
+ echo "PR_NUMBER=$PR_NUMBER" >> $GITHUB_ENV
+ rm pr_number.txt
+
+ # Post suggestions as a comment on the PR
+ - uses: googleapis/code-suggester@v4
+ with:
+ command: review
+ pull_number: ${{ env.PR_NUMBER }}
+ git_dir: '.'
diff --git a/.github/workflows/receive-pr.yml b/.github/workflows/receive-pr.yml
new file mode 100644
index 000000000..b2e22e802
--- /dev/null
+++ b/.github/workflows/receive-pr.yml
@@ -0,0 +1,56 @@
+# Description: This workflow runs OpenRewrite recipes against opened pull request and upload the patch.
+# Since this pull request receives untrusted code, we should **NOT** have any secrets in the environment.
+# https://securitylab.github.com/research/github-actions-preventing-pwn-requests/
+---
+name: receive-pr
+
+on:
+ pull_request:
+ types: [opened, synchronize]
+ branches:
+ - master
+
+concurrency:
+ group: '${{ github.workflow }} @ ${{ github.ref }}'
+ cancel-in-progress: true
+
+jobs:
+ upload-patch:
+ runs-on: ubuntu-latest
+ timeout-minutes: 10
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ ref: ${{github.event.pull_request.head.ref}}
+ repository: ${{github.event.pull_request.head.repo.full_name}}
+ - uses: actions/setup-java@v4
+ with:
+ java-version: '21'
+ distribution: 'temurin'
+ cache: 'maven'
+
+ # Capture the PR number
+ # https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#using-data-from-the-triggering-workflow
+ - name: Create pr_number.txt
+ run: echo "${{ github.event.number }}" > pr_number.txt
+ - uses: actions/upload-artifact@v4
+ with:
+ name: pr_number
+ path: pr_number.txt
+ - name: Remove pr_number.txt
+ run: rm -f pr_number.txt
+
+ # Execute recipes
+ - name: Apply OpenRewrite recipes
+ run: ./mvnw -Dtoolchain.skip=true -Dlicense.skip=true -DskipTests=true -P openrewrite clean install
+ secrets:
+ DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
+
+ # Capture the diff
+ - name: Create patch
+ run: |
+ git diff | tee git-diff.patch
+ - uses: actions/upload-artifact@v4
+ with:
+ name: patch
+ path: git-diff.patch
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 000000000..d19b71053
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,72 @@
+# 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
+.mvn/.develocity/develocity-workspace-id
diff --git a/.mvn/develocity.xml b/.mvn/develocity.xml
new file mode 100644
index 000000000..1fe2a983d
--- /dev/null
+++ b/.mvn/develocity.xml
@@ -0,0 +1,43 @@
+
+
+
+
+ https://develocity.commonhaus.dev
+ false
+
+ feign
+
+ #{isFalse(env['CI'])}
+
+ authenticated
+
+
+ #{{'0.0.0.0'}}
+
+
+
+
+ false
+
+
+ false
+ #{isTrue(env['CI'])}
+
+
+
diff --git a/.mvn/extensions.xml b/.mvn/extensions.xml
new file mode 100644
index 000000000..2d55265b1
--- /dev/null
+++ b/.mvn/extensions.xml
@@ -0,0 +1,29 @@
+
+
+
+
+ com.gradle
+ develocity-maven-extension
+ 1.22.1
+
+
+ com.gradle
+ common-custom-user-data-maven-extension
+ 2.0.1
+
+
diff --git a/.mvn/jvm.config b/.mvn/jvm.config
new file mode 100644
index 000000000..c79df8879
--- /dev/null
+++ b/.mvn/jvm.config
@@ -0,0 +1 @@
+-XX:CICompilerCount=1 -XX:TieredStopAtLevel=1 -Djava.security.egd=file:/dev/./urandom
diff --git a/.mvn/maven.config b/.mvn/maven.config
new file mode 100644
index 000000000..8b1378917
--- /dev/null
+++ b/.mvn/maven.config
@@ -0,0 +1 @@
+
diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties
new file mode 100644
index 000000000..fe378a151
--- /dev/null
+++ b/.mvn/wrapper/maven-wrapper.properties
@@ -0,0 +1,18 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you 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.
+wrapperVersion=3.3.1
+distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.6/apache-maven-3.9.6-bin.zip
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 000000000..7f3c52d2f
--- /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>`
+* Encoder and Decoders are specified via `Provides.Type.SET` binding.
+* Default Encoder and Form Encoder is `Encoder.Text`
+* Default Decoder is `Decoder.TextStream`
+* ErrorDecoder now returns Exception, not fallback.
+* There can only be one `ErrorDecoder` and `Request.Options` binding now.
+
+### Version 2.0.0
+* removes guava and jax-rs dependencies
+* adds JAX-RS integration
+
+### Version 1.1.0
+* adds Ribbon integration
+* adds cli example
+* exponential backoff customizable via Retryer.Default ctor
+
+### Version 1.0.0
+
+* Initial open source release
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 000000000..126217304
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,49 @@
+# Contributing to Feign
+Please read [HACKING](./HACKING.md) prior to raising change.
+
+If you would like to contribute code you can do so through GitHub by forking the repository and sending a pull request (on a branch other than `master` or `gh-pages`).
+
+## Pull Requests
+Pull requests eventually need to resolve to a single commit. The commit log should be easy to read as a change log. We use the following form to accomplish that.
+* First line is a <=72 character description in present tense, explaining what this does.
+ * Ex. "Fixes regression on encoding vnd headers" > "Fixed encoding bug", which forces the reader to look at code to understand impact.
+* Do not include issue links in the first line as that makes pull requests look weird.
+ * Ex. "Addresses #345" becomes a pull request title: "Addresses #345 #346"
+* After the first line, use markdown to concisely summarize the implementation.
+ * This isn't in leiu of comments, and it assumes the reader isn't intimately familar with code structure.
+* If the change closes an issue, note that at the end of the commit description ex. "Fixes #345"
+ * GitHub will automatically close change with this syntax.
+* If the change is notable, also update the [change log](./CHANGELOG.md) with your summary description.
+ * The unreleased minor version is often a good default.
+
+## Code Style
+
+When submitting code, please use the feign code format conventions. If you use Eclipse `m2eclipse` should take care of all settings automatically.
+You can also import formatter settings using the [`eclipse-java-style.xml`](https://github.com/OpenFeign/feign/blob/master/src/config/eclipse-java-style.xml) file.
+If using IntelliJ IDEA, you can use the [Eclipse Code Formatter Plugin](http://plugins.jetbrains.com/plugin/6546) to import the same file.
+
+## License
+
+By contributing your code, you agree to license your contribution under the terms of the [APLv2](./LICENSE)
+
+All files are released with the Apache 2.0 license.
+
+If you are adding a new file it should have a header like this:
+
+```
+/**
+ * Copyright 2012 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.
+ */
+ ```
diff --git a/HACKING.md b/HACKING.md
new file mode 100644
index 000000000..7e15d3166
--- /dev/null
+++ b/HACKING.md
@@ -0,0 +1,62 @@
+# Hacking Feign
+Feign is optimized for maintenance vs flexibility. It prefers small
+features that have been asked for repeated times, that are insured with
+tests, and have clear use cases. This limits the lines of code and count
+of modules in Feign's repo.
+
+Code design is opinionated including below:
+
+* Classes and methods default to package, not public visibility.
+* Changing certain implementation classes may be unsupported.
+* 3rd-party dependencies, and gnarly apis like java.beans are avoided.
+
+## How to request change
+The best way to approach something not yet supported is to ask on
+[gitter](https://gitter.im/OpenFeign/feign) or [raise an issue](https://github.com/OpenFeign/feign/issues).
+Asking for the feature you need (like how to deal with command groups)
+vs a specific implementation (like making a private type public) will
+give you more options to accomplish your goal.
+
+Advice usually comes in two parts: advice and workaround. Advice may be
+to change Feign's code, or to fork until the feature is more widely
+requested.
+
+## How change works
+High quality pull requests that have clear scope and tests that reflect
+the intent of the feature are often merged and released in days. If a
+merged change isn't immediately released and it is of priority to you,
+nag (make a comment) on your merged pull request until it is released.
+
+## How to experiment
+Changes to Feign's code are best addressed by the feature requestor in a
+pull request *after* discussing in an issue or on gitter. By discussing
+first, there's less chance of a mutually disappointing experience where
+a pull request is rejected. Moreover, the feature may be already present!
+
+Albeit rare, some features will be deferred or rejected for inclusion in
+Feign's main repository. In these cases, the choices are typically to
+either fork the repository, or make your own repository containing the
+change.
+
+### Forks are welcome!
+Forking isn't bad. It is a natural place to experiment and vet a feature
+before it ends up in Feign's main repository. Large features or those
+which haven't satisfied diverse need are often deferred to forks or
+separate repositories (see [Rule of Three](http://blog.codinghorror.com/rule-of-three/)).
+
+### Large integrations -> separate repositories
+If you look carefully, you'll notice Feign integrations are often less
+than 1000 lines of code including tests. Some features are rejected for
+inclusion solely due to the amount of maintenance. For example, adding
+some features might imply tying up maintainers for several days or weeks
+and resulting in a large percentage increase in the size of feign.
+
+Large integrations aren't bad, but to be sustainable, they need to be
+isolated where the maintenance of that feature doesn't endanger the
+maintainability of Feign itself. Feign has been going since 2012, without
+the need of full-time attention. This is largely because maintenance is
+low and approachable.
+
+A good example of a large integration is [spring-cloud-netflix](https://github.com/spring-cloud/spring-cloud-netflix/tree/master/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign).
+Spring Cloud Netflix is sustainable as it has had several people
+maintaining it, including Q&A support for years.
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 000000000..8c9fd075f
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright 2012 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.
diff --git a/NOTICE b/NOTICE
new file mode 100644
index 000000000..f547124cf
--- /dev/null
+++ b/NOTICE
@@ -0,0 +1,4 @@
+Feign
+Copyright 2012 The Feign Authors.
+
+Portions of this software developed by Commerce Technologies, Inc.
diff --git a/OSSMETADATA b/OSSMETADATA
new file mode 100644
index 000000000..b6f4252ce
--- /dev/null
+++ b/OSSMETADATA
@@ -0,0 +1 @@
+osslifecycle=archived
diff --git a/README.md b/README.md
index ebf660a86..a8c8d54d1 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,1529 @@
-feign
-=====
+# Feign simplifies the process of writing Java HTTP clients
+
+[](https://gitter.im/OpenFeign/feign?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
+[](https://circleci.com/gh/OpenFeign/feign/tree/master)
+[](https://search.maven.org/artifact/io.github.openfeign/feign-core/)
+
+Feign is a Java to HTTP client binder inspired by [Retrofit](https://github.com/square/retrofit), [JAXRS-2.0](https://jax-rs-spec.java.net/nonav/2.0/apidocs/index.html), and [WebSocket](http://www.oracle.com/technetwork/articles/java/jsr356-1937161.html). Feign's first goal was reducing the complexity of binding [Denominator](https://github.com/Netflix/Denominator) uniformly to HTTP APIs regardless of [ReSTfulness](http://www.slideshare.net/adrianfcole/99problems).
+
+---
+### Why Feign and not X?
+
+Feign uses tools like Jersey and CXF to write Java clients for ReST or SOAP services. Furthermore, Feign allows you to write your own code on top of http libraries such as Apache HC. Feign connects your code to http APIs with minimal overhead and code via customizable decoders and error handling, which can be written to any text-based http API.
+
+### How does Feign work?
+
+Feign works by processing annotations into a templatized request. Arguments are applied to these templates in a straightforward fashion before output. Although Feign is limited to supporting text-based APIs, it dramatically simplifies system aspects such as replaying requests. Furthermore, Feign makes it easy to unit test your conversions knowing this.
+
+### Java Version Compatibility
+
+Feign 10.x and above are built on Java 8 and should work on Java 9, 10, and 11. For those that need JDK 6 compatibility, please use Feign 9.x
+
+## Feature overview
+
+This is a map with current key features provided by feign:
+
+
+
+# Roadmap
+## Feign 11 and beyond
+Making _API_ clients easier
+
+Short Term - What we're working on now. â°
+---
+* Response Caching
+ * Support caching of api responses. Allow for users to define under what conditions a response is eligible for caching and what type of caching mechanism should be used.
+ * Support in-memory caching and external cache implementations (EhCache, Google, Spring, etc...)
+* Complete URI Template expression support
+ * Support [level 1 through level 4](https://tools.ietf.org/html/rfc6570#section-1.2) URI template expressions.
+ * Use [URI Templates TCK](https://github.com/uri-templates/uritemplate-test) to verify compliance.
+* `Logger` API refactor
+ * Refactor the `Logger` API to adhere closer to frameworks like SLF4J providing a common mental model for logging within Feign. This model will be used by Feign itself throughout and provide clearer direction on how the `Logger` will be used.
+* `Retry` API refactor
+ * Refactor the `Retry` API to support user-supplied conditions and better control over back-off policies. **This may result in non-backward-compatible breaking changes**
+
+Medium Term - What's up next. â˛
+---
+* Async execution support via `CompletableFuture`
+ * Allow for `Future` chaining and executor management for the request/response lifecycle. **Implementation will require non-backward-compatible breaking changes**. However this feature is required before Reactive execution can be considered.
+* Reactive execution support via [Reactive Streams](https://www.reactive-streams.org/)
+ * For JDK 9+, consider a native implementation that uses `java.util.concurrent.Flow`.
+ * Support for [Project Reactor](https://projectreactor.io/) and [RxJava 2+](https://github.com/ReactiveX/RxJava) implementations on JDK 8.
+
+Long Term - The future âī¸
+---
+* Additional Circuit Breaker Support.
+ * Support additional Circuit Breaker implementations like [Resilience4J](https://resilience4j.readme.io/) and Spring Circuit Breaker
+
+---
+
+# Usage
+
+The feign library is available from [Maven Central](https://central.sonatype.com/artifact/io.github.openfeign/feign-core).
+
+```xml
+
+ io.github.openfeign
+ feign-core
+ ??feign.version??
+
+```
+
+### Basics
+
+Usage typically looks like this, an adaptation of the [canonical Retrofit sample](https://github.com/square/retrofit/blob/master/samples/src/main/java/com/example/retrofit/SimpleService.java).
+
+```java
+interface GitHub {
+ @RequestLine("GET /repos/{owner}/{repo}/contributors")
+ List contributors(@Param("owner") String owner, @Param("repo") String repo);
+
+ @RequestLine("POST /repos/{owner}/{repo}/issues")
+ void createIssue(Issue issue, @Param("owner") String owner, @Param("repo") String repo);
+
+}
+
+public static class Contributor {
+ String login;
+ int contributions;
+}
+
+public static class Issue {
+ String title;
+ String body;
+ List assignees;
+ int milestone;
+ List labels;
+}
+
+public class MyApp {
+ public static void main(String... args) {
+ GitHub github = Feign.builder()
+ .decoder(new GsonDecoder())
+ .target(GitHub.class, "https://api.github.com");
+
+ // Fetch and print a list of the contributors to this library.
+ List contributors = github.contributors("OpenFeign", "feign");
+ for (Contributor contributor : contributors) {
+ System.out.println(contributor.login + " (" + contributor.contributions + ")");
+ }
+ }
+}
+```
+
+### Interface Annotations
+
+Feign annotations define the `Contract` between the interface and how the underlying client
+should work. Feign's default contract defines the following annotations:
+
+| Annotation | Interface Target | Usage |
+|----------------|------------------|-------|
+| `@RequestLine` | Method | Defines the `HttpMethod` and `UriTemplate` for request. `Expressions`, values wrapped in curly-braces `{expression}` are resolved using their corresponding `@Param` annotated parameters. |
+| `@Param` | Parameter | Defines a template variable, whose value will be used to resolve the corresponding template `Expression`, by name provided as annotation value. If value is missing it will try to get the name from bytecode method parameter name (if the code was compiled with `-parameters` flag). |
+| `@Headers` | Method, Type | Defines a `HeaderTemplate`; a variation on a `UriTemplate`. that uses `@Param` annotated values to resolve the corresponding `Expressions`. When used on a `Type`, the template will be applied to every request. When used on a `Method`, the template will apply only to the annotated method. |
+| `@QueryMap` | Parameter | Defines a `Map` of name-value pairs, or POJO, to expand into a query string. |
+| `@HeaderMap` | Parameter | Defines a `Map` of name-value pairs, to expand into `Http Headers` |
+| `@Body` | Method | Defines a `Template`, similar to a `UriTemplate` and `HeaderTemplate`, that uses `@Param` annotated values to resolve the corresponding `Expressions`.|
+
+
+> **Overriding the Request Line**
+>
+> If there is a need to target a request to a different host then the one supplied when the Feign client was created, or
+> you want to supply a target host for each request, include a `java.net.URI` parameter and Feign will use that value
+> as the request target.
+>
+> ```java
+> @RequestLine("POST /repos/{owner}/{repo}/issues")
+> void createIssue(URI host, Issue issue, @Param("owner") String owner, @Param("repo") String repo);
+> ```
+>
+
+### Templates and Expressions
+
+Feign `Expressions` represent Simple String Expressions (Level 1) as defined by [URI Template - RFC 6570](https://tools.ietf.org/html/rfc6570). `Expressions` are expanded using
+their corresponding `Param` annotated method parameters.
+
+*Example*
+
+```java
+public interface GitHub {
+
+ @RequestLine("GET /repos/{owner}/{repo}/contributors")
+ List contributors(@Param("owner") String owner, @Param("repo") String repository);
+
+ class Contributor {
+ String login;
+ int contributions;
+ }
+}
+
+public class MyApp {
+ public static void main(String[] args) {
+ GitHub github = Feign.builder()
+ .decoder(new GsonDecoder())
+ .target(GitHub.class, "https://api.github.com");
+
+ /* The owner and repository parameters will be used to expand the owner and repo expressions
+ * defined in the RequestLine.
+ *
+ * the resulting uri will be https://api.github.com/repos/OpenFeign/feign/contributors
+ */
+ github.contributors("OpenFeign", "feign");
+ }
+}
+```
+
+Expressions must be enclosed in curly braces `{}` and may contain regular expression patterns, separated by a colon `:` to restrict
+resolved values. *Example* `owner` must be alphabetic. `{owner:[a-zA-Z]*}`
+
+#### Request Parameter Expansion
+
+`RequestLine` and `QueryMap` templates follow the [URI Template - RFC 6570](https://tools.ietf.org/html/rfc6570) specification for Level 1 templates, which specifies the following:
+
+* Unresolved expressions are omitted.
+* All literals and variable values are pct-encoded, if not already encoded or marked `encoded` via a `@Param` annotation.
+
+We also have limited support for Level 3, Path Style Expressions, with the following restrictions:
+
+* Maps and Lists are expanded by default.
+* Only Single variable templates are supported.
+
+*Examples:*
+
+```
+{;who} ;who=fred
+{;half} ;half=50%25
+{;empty} ;empty
+{;list} ;list=red;list=green;list=blue
+{;map} ;semi=%3B;dot=.;comma=%2C
+```
+
+```java
+public interface MatrixService {
+
+ @RequestLine("GET /repos{;owners}")
+ List contributors(@Param("owners") List owners);
+
+ class Contributor {
+ String login;
+ int contributions;
+ }
+}
+```
+
+If `owners` in the above example is defined as `Matt, Jeff, Susan`, the uri will expand to `/repos;owners=Matt;owners=Jeff;owners=Susan`
+
+For more information see [RFC 6570, Section 3.2.7](https://datatracker.ietf.org/doc/html/rfc6570#section-3.2.7)
+
+#### Undefined vs. Empty Values ####
+
+Undefined expressions are expressions where the value for the expression is an explicit `null` or no value is provided.
+Per [URI Template - RFC 6570](https://tools.ietf.org/html/rfc6570), it is possible to provide an empty value
+for an expression. When Feign resolves an expression, it first determines if the value is defined, if it is then
+the query parameter will remain. If the expression is undefined, the query parameter is removed. See below
+for a complete breakdown.
+
+*Empty String*
+```java
+public void test() {
+ Map parameters = new LinkedHashMap<>();
+ parameters.put("param", "");
+ this.demoClient.test(parameters);
+}
+```
+Result
+```
+http://localhost:8080/test?param=
+```
+
+*Missing*
+```java
+public void test() {
+ Map parameters = new LinkedHashMap<>();
+ this.demoClient.test(parameters);
+}
+```
+Result
+```
+http://localhost:8080/test
+```
+
+*Undefined*
+```java
+public void test() {
+ Map parameters = new LinkedHashMap<>();
+ parameters.put("param", null);
+ this.demoClient.test(parameters);
+}
+```
+Result
+```
+http://localhost:8080/test
+```
+
+See [Advanced Usage](#advanced-usage) for more examples.
+
+> **What about slashes? `/`**
+>
+> @RequestLine templates do not encode slash `/` characters by default. To change this behavior, set the `decodeSlash` property on the `@RequestLine` to `false`.
+
+> **What about plus? `+`**
+>
+> Per the URI specification, a `+` sign is allowed in both the path and query segments of a URI, however, handling of
+> the symbol on the query can be inconsistent. In some legacy systems, the `+` is equivalent to the a space. Feign takes the approach of modern systems, where a
+> `+` symbol should not represent a space and is explicitly encoded as `%2B` when found on a query string.
+>
+> If you wish to use `+` as a space, then use the literal ` ` character or encode the value directly as `%20`
+
+##### Custom Expansion
+
+The `@Param` annotation has an optional property `expander` allowing for complete control over the individual parameter's expansion.
+The `expander` property must reference a class that implements the `Expander` interface:
+
+```java
+public interface Expander {
+ String expand(Object value);
+}
+```
+The result of this method adheres to the same rules stated above. If the result is `null` or an empty string,
+the value is omitted. If the value is not pct-encoded, it will be. See [Custom @Param Expansion](#custom-param-expansion) for more examples.
+
+#### Request Headers Expansion
+
+`Headers` and `HeaderMap` templates follow the same rules as [Request Parameter Expansion](#request-parameter-expansion)
+with the following alterations:
+
+* Unresolved expressions are omitted. If the result is an empty header value, the entire header is removed.
+* No pct-encoding is performed.
+
+See [Headers](#headers) for examples.
+
+> **A Note on `@Param` parameters and their names**:
+>
+> All expressions with the same name, regardless of their position on the `@RequestLine`, `@QueryMap`, `@BodyTemplate`, or `@Headers` will resolve to the same value.
+> In the following example, the value of `contentType`, will be used to resolve both the header and path expression:
+>
+> ```java
+> public interface ContentService {
+> @RequestLine("GET /api/documents/{contentType}")
+> @Headers("Accept: {contentType}")
+> String getDocumentByType(@Param("contentType") String type);
+> }
+>```
+>
+> Keep this in mind when designing your interfaces.
+
+#### Request Body Expansion
+
+`Body` templates follow the same rules as [Request Parameter Expansion](#request-parameter-expansion)
+with the following alterations:
+
+* Unresolved expressions are omitted.
+* Expanded value will **not** be passed through an `Encoder` before being placed on the request body.
+* A `Content-Type` header must be specified. See [Body Templates](#body-templates) for examples.
+
+---
+### Customization
+
+Feign has several aspects that can be customized.
+For simple cases, you can use `Feign.builder()` to construct an API interface with your custom components.
+For request setting, you can use `options(Request.Options options)` on `target()` to set connectTimeout, connectTimeoutUnit, readTimeout, readTimeoutUnit, followRedirects.
+For example:
+
+```java
+interface Bank {
+ @RequestLine("POST /account/{id}")
+ Account getAccountInfo(@Param("id") String id);
+}
+
+public class BankService {
+ public static void main(String[] args) {
+ Bank bank = Feign.builder()
+ .decoder(new AccountDecoder())
+ .options(new Request.Options(10, TimeUnit.SECONDS, 60, TimeUnit.SECONDS, true))
+ .target(Bank.class, "https://api.examplebank.com");
+ }
+}
+```
+
+### Multiple Interfaces
+Feign can produce multiple api interfaces. These are defined as `Target` (default `HardCodedTarget`), which allow for dynamic discovery and decoration of requests prior to execution.
+
+For example, the following pattern might decorate each request with the current url and auth token from the identity service.
+
+```java
+public class CloudService {
+ public static void main(String[] args) {
+ CloudDNS cloudDNS = Feign.builder()
+ .target(new CloudIdentityTarget(user, apiKey));
+ }
+
+ class CloudIdentityTarget extends Target {
+ /* implementation of a Target */
+ }
+}
+```
+
+### Examples
+Feign includes example [GitHub](./example-github) and [Wikipedia](./example-wikipedia) clients. The denominator project can also be scraped for Feign in practice. Particularly, look at its [example daemon](https://github.com/Netflix/denominator/tree/master/example-daemon).
+
+---
+### Integrations
+Feign intends to work well with other Open Source tools. Modules are welcome to integrate with your favorite projects!
+
+### Encoder/Decoder
+
+#### Gson
+[Gson](./gson) includes an encoder and decoder you can use with a JSON API.
+
+Add `GsonEncoder` and/or `GsonDecoder` to your `Feign.Builder` like so:
+
+```java
+public class Example {
+ public static void main(String[] args) {
+ GsonCodec codec = new GsonCodec();
+ GitHub github = Feign.builder()
+ .encoder(new GsonEncoder())
+ .decoder(new GsonDecoder())
+ .target(GitHub.class, "https://api.github.com");
+ }
+}
+```
+
+#### Jackson
+[Jackson](./jackson) includes an encoder and decoder you can use with a JSON API.
+
+Add `JacksonEncoder` and/or `JacksonDecoder` to your `Feign.Builder` like so:
+
+```java
+public class Example {
+ public static void main(String[] args) {
+ GitHub github = Feign.builder()
+ .encoder(new JacksonEncoder())
+ .decoder(new JacksonDecoder())
+ .target(GitHub.class, "https://api.github.com");
+ }
+}
+```
+
+For the lighter weight Jackson Jr, use `JacksonJrEncoder` and `JacksonJrDecoder` from
+the [Jackson Jr Module](./jackson-jr).
+
+#### Moshi
+[Moshi](./moshi) includes an encoder and decoder you can use with a JSON API.
+Add `MoshiEncoder` and/or `MoshiDecoder` to your `Feign.Builder` like so:
+
+```java
+GitHub github = Feign.builder()
+ .encoder(new MoshiEncoder())
+ .decoder(new MoshiDecoder())
+ .target(GitHub.class, "https://api.github.com");
+```
+
+#### Sax
+[SaxDecoder](./sax) allows you to decode XML in a way that is compatible with normal JVM and also Android environments.
+
+Here's an example of how to configure Sax response parsing:
+```java
+public class Example {
+ public static void main(String[] args) {
+ Api api = Feign.builder()
+ .decoder(SAXDecoder.builder()
+ .registerContentHandler(UserIdHandler.class)
+ .build())
+ .target(Api.class, "https://apihost");
+ }
+}
+```
+
+#### JAXB
+[JAXB](./jaxb) includes an encoder and decoder you can use with an XML API.
+
+Add `JAXBEncoder` and/or `JAXBDecoder` to your `Feign.Builder` like so:
+
+```java
+public class Example {
+ public static void main(String[] args) {
+ Api api = Feign.builder()
+ .encoder(new JAXBEncoder())
+ .decoder(new JAXBDecoder())
+ .target(Api.class, "https://apihost");
+ }
+}
+```
+
+#### SOAP
+[SOAP](./soap) includes an encoder and decoder you can use with an XML API.
+
+
+This module adds support for encoding and decoding SOAP Body objects via JAXB and SOAPMessage. It also provides SOAPFault decoding capabilities by wrapping them into the original `javax.xml.ws.soap.SOAPFaultException`, so that you'll only need to catch `SOAPFaultException` in order to handle SOAPFault.
+
+Add `SOAPEncoder` and/or `SOAPDecoder` to your `Feign.Builder` like so:
+
+```java
+public class Example {
+ public static void main(String[] args) {
+ Api api = Feign.builder()
+ .encoder(new SOAPEncoder(jaxbFactory))
+ .decoder(new SOAPDecoder(jaxbFactory))
+ .errorDecoder(new SOAPErrorDecoder())
+ .target(MyApi.class, "http://api");
+ }
+}
+```
+
+NB: you may also need to add `SOAPErrorDecoder` if SOAP Faults are returned in response with error http codes (4xx, 5xx, ...)
+
+#### Fastjson2
+
+[fastjson2](./fastjson2) includes an encoder and decoder you can use with a JSON API.
+
+Add `Fastjson2Encoder` and/or `Fastjson2Decoder` to your `Feign.Builder` like so:
+
+```java
+public class Example {
+ public static void main(String[] args) {
+ GitHub github = Feign.builder()
+ .encoder(new Fastjson2Encoder())
+ .decoder(new Fastjson2Decoder())
+ .target(GitHub.class, "https://api.github.com");
+ }
+}
+```
+
+### Contract
+
+#### JAX-RS
+[JAXRSContract](./jaxrs) overrides annotation processing to instead use standard ones supplied by the JAX-RS specification. This is currently targeted at the 1.1 spec.
+
+Here's the example above re-written to use JAX-RS:
+```java
+interface GitHub {
+ @GET @Path("/repos/{owner}/{repo}/contributors")
+ List contributors(@PathParam("owner") String owner, @PathParam("repo") String repo);
+}
+
+public class Example {
+ public static void main(String[] args) {
+ GitHub github = Feign.builder()
+ .contract(new JAXRSContract())
+ .target(GitHub.class, "https://api.github.com");
+ }
+}
+```
+
+### Client
+
+#### OkHttp
+[OkHttpClient](./okhttp) directs Feign's http requests to [OkHttp](http://square.github.io/okhttp/), which enables SPDY and better network control.
+
+To use OkHttp with Feign, add the OkHttp module to your classpath. Then, configure Feign to use the OkHttpClient:
+
+```java
+public class Example {
+ public static void main(String[] args) {
+ GitHub github = Feign.builder()
+ .client(new OkHttpClient())
+ .target(GitHub.class, "https://api.github.com");
+ }
+}
+```
+
+#### Ribbon
+[RibbonClient](./ribbon) overrides URL resolution of Feign's client, adding smart routing and resiliency capabilities provided by [Ribbon](https://github.com/Netflix/ribbon).
+
+Integration requires you to pass your ribbon client name as the host part of the url, for example `myAppProd`.
+```java
+public class Example {
+ public static void main(String[] args) {
+ MyService api = Feign.builder()
+ .client(RibbonClient.create())
+ .target(MyService.class, "https://myAppProd");
+ }
+}
+```
+
+#### Java 11 Http2
+[Http2Client](./java11) directs Feign's http requests to Java11 [New HTTP/2 Client](https://openjdk.java.net/jeps/321) that implements HTTP/2.
+
+To use New HTTP/2 Client with Feign, use Java SDK 11. Then, configure Feign to use the Http2Client:
+
+```java
+GitHub github = Feign.builder()
+ .client(new Http2Client())
+ .target(GitHub.class, "https://api.github.com");
+```
+
+### Breaker
+
+#### Hystrix
+[HystrixFeign](./hystrix) configures circuit breaker support provided by [Hystrix](https://github.com/Netflix/Hystrix).
+
+To use Hystrix with Feign, add the Hystrix module to your classpath. Then use the `HystrixFeign` builder:
+
+```java
+public class Example {
+ public static void main(String[] args) {
+ MyService api = HystrixFeign.builder().target(MyService.class, "https://myAppProd");
+ }
+}
+```
+
+### Logger
+
+#### SLF4J
+[SLF4JModule](./slf4j) allows directing Feign's logging to [SLF4J](http://www.slf4j.org/), allowing you to easily use a logging backend of your choice (Logback, Log4J, etc.)
+
+To use SLF4J with Feign, add both the SLF4J module and an SLF4J binding of your choice to your classpath. Then, configure Feign to use the Slf4jLogger:
+
+```java
+public class Example {
+ public static void main(String[] args) {
+ GitHub github = Feign.builder()
+ .logger(new Slf4jLogger())
+ .logLevel(Level.FULL)
+ .target(GitHub.class, "https://api.github.com");
+ }
+}
+```
+
+### Decoders
+`Feign.builder()` allows you to specify additional configuration such as how to decode a response.
+
+If any methods in your interface return types besides `Response`, `String`, `byte[]` or `void`, you'll need to configure a non-default `Decoder`.
+
+Here's how to configure JSON decoding (using the `feign-gson` extension):
+
+```java
+public class Example {
+ public static void main(String[] args) {
+ GitHub github = Feign.builder()
+ .decoder(new GsonDecoder())
+ .target(GitHub.class, "https://api.github.com");
+ }
+}
+```
+
+If you need to pre-process the response before give it to the Decoder, you can use the `mapAndDecode` builder method.
+An example use case is dealing with an API that only serves jsonp, you will maybe need to unwrap the jsonp before
+send it to the Json decoder of your choice:
+
+```java
+public class Example {
+ public static void main(String[] args) {
+ JsonpApi jsonpApi = Feign.builder()
+ .mapAndDecode((response, type) -> jsopUnwrap(response, type), new GsonDecoder())
+ .target(JsonpApi.class, "https://some-jsonp-api.com");
+ }
+}
+```
+
+If any methods in your interface return type `Stream`, you'll need to configure a `StreamDecoder`.
+
+Here's how to configure Stream decoder without delegate decoder:
+
+```java
+public class Example {
+ public static void main(String[] args) {
+ GitHub github = Feign.builder()
+ .decoder(StreamDecoder.create((r, t) -> {
+ BufferedReader bufferedReader = new BufferedReader(r.body().asReader(UTF_8));
+ return bufferedReader.lines().iterator();
+ }))
+ .target(GitHub.class, "https://api.github.com");
+ }
+}
+```
+
+Here's how to configure Stream decoder with delegate decoder:
+
+```java
+
+public class Example {
+ public static void main(String[] args) {
+ GitHub github = Feign.builder()
+ .decoder(StreamDecoder.create((r, t) -> {
+ BufferedReader bufferedReader = new BufferedReader(r.body().asReader(UTF_8));
+ return bufferedReader.lines().iterator();
+ }, (r, t) -> "this is delegate decoder"))
+ .target(GitHub.class, "https://api.github.com");
+ }
+}
+```
+
+### Encoders
+The simplest way to send a request body to a server is to define a `POST` method that has a `String` or `byte[]` parameter without any annotations on it. You will likely need to add a `Content-Type` header.
+
+```java
+interface LoginClient {
+ @RequestLine("POST /")
+ @Headers("Content-Type: application/json")
+ void login(String content);
+}
+
+public class Example {
+ public static void main(String[] args) {
+ client.login("{\"user_name\": \"denominator\", \"password\": \"secret\"}");
+ }
+}
+```
+
+By configuring an `Encoder`, you can send a type-safe request body. Here's an example using the `feign-gson` extension:
+
+```java
+static class Credentials {
+ final String user_name;
+ final String password;
+
+ Credentials(String user_name, String password) {
+ this.user_name = user_name;
+ this.password = password;
+ }
+}
+
+interface LoginClient {
+ @RequestLine("POST /")
+ void login(Credentials creds);
+}
+
+public class Example {
+ public static void main(String[] args) {
+ LoginClient client = Feign.builder()
+ .encoder(new GsonEncoder())
+ .target(LoginClient.class, "https://foo.com");
+
+ client.login(new Credentials("denominator", "secret"));
+ }
+}
+```
+
+### @Body templates
+The `@Body` annotation indicates a template to expand using parameters annotated with `@Param`. You will likely need to add a `Content-Type` header.
+
+```java
+interface LoginClient {
+
+ @RequestLine("POST /")
+ @Headers("Content-Type: application/xml")
+ @Body(" ")
+ void xml(@Param("user_name") String user, @Param("password") String password);
+
+ @RequestLine("POST /")
+ @Headers("Content-Type: application/json")
+ // json curly braces must be escaped!
+ @Body("%7B\"user_name\": \"{user_name}\", \"password\": \"{password}\"%7D")
+ void json(@Param("user_name") String user, @Param("password") String password);
+}
+
+public class Example {
+ public static void main(String[] args) {
+ client.xml("denominator", "secret"); //
+ client.json("denominator", "secret"); // {"user_name": "denominator", "password": "secret"}
+ }
+}
+```
+
+### Headers
+Feign supports settings headers on requests either as part of the api or as part of the client
+depending on the use case.
+
+#### Set headers using apis
+In cases where specific interfaces or calls should always have certain header values set, it
+makes sense to define headers as part of the api.
+
+Static headers can be set on an api interface or method using the `@Headers` annotation.
+
+```java
+@Headers("Accept: application/json")
+interface BaseApi {
+ @Headers("Content-Type: application/json")
+ @RequestLine("PUT /api/{key}")
+ void put(@Param("key") String key, V value);
+}
+```
+
+Methods can specify dynamic content for static headers using variable expansion in `@Headers`.
+
+```java
+public interface Api {
+ @RequestLine("POST /")
+ @Headers("X-Ping: {token}")
+ void post(@Param("token") String token);
+}
+```
+
+In cases where both the header field keys and values are dynamic and the range of possible keys cannot
+be known ahead of time and may vary between different method calls in the same api/client (e.g. custom
+metadata header fields such as "x-amz-meta-\*" or "x-goog-meta-\*"), a Map parameter can be annotated
+with `HeaderMap` to construct a query that uses the contents of the map as its header parameters.
+
+```java
+public interface Api {
+ @RequestLine("POST /")
+ void post(@HeaderMap Map headerMap);
+}
+```
+
+These approaches specify header entries as part of the api and do not require any customizations
+when building the Feign client.
+
+#### Setting headers per target
+To customize headers for each request method on a Target, a RequestInterceptor can be used. RequestInterceptors can be
+shared across Target instances and are expected to be thread-safe. RequestInterceptors are applied to all request
+methods on a Target.
+
+If you need per method customization, a custom Target is required, as the a RequestInterceptor does not have access to
+the current method metadata.
+
+For an example of setting headers using a `RequestInterceptor`, see the `Request Interceptors` section.
+
+Headers can be set as part of a custom `Target`.
+
+```java
+ static class DynamicAuthTokenTarget implements Target {
+ public DynamicAuthTokenTarget(Class clazz,
+ UrlAndTokenProvider provider,
+ ThreadLocal requestIdProvider);
+
+ @Override
+ public Request apply(RequestTemplate input) {
+ TokenIdAndPublicURL urlAndToken = provider.get();
+ if (input.url().indexOf("http") != 0) {
+ input.insert(0, urlAndToken.publicURL);
+ }
+ input.header("X-Auth-Token", urlAndToken.tokenId);
+ input.header("X-Request-ID", requestIdProvider.get());
+
+ return input.request();
+ }
+ }
+
+ public class Example {
+ public static void main(String[] args) {
+ Bank bank = Feign.builder()
+ .target(new DynamicAuthTokenTarget(Bank.class, provider, requestIdProvider));
+ }
+ }
+```
+
+These approaches depend on the custom `RequestInterceptor` or `Target` being set on the Feign
+client when it is built and can be used as a way to set headers on all api calls on a per-client
+basis. This can be useful for doing things such as setting an authentication token in the header
+of all api requests on a per-client basis. The methods are run when the api call is made on the
+thread that invokes the api call, which allows the headers to be set dynamically at call time and
+in a context-specific manner -- for example, thread-local storage can be used to set different
+header values depending on the invoking thread, which can be useful for things such as setting
+thread-specific trace identifiers for requests.
+
+### Advanced usage
+
+#### Base Apis
+In many cases, apis for a service follow the same conventions. Feign supports this pattern via single-inheritance interfaces.
+
+Consider the example:
+```java
+interface BaseAPI {
+ @RequestLine("GET /health")
+ String health();
+
+ @RequestLine("GET /all")
+ List all();
+}
+```
+
+You can define and target a specific api, inheriting the base methods.
+```java
+interface CustomAPI extends BaseAPI {
+ @RequestLine("GET /custom")
+ String custom();
+}
+```
+
+In many cases, resource representations are also consistent. For this reason, type parameters are supported on the base api interface.
+
+```java
+@Headers("Accept: application/json")
+interface BaseApi {
+
+ @RequestLine("GET /api/{key}")
+ V get(@Param("key") String key);
+
+ @RequestLine("GET /api")
+ List list();
+
+ @Headers("Content-Type: application/json")
+ @RequestLine("PUT /api/{key}")
+ void put(@Param("key") String key, V value);
+}
+
+interface FooApi extends BaseApi { }
+
+interface BarApi extends BaseApi { }
+```
+
+#### Logging
+You can log the http messages going to and from the target by setting up a `Logger`. Here's the easiest way to do that:
+```java
+public class Example {
+ public static void main(String[] args) {
+ GitHub github = Feign.builder()
+ .decoder(new GsonDecoder())
+ .logger(new Logger.JavaLogger("GitHub.Logger").appendToFile("logs/http.log"))
+ .logLevel(Logger.Level.FULL)
+ .target(GitHub.class, "https://api.github.com");
+ }
+}
+```
+
+> **A Note on JavaLogger**:
+> Avoid using of default ```JavaLogger()``` constructor - it was marked as deprecated and will be removed soon.
+
+The SLF4JLogger (see above) may also be of interest.
+
+To filter out sensitive information like authorization or tokens
+override methods `shouldLogRequestHeader` or `shouldLogResponseHeader`.
+
+#### Request Interceptors
+When you need to change all requests, regardless of their target, you'll want to configure a `RequestInterceptor`.
+For example, if you are acting as an intermediary, you might want to propagate the `X-Forwarded-For` header.
+
+```java
+static class ForwardedForInterceptor implements RequestInterceptor {
+ @Override public void apply(RequestTemplate template) {
+ template.header("X-Forwarded-For", "origin.host.com");
+ }
+}
+
+public class Example {
+ public static void main(String[] args) {
+ Bank bank = Feign.builder()
+ .decoder(accountDecoder)
+ .requestInterceptor(new ForwardedForInterceptor())
+ .target(Bank.class, "https://api.examplebank.com");
+ }
+}
+```
+
+Another common example of an interceptor would be authentication, such as using the built-in `BasicAuthRequestInterceptor`.
+
+```java
+public class Example {
+ public static void main(String[] args) {
+ Bank bank = Feign.builder()
+ .decoder(accountDecoder)
+ .requestInterceptor(new BasicAuthRequestInterceptor(username, password))
+ .target(Bank.class, "https://api.examplebank.com");
+ }
+}
+```
+
+#### Custom @Param Expansion
+Parameters annotated with `Param` expand based on their `toString`. By
+specifying a custom `Param.Expander`, users can control this behavior,
+for example formatting dates.
+
+```java
+public interface Api {
+ @RequestLine("GET /?since={date}") Result list(@Param(value = "date", expander = DateToMillis.class) Date date);
+}
+```
+
+#### Dynamic Query Parameters
+A Map parameter can be annotated with `QueryMap` to construct a query that uses the contents of the map as its query parameters.
+
+```java
+public interface Api {
+ @RequestLine("GET /find")
+ V find(@QueryMap Map queryMap);
+}
+```
+
+This may also be used to generate the query parameters from a POJO object using a `QueryMapEncoder`.
+
+```java
+public interface Api {
+ @RequestLine("GET /find")
+ V find(@QueryMap CustomPojo customPojo);
+}
+```
+
+When used in this manner, without specifying a custom `QueryMapEncoder`, the query map will be generated using member variable names as query parameter names. You can annotate a specific field of `CustomPojo` with the `@Param` annotation to specify a different name to the query parameter. The following POJO will generate query params of "/find?name={name}&number={number}®ion_id={regionId}" (order of included query parameters not guaranteed, and as usual, if any value is null, it will be left out).
+
+```java
+public class CustomPojo {
+ private final String name;
+ private final int number;
+ @Param("region_id")
+ private final String regionId;
+
+ public CustomPojo (String name, int number, String regionId) {
+ this.name = name;
+ this.number = number;
+ this.regionId = regionId;
+ }
+}
+```
+
+To setup a custom `QueryMapEncoder`:
+
+```java
+public class Example {
+ public static void main(String[] args) {
+ MyApi myApi = Feign.builder()
+ .queryMapEncoder(new MyCustomQueryMapEncoder())
+ .target(MyApi.class, "https://api.hostname.com");
+ }
+}
+```
+
+When annotating objects with @QueryMap, the 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 BeanQueryMapEncoder
+
+```java
+public class Example {
+ public static void main(String[] args) {
+ MyApi myApi = Feign.builder()
+ .queryMapEncoder(new BeanQueryMapEncoder())
+ .target(MyApi.class, "https://api.hostname.com");
+ }
+}
+```
+
+### Error Handling
+If you need more control over handling unexpected responses, Feign instances can
+register a custom `ErrorDecoder` via the builder.
+
+```java
+public class Example {
+ public static void main(String[] args) {
+ MyApi myApi = Feign.builder()
+ .errorDecoder(new MyErrorDecoder())
+ .target(MyApi.class, "https://api.hostname.com");
+ }
+}
+```
+
+All responses that result in an HTTP status not in the 2xx range will trigger the `ErrorDecoder`'s `decode` method, allowing
+you to handle the response, wrap the failure into a custom exception or perform any additional processing.
+If you want to retry the request again, throw a `RetryableException`. This will invoke the registered
+`Retryer`.
+
+### Retry
+Feign, by default, will automatically retry `IOException`s, regardless of HTTP method, treating them as transient network
+related exceptions, and any `RetryableException` thrown from an `ErrorDecoder`. To customize this
+behavior, register a custom `Retryer` instance via the builder.
+
+The following example shows how to refresh token and retry with `ErrorDecoder` and `Retryer` when received a 401 response.
+
+```java
+public class Example {
+ public static void main(String[] args) {
+ var github = Feign.builder()
+ .decoder(new GsonDecoder())
+ .retryer(new MyRetryer(100, 3))
+ .errorDecoder(new MyErrorDecoder())
+ .target(Github.class, "https://api.github.com");
+
+ var contributors = github.contributors("foo", "bar", "invalid_token");
+ for (var contributor : contributors) {
+ System.out.println(contributor.login + " " + contributor.contributions);
+ }
+ }
+
+ static class MyErrorDecoder implements ErrorDecoder {
+
+ private final ErrorDecoder defaultErrorDecoder = new Default();
+
+ @Override
+ public Exception decode(String methodKey, Response response) {
+ // wrapper 401 to RetryableException in order to retry
+ if (response.status() == 401) {
+ return new RetryableException(response.status(), response.reason(), response.request().httpMethod(), null, response.request());
+ }
+ return defaultErrorDecoder.decode(methodKey, response);
+ }
+ }
+
+ static class MyRetryer implements Retryer {
+
+ private final long period;
+ private final int maxAttempts;
+ private int attempt = 1;
+
+ public MyRetryer(long period, int maxAttempts) {
+ this.period = period;
+ this.maxAttempts = maxAttempts;
+ }
+
+ @Override
+ public void continueOrPropagate(RetryableException e) {
+ if (++attempt > maxAttempts) {
+ throw e;
+ }
+ if (e.status() == 401) {
+ // remove Authorization first, otherwise Feign will add a new Authorization header
+ // cause github responses a 400 bad request
+ e.request().requestTemplate().removeHeader("Authorization");
+ e.request().requestTemplate().header("Authorization", "Bearer " + getNewToken());
+ try {
+ Thread.sleep(period);
+ } catch (InterruptedException ex) {
+ throw e;
+ }
+ } else {
+ throw e;
+ }
+ }
+
+ // Access an external api to obtain new token
+ // In this example, we can simply return a fixed token to demonstrate how Retryer works
+ private String getNewToken() {
+ return "newToken";
+ }
+
+ @Override
+ public Retryer clone() {
+ return new MyRetryer(period, maxAttempts);
+ }
+}
+```
+
+`Retryer`s are responsible for determining if a retry should occur by returning either a `true` or
+`false` from the method `continueOrPropagate(RetryableException e);` A `Retryer` instance will be
+created for each `Client` execution, allowing you to maintain state bewteen each request if desired.
+
+If the retry is determined to be unsuccessful, the last `RetryException` will be thrown. To throw the original
+cause that led to the unsuccessful retry, build your Feign client with the `exceptionPropagationPolicy()` option.
+
+#### Response Interceptor
+If you need to treat what would otherwise be an error as a success and return a result rather than throw an exception then you may use a `ResponseInterceptor`.
+
+As an example Feign includes a simple `RedirectionInterceptor` that can be used to extract the location header from redirection responses.
+```java
+public interface Api {
+ // returns a 302 response
+ @RequestLine("GET /location")
+ String location();
+}
+
+public class MyApp {
+ public static void main(String[] args) {
+ // Configure the HTTP client to ignore redirection
+ Api api = Feign.builder()
+ .options(new Options(10, TimeUnit.SECONDS, 60, TimeUnit.SECONDS, false))
+ .responseInterceptor(new RedirectionInterceptor())
+ .target(Api.class, "https://redirect.example.com");
+ }
+}
+```
+
+### Metrics
+By default, feign won't collect any metrics.
+
+But, it's possible to add metric collection capabilities to any feign client.
+
+Metric Capabilities provide a first-class Metrics API that users can tap into to gain insight into the request/response lifecycle.
+
+> **A Note on Metrics modules**:
+>
+> All the metric-integrations are built in separate modules and not available in the `feign-core` module. You will need to add them to your dependencies.
+
+#### Dropwizard Metrics 4
+
+```
+public class MyApp {
+ public static void main(String[] args) {
+ GitHub github = Feign.builder()
+ .addCapability(new Metrics4Capability())
+ .target(GitHub.class, "https://api.github.com");
+
+ github.contributors("OpenFeign", "feign");
+ // metrics will be available from this point onwards
+ }
+}
+```
+
+#### Dropwizard Metrics 5
+
+```
+public class MyApp {
+ public static void main(String[] args) {
+ GitHub github = Feign.builder()
+ .addCapability(new Metrics5Capability())
+ .target(GitHub.class, "https://api.github.com");
+
+ github.contributors("OpenFeign", "feign");
+ // metrics will be available from this point onwards
+ }
+}
+```
+
+#### Micrometer
+
+```
+public class MyApp {
+ public static void main(String[] args) {
+ GitHub github = Feign.builder()
+ .addCapability(new MicrometerCapability())
+ .target(GitHub.class, "https://api.github.com");
+
+ github.contributors("OpenFeign", "feign");
+ // metrics will be available from this point onwards
+ }
+}
+```
+
+#### Static and Default Methods
+Interfaces targeted by Feign may have static or default methods (if using Java 8+).
+These allows Feign clients to contain logic that is not expressly defined by the underlying API.
+For example, static methods make it easy to specify common client build configurations; default methods can be used to compose queries or define default parameters.
+
+```java
+interface GitHub {
+ @RequestLine("GET /repos/{owner}/{repo}/contributors")
+ List contributors(@Param("owner") String owner, @Param("repo") String repo);
+
+ @RequestLine("GET /users/{username}/repos?sort={sort}")
+ List repos(@Param("username") String owner, @Param("sort") String sort);
+
+ default List repos(String owner) {
+ return repos(owner, "full_name");
+ }
+
+ /**
+ * Lists all contributors for all repos owned by a user.
+ */
+ default List contributors(String user) {
+ MergingContributorList contributors = new MergingContributorList();
+ for(Repo repo : this.repos(owner)) {
+ contributors.addAll(this.contributors(user, repo.getName()));
+ }
+ return contributors.mergeResult();
+ }
+
+ static GitHub connect() {
+ return Feign.builder()
+ .decoder(new GsonDecoder())
+ .target(GitHub.class, "https://api.github.com");
+ }
+}
+```
+
+
+### Async execution via `CompletableFuture`
+
+Feign 10.8 introduces a new builder `AsyncFeign` that allow methods to return `CompletableFuture` instances.
+
+```java
+interface GitHub {
+ @RequestLine("GET /repos/{owner}/{repo}/contributors")
+ CompletableFuture> contributors(@Param("owner") String owner, @Param("repo") String repo);
+}
+
+public class MyApp {
+ public static void main(String... args) {
+ GitHub github = AsyncFeign.builder()
+ .decoder(new GsonDecoder())
+ .target(GitHub.class, "https://api.github.com");
+
+ // Fetch and print a list of the contributors to this library.
+ CompletableFuture> contributors = github.contributors("OpenFeign", "feign");
+ for (Contributor contributor : contributors.get(1, TimeUnit.SECONDS)) {
+ System.out.println(contributor.login + " (" + contributor.contributions + ")");
+ }
+ }
+}
+```
+
+Initial implementation include 2 async clients:
+- `AsyncClient.Default`
+- `AsyncApacheHttp5Client`
+
+## Mavenâs Bill of Material (BOM)
+
+Keeping all feign libraries on the same version is essential to avoid incompatible binaries. When consuming external dependencies, can be tricky to make sure only one version is present.
+
+With that in mind, feign build generates a module called `feign-bom` that locks the versions for all `feign-*` modules.
+
+The Bill Of Material is a special POM file that groups dependency versions that are known to be valid and tested to work together. This will reduce the developersâ pain of having to test the compatibility of different versions and reduce the chances to have version mismatches.
+
+
+[Here](https://repo1.maven.org/maven2/io/github/openfeign/feign-bom/11.9/feign-bom-11.9.pom) is one example of what feign BOM file looks like.
+
+#### Usage
+
+```xml
+
+
+...
+
+
+
+
+ io.github.openfeign
+ feign-bom
+ ??feign.version??
+ pom
+ import
+
+
+
+
+```
+# Form Encoder
+
+[](https://travis-ci.org/OpenFeign/feign-form)
+[](https://maven-badges.herokuapp.com/maven-central/io.github.openfeign.form/feign-form)
+[](http://www.apache.org/licenses/LICENSE-2.0.html)
+
+This module adds support for encoding **application/x-www-form-urlencoded** and **multipart/form-data** forms.
+
+## Add dependency
+
+Include the dependency to your app:
+
+**Maven**:
+
+```xml
+
+ ...
+
+ io.github.openfeign.form
+ feign-form
+ 4.0.0
+
+ ...
+
+```
+
+**Gradle**:
+
+```groovy
+compile 'io.github.openfeign.form:feign-form:4.0.0'
+```
+
+## Requirements
+
+The `feign-form` extension depend on `OpenFeign` and its *concrete* versions:
+
+- all `feign-form` releases before **3.5.0** works with `OpenFeign` **9.\*** versions;
+- starting from `feign-form`'s version **3.5.0**, the module works with `OpenFeign` **10.1.0** versions and greater.
+
+> **IMPORTANT:** there is no backward compatibility and no any gurantee that the `feign-form`'s versions after **3.5.0** work with `OpenFeign` before **10.\***. `OpenFeign` was refactored in 10th release, so the best approach - use the freshest `OpenFeign` and `feign-form` versions.
+
+Notes:
+
+- [spring-cloud-openfeign](https://github.com/spring-cloud/spring-cloud-openfeign) uses `OpenFeign` **9.\*** till **v2.0.3.RELEASE** and uses **10.\*** after. Anyway, the dependency already has suitable `feign-form` version, see [dependency pom](https://github.com/spring-cloud/spring-cloud-openfeign/blob/master/spring-cloud-openfeign-dependencies/pom.xml#L19), so you don't need to specify it separately;
+
+- `spring-cloud-starter-feign` is a **deprecated** dependency and it always uses the `OpenFeign`'s **9.\*** versions.
+
+## Usage
+
+Add `FormEncoder` to your `Feign.Builder` like so:
+
+```java
+SomeApi github = Feign.builder()
+ .encoder(new FormEncoder())
+ .target(SomeApi.class, "http://api.some.org");
+```
+
+Moreover, you can decorate the existing encoder, for example JsonEncoder like this:
+
+```java
+SomeApi github = Feign.builder()
+ .encoder(new FormEncoder(new JacksonEncoder()))
+ .target(SomeApi.class, "http://api.some.org");
+```
+
+And use them together:
+
+```java
+interface SomeApi {
+
+ @RequestLine("POST /json")
+ @Headers("Content-Type: application/json")
+ void json (Dto dto);
+
+ @RequestLine("POST /form")
+ @Headers("Content-Type: application/x-www-form-urlencoded")
+ void from (@Param("field1") String field1, @Param("field2") String[] values);
+}
+```
+
+You can specify two types of encoding forms by `Content-Type` header.
+
+### application/x-www-form-urlencoded
+
+```java
+interface SomeApi {
+
+ @RequestLine("POST /authorization")
+ @Headers("Content-Type: application/x-www-form-urlencoded")
+ void authorization (@Param("email") String email, @Param("password") String password);
+
+ // Group all parameters within a POJO
+ @RequestLine("POST /user")
+ @Headers("Content-Type: application/x-www-form-urlencoded")
+ void addUser (User user);
+
+ class User {
+
+ Integer id;
+
+ String name;
+ }
+}
+```
+
+### multipart/form-data
+
+```java
+interface SomeApi {
+
+ // File parameter
+ @RequestLine("POST /send_photo")
+ @Headers("Content-Type: multipart/form-data")
+ void sendPhoto (@Param("is_public") Boolean isPublic, @Param("photo") File photo);
+
+ // byte[] parameter
+ @RequestLine("POST /send_photo")
+ @Headers("Content-Type: multipart/form-data")
+ void sendPhoto (@Param("is_public") Boolean isPublic, @Param("photo") byte[] photo);
+
+ // FormData parameter
+ @RequestLine("POST /send_photo")
+ @Headers("Content-Type: multipart/form-data")
+ void sendPhoto (@Param("is_public") Boolean isPublic, @Param("photo") FormData photo);
+
+ // Group all parameters within a POJO
+ @RequestLine("POST /send_photo")
+ @Headers("Content-Type: multipart/form-data")
+ void sendPhoto (MyPojo pojo);
+
+ class MyPojo {
+
+ @FormProperty("is_public")
+ Boolean isPublic;
+
+ File photo;
+ }
+}
+```
+
+In the example above, the `sendPhoto` method uses the `photo` parameter using three different supported types.
+
+* `File` will use the File's extension to detect the `Content-Type`;
+* `byte[]` will use `application/octet-stream` as `Content-Type`;
+* `FormData` will use the `FormData`'s `Content-Type` and `fileName`;
+* Client's custom POJO for grouping parameters (including types above).
+
+`FormData` is custom object that wraps a `byte[]` and defines a `Content-Type` and `fileName` like this:
+
+```java
+ FormData formData = new FormData("image/png", "filename.png", myDataAsByteArray);
+ someApi.sendPhoto(true, formData);
+```
+
+### Spring MultipartFile and Spring Cloud Netflix @FeignClient support
+
+You can also use Form Encoder with Spring `MultipartFile` and `@FeignClient`.
+
+Include the dependencies to your project's pom.xml file:
+
+```xml
+
+
+ io.github.openfeign.form
+ feign-form
+ 4.0.0
+
+
+ io.github.openfeign.form
+ feign-form-spring
+ 4.0.0
+
+
+```
+
+```java
+@FeignClient(
+ name = "file-upload-service",
+ configuration = FileUploadServiceClient.MultipartSupportConfig.class
+)
+public interface FileUploadServiceClient extends IFileUploadServiceClient {
+
+ public class MultipartSupportConfig {
+
+ @Autowired
+ private ObjectFactory messageConverters;
+
+ @Bean
+ public Encoder feignFormEncoder () {
+ return new SpringFormEncoder(new SpringEncoder(messageConverters));
+ }
+ }
+}
+```
+
+Or, if you don't need Spring's standard encoder:
+
+```java
+@FeignClient(
+ name = "file-upload-service",
+ configuration = FileUploadServiceClient.MultipartSupportConfig.class
+)
+public interface FileUploadServiceClient extends IFileUploadServiceClient {
+
+ public class MultipartSupportConfig {
+
+ @Bean
+ public Encoder feignFormEncoder () {
+ return new SpringFormEncoder();
+ }
+ }
+}
+```
+
+Thanks to [tf-haotri-pham](https://github.com/tf-haotri-pham) for his feature, which makes use of Apache commons-fileupload library, which handles the parsing of the multipart response. The body data parts are held as byte arrays in memory.
+
+To use this feature, include SpringManyMultipartFilesReader in the list of message converters for the Decoder and have the Feign client return an array of MultipartFile:
+
+```java
+@FeignClient(
+ name = "${feign.name}",
+ url = "${feign.url}"
+ configuration = DownloadClient.ClientConfiguration.class
+)
+public interface DownloadClient {
+
+ @RequestMapping("/multipart/download/{fileId}")
+ MultipartFile[] download(@PathVariable("fileId") String fileId);
+
+ class ClientConfiguration {
+
+ @Autowired
+ private ObjectFactory messageConverters;
+
+ @Bean
+ public Decoder feignDecoder () {
+ List> springConverters =
+ messageConverters.getObject().getConverters();
+
+ List> decoderConverters =
+ new ArrayList>(springConverters.size() + 1);
+
+ decoderConverters.addAll(springConverters);
+ decoderConverters.add(new SpringManyMultipartFilesReader(4096));
+
+ HttpMessageConverters httpMessageConverters = new HttpMessageConverters(decoderConverters);
+
+ return new SpringDecoder(new ObjectFactory() {
+
+ @Override
+ public HttpMessageConverters getObject() {
+ return httpMessageConverters;
+ }
+ });
+ }
+ }
+}
+```
diff --git a/RELEASE.md b/RELEASE.md
new file mode 100644
index 000000000..2714f5486
--- /dev/null
+++ b/RELEASE.md
@@ -0,0 +1,68 @@
+# Feign Release Process
+
+This repo uses [semantic versions](http://semver.org/). Please keep this in mind when choosing version numbers.
+
+1. **Alert others you are releasing**
+
+ There should be no commits made to master while the release is in progress (about 10 minutes). Before you start
+ a release, alert others on [gitter](https://gitter.im/OpenFeign/feign) so that they don't accidentally merge
+ anything. If they do, and the build fails because of that, you'll have to recreate the release tag described below.
+
+1. **Push a git tag**
+
+ Prepare the next release by running the [release script](scripts/release.sh) from a clean checkout of the master branch.
+ This script will:
+ * Update all versions to the next release.
+ * Tag the release.
+ * Update all versions to the next development version.
+
+1. **Wait for CI**
+
+ This part is controlled by the [CircleCI configuration](.circleci/config.yml), specifically the `deploy` job. Which
+ creates the release artifacts and deploys them to maven central.
+
+## Credentials
+
+Credentials of various kind are needed for the release process to work. If you notice something
+failing due to unauthorized, you will need to modify the stored values in `Sonatype` [CircleCI Context](https://circleci.com/docs/2.0/contexts/)
+for the OpenFeign organization.
+
+`SONATYPE_USER` - the username of the Sonatype account used to upload artifacts.
+`SONATYPE_PASSWORD` - password for the Sonatype account.
+`GPG_KEY` - the gpg key used to sign the artifacts.
+`GPG_PASSPHRASE` - the passphrase for the gpg key
+
+### Troubleshooting invalid credentials
+
+If the `deploy` job fails due to invalid credentials, double check the `SONATYPE_USER` and `SONATYPE_PASSWORD`
+variables first and correct them.
+
+### Troubleshooting GPG issues
+
+If the `deploy` job fails when signing artifacts, the GPG key may have expired or is incorrect. To update the
+`GPG_KEY`, you must export a valid GPG key to ascii and replace all newline characters with `\n`. This will
+allow CircleCi to inject the key into the environment in a way where it can be imported again. Use the following command
+to generate the key file.
+
+```shell
+gpg -a --export-secret-keys | cat -e | sed | sed 's/\$/\\n/g' > gpg_key.asc
+```
+
+Paste the contents of this file into the `GPG_KEY` variable in the context and try the job again.
+
+## First release of the year
+
+The license plugin verifies license headers of files include a copyright notice indicating the years a file was affected.
+This information is taken from git history. There's a once-a-year problem with files that include version numbers (pom.xml).
+When a release tag is made, it increments version numbers, then commits them to git. On the first release of the year,
+further commands will fail due to the version increments invalidating the copyright statement. The way to sort this out is
+the following:
+
+Before you do the first release of the year, move the SNAPSHOT version back and forth from whatever the current is.
+In-between, re-apply the licenses.
+```bash
+$ ./mvnw versions:set -DnewVersion=1.3.3-SNAPSHOT -DgenerateBackupPoms=false
+$ ./mvnw com.mycila:license-maven-plugin:format
+$ ./mvnw versions:set -DnewVersion=1.3.2-SNAPSHOT -DgenerateBackupPoms=false
+$ git commit -am"Adjusts copyright headers for this year"
+```
diff --git a/annotation-error-decoder/README.md b/annotation-error-decoder/README.md
new file mode 100644
index 000000000..f99d5db77
--- /dev/null
+++ b/annotation-error-decoder/README.md
@@ -0,0 +1,359 @@
+Annotation Error Decoder
+=========================
+
+This module allows to annotate Feign's interfaces with annotations to generate Exceptions based on error codes
+
+To use AnnotationErrorDecoder with Feign, add the Annotation Error Decoder module to your classpath. Then, configure
+Feign to use the AnnotationErrorDecoder:
+
+```java
+GitHub github = Feign.builder()
+ .errorDecoder(
+ AnnotationErrorDecoder.builderFor(GitHub.class).build()
+ )
+ .target(GitHub.class, "https://api.github.com");
+```
+
+## Leveraging the annotations and priority order
+For annotation decoding to work, the class must be annotated with `@ErrorHandling` tags or meta-annotations.
+The tags are valid in both the class level as well as method level. They will be treated from 'most specific' to
+'least specific' in the following order:
+* A code specific exception defined on the method
+* A code specific exception defined on the class
+* The default exception of the method
+* The default exception of the class
+
+```java
+@ErrorHandling(codeSpecific =
+ {
+ @ErrorCodes( codes = {401}, generate = UnAuthorizedException.class),
+ @ErrorCodes( codes = {403}, generate = ForbiddenException.class),
+ @ErrorCodes( codes = {404}, generate = UnknownItemException.class),
+ },
+ defaultException = ClassLevelDefaultException.class
+)
+interface GitHub {
+
+ @ErrorHandling(codeSpecific =
+ {
+ @ErrorCodes( codes = {404}, generate = NonExistentRepoException.class),
+ @ErrorCodes( codes = {502, 503, 504}, generate = RetryAfterCertainTimeException.class),
+ },
+ defaultException = FailedToGetContributorsException.class
+ )
+ @RequestLine("GET /repos/{owner}/{repo}/contributors")
+ List contributors(@Param("owner") String owner, @Param("repo") String repo);
+}
+```
+In the above example, error responses to 'contributors' would hence be mapped as follows by status codes:
+
+| Code | Exception | Reason |
+| ----------- | -------------------------------- | --------------------- |
+| 401 | `UnAuthorizedException` | from Class definition |
+| 403 | `ForbiddenException` | from Class definition |
+| 404 | `NonExistenRepoException` | from Method definition, note that the class generic exception won't be thrown here |
+| 502,503,504 | `RetryAfterCertainTimeException` | from method definition. Note that you can have multiple error codes generate the same type of exception |
+| Any Other | `FailedToGetContributorsException` | from Method default |
+
+For a class level default exception to be thrown, the method must not have a `defaultException` defined, nor must the error code
+be mapped at either the method or class level.
+
+If the return code cannot be mapped to any code and no default exceptions have been configured, then the decoder will
+drop to a default decoder (by default, the standard one provided by feign). You can change the default drop-into decoder
+as follows:
+
+```java
+GitHub github = Feign.builder()
+ .errorDecoder(
+ AnnotationErrorDecoder.builderFor(GitHub.class)
+ .withDefaultDecoder(new MyOtherErrorDecoder())
+ .build()
+ )
+ .target(GitHub.class, "https://api.github.com");
+```
+
+
+## Complex Exceptions
+
+Any exception can be used if they have a default constructor:
+
+```java
+class DefaultConstructorException extends Exception {}
+```
+
+However, if you want to have parameters (such as the feign.Request object or response body or response headers), you have to annotate its
+constructor appropriately (the body annotation is optional, provided there aren't paramters which will clash)
+
+All the following examples are valid exceptions:
+```java
+class JustBody extends Exception {
+
+ @FeignExceptionConstructor
+ public JustBody(String body) {
+
+ }
+}
+class JustRequest extends Exception {
+
+ @FeignExceptionConstructor
+ public JustRequest(Request request) {
+
+ }
+}
+class RequestAndResponseBody extends Exception {
+
+ @FeignExceptionConstructor
+ public RequestAndResponseBody(Request request, String body) {
+
+ }
+}
+//Headers must be of type Map>
+class BodyAndHeaders extends Exception {
+
+ @FeignExceptionConstructor
+ public BodyAndHeaders(@ResponseBody String body, @ResponseHeaders Map> headers) {
+
+ }
+}
+class RequestAndResponseBodyAndHeaders extends Exception {
+
+ @FeignExceptionConstructor
+ public RequestAndResponseBodyAndHeaders(Request request, @ResponseBody String body, @ResponseHeaders Map> headers) {
+
+ }
+}
+class JustHeaders extends Exception {
+
+ @FeignExceptionConstructor
+ public JustHeaders(@ResponseHeaders Map> headers) {
+
+ }
+}
+```
+
+If you want to have the body decoded, you'll need to pass a decoder at construction time (just as for normal responses):
+
+```java
+GitHub github = Feign.builder()
+ .errorDecoder(
+ AnnotationErrorDecoder.builderFor(GitHub.class)
+ .withResponseBodyDecoder(new JacksonDecoder())
+ .build()
+ )
+ .target(GitHub.class, "https://api.github.com");
+```
+
+This will enable you to create exceptions where the body is a complex pojo:
+
+```java
+class ComplexPojoException extends Exception {
+
+ @FeignExceptionConstructor
+ public ComplexPojoException(GithubExceptionResponse body) {
+ if (body != null) {
+ // extract data
+ } else {
+ // fallback code
+ }
+ }
+}
+//The pojo can then be anything you'd like provided the decoder can manage it
+class GithubExceptionResponse {
+ public String message;
+ public int githubCode;
+ public List urlsForHelp;
+}
+```
+
+It's worth noting that at setup/startup time, the generators are checked with a null value of the body.
+If you don't do the null-checker, you'll get an NPE and startup will fail.
+
+
+## Inheriting from other interface definitions
+You can create a client interface that inherits from a different one. However, there are some limitations that
+you should be aware of (for most cases, these shouldn't be an issue):
+* The inheritance is not natural java inheritance of annotations - as these don't work on interfaces
+* Instead, the error looks at the class and if it finds the `@ErrorHandling` annotation, it uses that one.
+* If not, it will look at *all* the interfaces the main interface `extends` - but it does so in the order the
+java API gives it - so order is not guaranteed.
+* If it finds the annotation in one of those parents, it uses that definition, without looking at any other
+* That means that if more than one interface was extended which contained the `@ErrorHandling` annotation, we can't
+really guarantee which one of the parents will be selected and you should really do handling at the child interface
+ * so far, the java API seems to return in order of definition after the `extends`, but it's a really bad practice
+ if you have to depend on that... so our suggestion: don't.
+
+That means that as long as you only ever extend from a base interface (where you may decide that all 404's are "NotFoundException", for example)
+then you should be ok. But if you get complex in polymorphism, all bets are off - so don't go crazy!
+
+Example:
+In the following code:
+* The base `FeignClientBase` interface defines a default set of exceptions at class level
+* the `GitHub1` and `GitHub2` interfaces will inherit the class-level error handling, which means that
+any 401/403/404 will be handled correctly (provided the method doesn't specify a more specific exception)
+* the `GitHub3` interface however, by defining its own error handling, will handle all 401's, but not the
+403/404's since there's no merging/etc (not really in the plan to implement either...)
+```java
+
+@ErrorHandling(codeSpecific =
+ {
+ @ErrorCodes( codes = {401}, generate = UnAuthorizedException.class),
+ @ErrorCodes( codes = {403}, generate = ForbiddenException.class),
+ @ErrorCodes( codes = {404}, generate = UnknownItemException.class),
+ },
+ defaultException = ClassLevelDefaultException.class
+)
+interface FeignClientBase {}
+
+interface GitHub1 extends FeignClientBase {
+
+ @ErrorHandling(codeSpecific =
+ {
+ @ErrorCodes( codes = {404}, generate = NonExistentRepoException.class),
+ @ErrorCodes( codes = {502, 503, 504}, generate = RetryAfterCertainTimeException.class),
+ },
+ defaultException = FailedToGetContributorsException.class
+ )
+ @RequestLine("GET /repos/{owner}/{repo}/contributors")
+ List contributors(@Param("owner") String owner, @Param("repo") String repo);
+}
+
+interface GitHub2 extends FeignClientBase {
+
+ @ErrorHandling(codeSpecific =
+ {
+ @ErrorCodes( codes = {404}, generate = NonExistentRepoException.class),
+ @ErrorCodes( codes = {502, 503, 504}, generate = RetryAfterCertainTimeException.class),
+ },
+ defaultException = FailedToGetContributorsException.class
+ )
+ @RequestLine("GET /repos/{owner}/{repo}/contributors")
+ List contributors(@Param("owner") String owner, @Param("repo") String repo);
+}
+
+@ErrorHandling(codeSpecific =
+ {
+ @ErrorCodes( codes = {401}, generate = UnAuthorizedException.class)
+ },
+ defaultException = ClassLevelDefaultException.class
+)
+interface GitHub3 extends FeignClientBase {
+
+ @ErrorHandling(codeSpecific =
+ {
+ @ErrorCodes( codes = {404}, generate = NonExistentRepoException.class),
+ @ErrorCodes( codes = {502, 503, 504}, generate = RetryAfterCertainTimeException.class),
+ },
+ defaultException = FailedToGetContributorsException.class
+ )
+ @RequestLine("GET /repos/{owner}/{repo}/contributors")
+ List contributors(@Param("owner") String owner, @Param("repo") String repo);
+}
+```
+
+## Meta-annotations
+When you want to share the same configuration of one `@ErrorHandling` annotation the `@ErrorHandling` annotation
+can be moved to a meta-annotation. Then later on this meta-annotation can be used on a method or at class level to
+reduce the amount duplicated code. A meta-annotation is a special annotation that contains the `@ErrorHandling`
+annotation and possibly other annotations, e.g. Spring-Rest annotations.
+
+There are some limitations and rules to keep in mind when using meta-annotation:
+- inheritance for meta-annotations when using interface inheritance is supported and is following the same rules as for
+ interface inheritance (see above)
+ - `@ErrorHandling` has **precedence** over any meta-annotation when placed together on a class or method
+ - a meta-annotation on a child interface (method or class) has **precedence** over the error handling defined in the
+ parent interface
+- having a meta-annotation on a meta-annotation is not supported, only the annotations on a type are checked for a
+ `@ErrorHandling`
+- when multiple meta-annotations with an `@ErrorHandling` annotation are present on a class or method the first one
+ which is returned by java API is used to figure out the error handling, the others are not considered, so it is
+ advisable to have only one meta-annotation on each method or class as the order is not guaranteed.
+- **no merging** of configurations is supported, e.g. multiple meta-annotations on the same type, meta-annotation with
+ `@ErrorHandling` on the same type
+
+Example:
+
+Let's assume multiple methods need to handle the response-code `404` in the same way but differently what is
+specified in the `@ErrorHandling` annotation on the class-level. In that case, to avoid also duplicate annotation definitions
+on the affected methods a meta-annotation can reduce the amount of code to be written to handle this `404` differently.
+
+In the following code the status-code `404` is handled on a class level which throws an `UnknownItemException` for all
+methods inside this interface. For the methods `contributors` and `languages` a different exceptions needs to be thrown,
+in this case it is a `NoDataFoundException`. The `teams`method will still use the exception defined by the class-level
+error handling annotation. To simplify the code a meta-annotation can be created and be used in the interface to keep
+the interface small and readable.
+
+```java
+@ErrorHandling(
+ codeSpecific = {
+ @ErrorCodes(codes = {404}, generate = NoDataFoundException.class),
+ },
+ defaultException = GithubRemoteException.class)
+@Retention(RetentionPolicy.RUNTIME)
+@interface NoDataErrorHandling {
+}
+```
+
+Having this meta-annotation in place it can be used to transform the interface into a much smaller one, keeping the same
+behavior.
+- `contributers` will throw a `NoDataFoundException` for status code `404` as defined on method level and a
+ `GithubRemoteException` for all other status codes
+- `languages` will throw a `NoDataFoundException` for status code `404` as defined on method level and a
+ `GithubRemoteException` for all other status codes
+- `teams` will throw a `UnknownItemException` for status code `404` as defined on class level and a
+ `ClassLevelDefaultException` for all other status codes
+
+Before:
+```java
+@ErrorHandling(codeSpecific =
+ {
+ @ErrorCodes( codes = {404}, generate = UnknownItemException.class)
+ },
+ defaultException = ClassLevelDefaultException.class
+)
+interface GitHub {
+ @ErrorHandling(codeSpecific =
+ {
+ @ErrorCodes( codes = {404}, generate = NoDataFoundException.class)
+ },
+ defaultException = GithubRemoteException.class
+ )
+ @RequestLine("GET /repos/{owner}/{repo}/contributors")
+ List contributors(@Param("owner") String owner, @Param("repo") String repo);
+
+ @ErrorHandling(codeSpecific =
+ {
+ @ErrorCodes( codes = {404}, generate = NoDataFoundException.class)
+ },
+ defaultException = GithubRemoteException.class
+ )
+ @RequestLine("GET /repos/{owner}/{repo}/languages")
+ Map languages(@Param("owner") String owner, @Param("repo") String repo);
+
+ @ErrorHandling
+ @RequestLine("GET /repos/{owner}/{repo}/team")
+ List languages(@Param("owner") String owner, @Param("repo") String repo);
+}
+```
+
+After:
+```java
+@ErrorHandling(codeSpecific =
+ {
+ @ErrorCodes( codes = {404}, generate = UnknownItemException.class)
+ },
+ defaultException = ClassLevelDefaultException.class
+)
+interface GitHub {
+ @NoDataErrorHandling
+ @RequestLine("GET /repos/{owner}/{repo}/contributors")
+ List contributors(@Param("owner") String owner, @Param("repo") String repo);
+
+ @NoDataErrorHandling
+ @RequestLine("GET /repos/{owner}/{repo}/languages")
+ Map languages(@Param("owner") String owner, @Param("repo") String repo);
+
+ @ErrorHandling
+ @RequestLine("GET /repos/{owner}/{repo}/team")
+ List languages(@Param("owner") String owner, @Param("repo") String repo);
+}
+```
diff --git a/annotation-error-decoder/pom.xml b/annotation-error-decoder/pom.xml
new file mode 100644
index 000000000..ca93ddb92
--- /dev/null
+++ b/annotation-error-decoder/pom.xml
@@ -0,0 +1,58 @@
+
+
+
+ 4.0.0
+
+
+ io.github.openfeign
+ parent
+ 13.5-SNAPSHOT
+
+
+ feign-annotation-error-decoder
+ Feign Annotation Error Decoder
+ Feign Annotation Error Decoder
+
+
+ ${project.basedir}/..
+
+
+
+
+ ${project.groupId}
+ feign-core
+
+
+
+ ${project.groupId}
+ feign-core
+ test-jar
+ test
+
+
+
+ com.squareup.okhttp3
+ mockwebserver
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter
+ test
+
+
+
diff --git a/annotation-error-decoder/src/main/java/feign/error/AnnotationErrorDecoder.java b/annotation-error-decoder/src/main/java/feign/error/AnnotationErrorDecoder.java
new file mode 100644
index 000000000..8823fb0be
--- /dev/null
+++ b/annotation-error-decoder/src/main/java/feign/error/AnnotationErrorDecoder.java
@@ -0,0 +1,191 @@
+/*
+ * Copyright 2012-2023 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.error;
+
+import feign.Response;
+import feign.codec.Decoder;
+import feign.codec.ErrorDecoder;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.AnnotatedElement;
+import java.lang.reflect.Method;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+import static feign.Feign.configKey;
+
+public class AnnotationErrorDecoder implements ErrorDecoder {
+
+ private final Map errorHandlerMap;
+ private final ErrorDecoder defaultDecoder;
+
+
+ AnnotationErrorDecoder(Map errorHandlerMap,
+ ErrorDecoder defaultDecoder) {
+ this.errorHandlerMap = errorHandlerMap;
+ this.defaultDecoder = defaultDecoder;
+ }
+
+ @Override
+ public Exception decode(String methodKey, Response response) {
+ if (errorHandlerMap.containsKey(methodKey)) {
+ return errorHandlerMap.get(methodKey).decode(response);
+ }
+ return defaultDecoder.decode(methodKey, response);
+ }
+
+
+ public static AnnotationErrorDecoder.Builder builderFor(Class> apiType) {
+ return new Builder(apiType);
+ }
+
+ public static class Builder {
+ private final Class> apiType;
+ private ErrorDecoder defaultDecoder = new ErrorDecoder.Default();
+ private Decoder responseBodyDecoder = new Decoder.Default();
+
+
+ public Builder(Class> apiType) {
+ this.apiType = apiType;
+ }
+
+ public Builder withDefaultDecoder(ErrorDecoder defaultDecoder) {
+ this.defaultDecoder = defaultDecoder;
+ return this;
+ }
+
+ public Builder withResponseBodyDecoder(Decoder responseBodyDecoder) {
+ this.responseBodyDecoder = responseBodyDecoder;
+ return this;
+ }
+
+ public AnnotationErrorDecoder build() {
+ Map errorHandlerMap = generateErrorHandlerMapFromApi(apiType);
+ return new AnnotationErrorDecoder(errorHandlerMap, defaultDecoder);
+ }
+
+ Map generateErrorHandlerMapFromApi(Class> apiType) {
+
+ ExceptionGenerator classLevelDefault = new ExceptionGenerator.Builder()
+ .withResponseBodyDecoder(responseBodyDecoder)
+ .withExceptionType(ErrorHandling.NO_DEFAULT.class)
+ .build();
+ Map classLevelStatusCodeDefinitions =
+ new HashMap();
+
+ Optional classLevelErrorHandling =
+ readErrorHandlingIncludingInherited(apiType);
+ if (classLevelErrorHandling.isPresent()) {
+ ErrorHandlingDefinition classErrorHandlingDefinition =
+ readAnnotation(classLevelErrorHandling.get(), responseBodyDecoder);
+ classLevelDefault = classErrorHandlingDefinition.defaultThrow;
+ classLevelStatusCodeDefinitions = classErrorHandlingDefinition.statusCodesMap;
+ }
+
+ Map methodErrorHandlerMap =
+ new HashMap();
+ for (Method method : apiType.getMethods()) {
+ ErrorHandling methodLevelAnnotation = getErrorHandlingAnnotation(method);
+ if (methodLevelAnnotation != null) {
+ ErrorHandlingDefinition methodErrorHandling =
+ readAnnotation(methodLevelAnnotation, responseBodyDecoder);
+ ExceptionGenerator methodDefault = methodErrorHandling.defaultThrow;
+ if (methodDefault.getExceptionType().equals(ErrorHandling.NO_DEFAULT.class)) {
+ methodDefault = classLevelDefault;
+ }
+
+ MethodErrorHandler methodErrorHandler =
+ new MethodErrorHandler(methodErrorHandling.statusCodesMap,
+ classLevelStatusCodeDefinitions, methodDefault);
+
+ methodErrorHandlerMap.put(configKey(apiType, method), methodErrorHandler);
+ }
+ }
+
+ return methodErrorHandlerMap;
+ }
+
+ Optional readErrorHandlingIncludingInherited(Class> apiType) {
+ ErrorHandling apiTypeAnnotation = getErrorHandlingAnnotation(apiType);
+ if (apiTypeAnnotation != null) {
+ return Optional.of(apiTypeAnnotation);
+ }
+ for (Class> parentInterface : apiType.getInterfaces()) {
+ Optional errorHandling =
+ readErrorHandlingIncludingInherited(parentInterface);
+ if (errorHandling.isPresent()) {
+ return errorHandling;
+ }
+ }
+ // Finally, if there's a superclass that isn't Object check if the superclass has anything
+ if (!apiType.isInterface() && !apiType.getSuperclass().equals(Object.class)) {
+ return readErrorHandlingIncludingInherited(apiType.getSuperclass());
+ }
+ return Optional.empty();
+ }
+
+ private static ErrorHandling getErrorHandlingAnnotation(AnnotatedElement element) {
+ ErrorHandling annotation = element.getAnnotation(ErrorHandling.class);
+ if (annotation == null) {
+ for (Annotation metaAnnotation : element.getAnnotations()) {
+ annotation = metaAnnotation.annotationType().getAnnotation(ErrorHandling.class);
+ if (annotation != null) {
+ break;
+ }
+ }
+ }
+ return annotation;
+ }
+
+ static ErrorHandlingDefinition readAnnotation(ErrorHandling errorHandling,
+ Decoder responseBodyDecoder) {
+ ExceptionGenerator defaultException = new ExceptionGenerator.Builder()
+ .withResponseBodyDecoder(responseBodyDecoder)
+ .withExceptionType(errorHandling.defaultException())
+ .build();
+ Map statusCodesDefinition =
+ new HashMap();
+
+ for (ErrorCodes statusCodeDefinition : errorHandling.codeSpecific()) {
+ for (int statusCode : statusCodeDefinition.codes()) {
+ if (statusCodesDefinition.containsKey(statusCode)) {
+ throw new IllegalStateException(
+ "Status Code [" + statusCode + "] " +
+ "has already been declared to throw ["
+ + statusCodesDefinition.get(statusCode).getExceptionType().getName() + "] " +
+ "and [" + statusCodeDefinition.generate() + "] - dupe definition");
+ }
+ statusCodesDefinition.put(statusCode,
+ new ExceptionGenerator.Builder()
+ .withResponseBodyDecoder(responseBodyDecoder)
+ .withExceptionType(statusCodeDefinition.generate())
+ .build());
+ }
+ }
+
+ return new ErrorHandlingDefinition(defaultException, statusCodesDefinition);
+ }
+
+ private static class ErrorHandlingDefinition {
+ private final ExceptionGenerator defaultThrow;
+ private final Map statusCodesMap;
+
+
+ private ErrorHandlingDefinition(ExceptionGenerator defaultThrow,
+ Map statusCodesMap) {
+ this.defaultThrow = defaultThrow;
+ this.statusCodesMap = statusCodesMap;
+ }
+ }
+ }
+}
diff --git a/annotation-error-decoder/src/main/java/feign/error/ErrorCodes.java b/annotation-error-decoder/src/main/java/feign/error/ErrorCodes.java
new file mode 100644
index 000000000..51ffa9c44
--- /dev/null
+++ b/annotation-error-decoder/src/main/java/feign/error/ErrorCodes.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2012-2023 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.error;
+
+public @interface ErrorCodes {
+ int[] codes();
+
+ Class extends Exception> generate();
+}
diff --git a/annotation-error-decoder/src/main/java/feign/error/ErrorHandling.java b/annotation-error-decoder/src/main/java/feign/error/ErrorHandling.java
new file mode 100644
index 000000000..fd52bdbf1
--- /dev/null
+++ b/annotation-error-decoder/src/main/java/feign/error/ErrorHandling.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2012-2023 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.error;
+
+import feign.Response;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Inherited
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.TYPE, ElementType.METHOD})
+public @interface ErrorHandling {
+ ErrorCodes[] codeSpecific() default {};
+
+ Class extends Exception> defaultException() default NO_DEFAULT.class;
+
+ final class NO_DEFAULT extends Exception {
+ @FeignExceptionConstructor
+ public NO_DEFAULT(@ResponseBody Response response) {
+ super("Endpoint responded with " + response.status() + ", reason: " + response.reason());
+ }
+ }
+}
diff --git a/annotation-error-decoder/src/main/java/feign/error/ExceptionGenerator.java b/annotation-error-decoder/src/main/java/feign/error/ExceptionGenerator.java
new file mode 100644
index 000000000..23ab70a3d
--- /dev/null
+++ b/annotation-error-decoder/src/main/java/feign/error/ExceptionGenerator.java
@@ -0,0 +1,221 @@
+/*
+ * Copyright 2012-2023 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.error;
+
+import feign.Request;
+import feign.Response;
+import feign.Types;
+import feign.codec.DecodeException;
+import feign.codec.Decoder;
+import java.io.IOException;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Type;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import static feign.Util.checkState;
+
+class ExceptionGenerator {
+
+ private static final Response TEST_RESPONSE;
+
+ static {
+ Map> testHeaders = new HashMap>();
+ testHeaders.put("TestHeader", Arrays.asList("header-value"));
+
+ TEST_RESPONSE = Response.builder()
+ .status(500)
+ .body((Response.Body) null)
+ .headers(testHeaders)
+ .request(Request.create(Request.HttpMethod.GET, "http://test", testHeaders,
+ Request.Body.empty(), null))
+ .build();
+ }
+
+ private final Integer bodyIndex;
+ private final Integer requestIndex;
+ private final Integer headerMapIndex;
+ private final Integer numOfParams;
+ private final Type bodyType;
+ private final Class extends Exception> exceptionType;
+ private final Decoder bodyDecoder;
+
+ ExceptionGenerator(Integer bodyIndex, Integer requestIndex, Integer headerMapIndex,
+ Integer numOfParams, Type bodyType,
+ Class extends Exception> exceptionType, Decoder bodyDecoder) {
+ this.bodyIndex = bodyIndex;
+ this.requestIndex = requestIndex;
+ this.headerMapIndex = headerMapIndex;
+ this.numOfParams = numOfParams;
+ this.bodyType = bodyType;
+ this.exceptionType = exceptionType;
+ this.bodyDecoder = bodyDecoder;
+ }
+
+
+ Exception createException(Response response) throws InvocationTargetException,
+ NoSuchMethodException, InstantiationException, IllegalAccessException {
+
+ Class>[] paramClasses = new Class[numOfParams];
+ Object[] paramValues = new Object[numOfParams];
+ if (bodyIndex >= 0) {
+ paramClasses[bodyIndex] = Types.getRawType(bodyType);
+ paramValues[bodyIndex] = resolveBody(response);
+ }
+ if (requestIndex >= 0) {
+ paramClasses[requestIndex] = Request.class;
+ paramValues[requestIndex] = response.request();
+ }
+ if (headerMapIndex >= 0) {
+ paramValues[headerMapIndex] = response.headers();
+ paramClasses[headerMapIndex] = Map.class;
+ }
+ return exceptionType.getConstructor(paramClasses)
+ .newInstance(paramValues);
+
+ }
+
+ Class extends Exception> getExceptionType() {
+ return exceptionType;
+ }
+
+ private Object resolveBody(Response response) {
+ if (bodyType instanceof Class> && ((Class>) bodyType).isInstance(response)) {
+ return response;
+ }
+ try {
+ return bodyDecoder.decode(response, bodyType);
+ } catch (IOException e) {
+ // How do we log this?
+ return null;
+ } catch (DecodeException e) {
+ // How do we log this?
+ return null;
+ }
+ }
+
+ static class Builder {
+ private Class extends Exception> exceptionType;
+ private Decoder responseBodyDecoder;
+
+ public Builder withExceptionType(Class extends Exception> exceptionType) {
+ this.exceptionType = exceptionType;
+ return this;
+ }
+
+ public Builder withResponseBodyDecoder(Decoder bodyDecoder) {
+ this.responseBodyDecoder = bodyDecoder;
+ return this;
+ }
+
+ public ExceptionGenerator build() {
+ Constructor extends Exception> constructor = getConstructor(exceptionType);
+ Type[] parameterTypes = constructor.getGenericParameterTypes();
+ Annotation[][] parametersAnnotations = constructor.getParameterAnnotations();
+
+ Integer bodyIndex = -1;
+ Integer requestIndex = -1;
+ Integer headerMapIndex = -1;
+ Integer numOfParams = parameterTypes.length;
+ Type bodyType = null;
+
+ for (int i = 0; i < parameterTypes.length; i++) {
+ Annotation[] paramAnnotations = parametersAnnotations[i];
+ boolean foundAnnotation = false;
+ for (Annotation annotation : paramAnnotations) {
+ if (annotation.annotationType().equals(ResponseHeaders.class)) {
+ checkState(headerMapIndex == -1,
+ "Cannot have two parameters tagged with @ResponseHeaders");
+ checkState(Types.getRawType(parameterTypes[i]).equals(Map.class),
+ "Response Header map must be of type Map, but was %s", parameterTypes[i]);
+ headerMapIndex = i;
+ foundAnnotation = true;
+ break;
+ }
+ }
+ if (!foundAnnotation) {
+ if (parameterTypes[i].equals(Request.class)) {
+ checkState(requestIndex == -1,
+ "Cannot have two parameters either without annotations or with object of type feign.Request");
+ requestIndex = i;
+ } else {
+ checkState(bodyIndex == -1,
+ "Cannot have two parameters either without annotations or with @ResponseBody annotation");
+ bodyIndex = i;
+ bodyType = parameterTypes[i];
+ }
+ }
+ }
+
+ ExceptionGenerator generator = new ExceptionGenerator(
+ bodyIndex,
+ requestIndex,
+ headerMapIndex,
+ numOfParams,
+ bodyType,
+ exceptionType,
+ responseBodyDecoder);
+
+ validateGeneratorCanBeUsedToGenerateExceptions(generator);
+ return generator;
+ }
+
+ private void validateGeneratorCanBeUsedToGenerateExceptions(ExceptionGenerator generator) {
+ try {
+ generator.createException(TEST_RESPONSE);
+ } catch (Exception e) {
+ throw new IllegalStateException(
+ "Cannot generate exception - check constructor parameter types (are headers Map> or is something causing an exception on construction?)",
+ e);
+ }
+ }
+
+ private Constructor extends Exception> getConstructor(Class extends Exception> exceptionClass) {
+ Constructor extends Exception> preferredConstructor = null;
+ for (Constructor> constructor : exceptionClass.getConstructors()) {
+
+ FeignExceptionConstructor exceptionConstructor =
+ constructor.getAnnotation(FeignExceptionConstructor.class);
+ if (exceptionConstructor == null) {
+ continue;
+ }
+ Class>[] parameterTypes = constructor.getParameterTypes();
+ if (parameterTypes.length == 0) {
+ continue;
+ }
+ if (preferredConstructor == null) {
+ preferredConstructor = (Constructor extends Exception>) constructor;
+ } else {
+ throw new IllegalStateException(
+ "Too many constructors marked with @FeignExceptionConstructor");
+ }
+ }
+
+ if (preferredConstructor == null) {
+ try {
+ return exceptionClass.getConstructor();
+ } catch (NoSuchMethodException e) {
+ throw new IllegalStateException(
+ "Cannot find any suitable constructor in class [" + exceptionClass.getName()
+ + "] - did you forget to mark one with @FeignExceptionConstructor or at least have a public default constructor?",
+ e);
+ }
+ }
+ return preferredConstructor;
+ }
+ }
+}
diff --git a/annotation-error-decoder/src/main/java/feign/error/FeignExceptionConstructor.java b/annotation-error-decoder/src/main/java/feign/error/FeignExceptionConstructor.java
new file mode 100644
index 000000000..9939492ba
--- /dev/null
+++ b/annotation-error-decoder/src/main/java/feign/error/FeignExceptionConstructor.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2012-2023 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.error;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.CONSTRUCTOR})
+public @interface FeignExceptionConstructor {
+}
diff --git a/annotation-error-decoder/src/main/java/feign/error/MethodErrorHandler.java b/annotation-error-decoder/src/main/java/feign/error/MethodErrorHandler.java
new file mode 100644
index 000000000..a1770bb60
--- /dev/null
+++ b/annotation-error-decoder/src/main/java/feign/error/MethodErrorHandler.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2012-2023 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.error;
+
+import feign.Response;
+import java.lang.reflect.InvocationTargetException;
+import java.util.Map;
+
+class MethodErrorHandler {
+
+ private final Map methodLevelExceptionsByCode;
+ private final Map classLevelExceptionsByCode;
+ private final ExceptionGenerator defaultException;
+
+ MethodErrorHandler(Map methodLevelExceptionsByCode,
+ Map classLevelExceptionsByCode,
+ ExceptionGenerator defaultException) {
+ this.methodLevelExceptionsByCode = methodLevelExceptionsByCode;
+ this.classLevelExceptionsByCode = classLevelExceptionsByCode;
+ this.defaultException = defaultException;
+ }
+
+
+ public Exception decode(Response response) {
+ ExceptionGenerator constructorDefinition = getConstructorDefinition(response);
+ return createException(constructorDefinition, response);
+ }
+
+ private ExceptionGenerator getConstructorDefinition(Response response) {
+ if (methodLevelExceptionsByCode.containsKey(response.status())) {
+ return methodLevelExceptionsByCode.get(response.status());
+ }
+ if (classLevelExceptionsByCode.containsKey(response.status())) {
+ return classLevelExceptionsByCode.get(response.status());
+ }
+ return defaultException;
+ }
+
+ protected Exception createException(ExceptionGenerator constructorDefinition, Response response) {
+ try {
+ return constructorDefinition.createException(response);
+ } catch (IllegalAccessException e) {
+ throw new IllegalStateException("Cannot access constructor", e);
+ } catch (InstantiationException e) {
+ throw new IllegalStateException("Cannot instantiate exception with constructor", e);
+ } catch (InvocationTargetException e) {
+ throw new IllegalStateException("Cannot invoke constructor", e);
+ } catch (NoSuchMethodException e) {
+ throw new IllegalStateException("Constructor does not exist", e);
+ }
+ }
+}
diff --git a/annotation-error-decoder/src/main/java/feign/error/ResponseBody.java b/annotation-error-decoder/src/main/java/feign/error/ResponseBody.java
new file mode 100644
index 000000000..2619de244
--- /dev/null
+++ b/annotation-error-decoder/src/main/java/feign/error/ResponseBody.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2012-2023 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.error;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.PARAMETER})
+public @interface ResponseBody {
+}
diff --git a/annotation-error-decoder/src/main/java/feign/error/ResponseHeaders.java b/annotation-error-decoder/src/main/java/feign/error/ResponseHeaders.java
new file mode 100644
index 000000000..4a5700f13
--- /dev/null
+++ b/annotation-error-decoder/src/main/java/feign/error/ResponseHeaders.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2012-2023 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.error;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.PARAMETER})
+public @interface ResponseHeaders {
+}
diff --git a/annotation-error-decoder/src/test/java/feign/error/AbstractAnnotationErrorDecoderTest.java b/annotation-error-decoder/src/test/java/feign/error/AbstractAnnotationErrorDecoderTest.java
new file mode 100644
index 000000000..a9927f579
--- /dev/null
+++ b/annotation-error-decoder/src/test/java/feign/error/AbstractAnnotationErrorDecoderTest.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2012-2023 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.error;
+
+import static feign.Feign.configKey;
+import java.nio.charset.StandardCharsets;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import feign.Request;
+import feign.Response;
+
+public abstract class AbstractAnnotationErrorDecoderTest {
+
+
+
+ public abstract Class interfaceAtTest();
+
+ String feignConfigKey(String methodName) throws NoSuchMethodException {
+ return configKey(interfaceAtTest(), interfaceAtTest().getMethod(methodName));
+ }
+
+ Response testResponse(int status) {
+ return testResponse(status, "default Response body");
+ }
+
+ Response testResponse(int status, String body) {
+ return testResponse(status, body, new HashMap<>());
+ }
+
+ Response testResponse(int status, String body, Map> headers) {
+ return Response.builder()
+ .status(status)
+ .body(body, StandardCharsets.UTF_8)
+ .headers(headers)
+ .request(Request.create(Request.HttpMethod.GET, "http://test", headers,
+ Request.Body.empty(), null))
+ .build();
+ }
+}
diff --git a/annotation-error-decoder/src/test/java/feign/error/AnnotationErrorDecoderAnnotationInheritanceTest.java b/annotation-error-decoder/src/test/java/feign/error/AnnotationErrorDecoderAnnotationInheritanceTest.java
new file mode 100644
index 000000000..de3b4cf3b
--- /dev/null
+++ b/annotation-error-decoder/src/test/java/feign/error/AnnotationErrorDecoderAnnotationInheritanceTest.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2012-2023 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.error;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Arrays;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+
+public class AnnotationErrorDecoderAnnotationInheritanceTest extends
+ AbstractAnnotationErrorDecoderTest {
+ @Override
+ public Class interfaceAtTest() {
+ return TestClientInterfaceWithWithMetaAnnotation.class;
+ }
+
+ public static Iterable data() {
+ return Arrays.asList(new Object[][] {
+ {"Test Code Specific At Method", 402, "method1Test", MethodLevelDefaultException.class},
+ {"Test Code Specific At Method", 403, "method1Test", MethodLevelNotFoundException.class},
+ {"Test Code Specific At Method", 404, "method1Test", MethodLevelNotFoundException.class},
+ {"Test Code Specific At Method", 402, "method2Test", ClassLevelDefaultException.class},
+ {"Test Code Specific At Method", 403, "method2Test", MethodLevelNotFoundException.class},
+ {"Test Code Specific At Method", 404, "method2Test", ClassLevelNotFoundException.class},
+ });
+ } // first data value (0) is default
+
+ public String testType;
+ public int errorCode;
+ public String method;
+ public Class extends Exception> expectedExceptionClass;
+
+ @MethodSource("data")
+ @ParameterizedTest(
+ name = "{0}: When error code ({1}) on method ({2}) should return exception type ({3})")
+ void test(String testType,
+ int errorCode,
+ String method,
+ Class extends Exception> expectedExceptionClass)
+ throws Exception {
+ initAnnotationErrorDecoderAnnotationInheritanceTest(testType, errorCode, method,
+ expectedExceptionClass);
+ AnnotationErrorDecoder decoder =
+ AnnotationErrorDecoder.builderFor(TestClientInterfaceWithWithMetaAnnotation.class).build();
+
+ assertThat(decoder.decode(feignConfigKey(method), testResponse(errorCode)).getClass())
+ .isEqualTo(expectedExceptionClass);
+ }
+
+ @ClassError
+ public static interface TestClientInterfaceWithWithMetaAnnotation {
+ @MethodError
+ void method1Test();
+
+ @ErrorHandling(
+ codeSpecific = {@ErrorCodes(codes = {403}, generate = MethodLevelNotFoundException.class)})
+ void method2Test();
+ }
+
+ @ErrorHandling(
+ codeSpecific = {@ErrorCodes(codes = {404}, generate = ClassLevelNotFoundException.class),},
+ defaultException = ClassLevelDefaultException.class)
+ @Retention(RetentionPolicy.RUNTIME)
+ @interface ClassError {
+ }
+
+ @ErrorHandling(
+ codeSpecific = {
+ @ErrorCodes(codes = {404, 403}, generate = MethodLevelNotFoundException.class),},
+ defaultException = MethodLevelDefaultException.class)
+ @Retention(RetentionPolicy.RUNTIME)
+ @interface MethodError {
+ }
+
+ static class ClassLevelDefaultException extends Exception {
+ public ClassLevelDefaultException() {}
+ }
+ static class ClassLevelNotFoundException extends Exception {
+ public ClassLevelNotFoundException() {}
+ }
+ static class MethodLevelDefaultException extends Exception {
+ public MethodLevelDefaultException() {}
+ }
+ static class MethodLevelNotFoundException extends Exception {
+ public MethodLevelNotFoundException() {}
+ }
+
+ public void initAnnotationErrorDecoderAnnotationInheritanceTest(String testType,
+ int errorCode,
+ String method,
+ Class extends Exception> expectedExceptionClass) {
+ this.testType = testType;
+ this.errorCode = errorCode;
+ this.method = method;
+ this.expectedExceptionClass = expectedExceptionClass;
+ }
+}
diff --git a/annotation-error-decoder/src/test/java/feign/error/AnnotationErrorDecoderClassInheritanceTest.java b/annotation-error-decoder/src/test/java/feign/error/AnnotationErrorDecoderClassInheritanceTest.java
new file mode 100644
index 000000000..85479b1a7
--- /dev/null
+++ b/annotation-error-decoder/src/test/java/feign/error/AnnotationErrorDecoderClassInheritanceTest.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright 2012-2023 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.error;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import java.util.Arrays;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+import feign.error.AnnotationErrorDecoderClassInheritanceTest.ParentInterfaceWithErrorHandling.ClassLevelDefaultException;
+import feign.error.AnnotationErrorDecoderClassInheritanceTest.ParentInterfaceWithErrorHandling.ClassLevelNotFoundException;
+import feign.error.AnnotationErrorDecoderClassInheritanceTest.ParentInterfaceWithErrorHandling.Method1DefaultException;
+import feign.error.AnnotationErrorDecoderClassInheritanceTest.ParentInterfaceWithErrorHandling.Method1NotFoundException;
+import feign.error.AnnotationErrorDecoderClassInheritanceTest.ParentInterfaceWithErrorHandling.Method2NotFoundException;
+import feign.error.AnnotationErrorDecoderClassInheritanceTest.ParentInterfaceWithErrorHandling.Method3DefaultException;
+import feign.error.AnnotationErrorDecoderClassInheritanceTest.ParentInterfaceWithErrorHandling.ServeErrorException;
+import feign.error.AnnotationErrorDecoderClassInheritanceTest.ParentInterfaceWithErrorHandling.UnauthenticatedOrUnauthorizedException;
+
+public class AnnotationErrorDecoderClassInheritanceTest extends
+ AbstractAnnotationErrorDecoderTest {
+
+ @Override
+ public Class interfaceAtTest() {
+ return GrandChild.class;
+ }
+
+ public static Iterable data() {
+ return Arrays.asList(new Object[][] {
+ {"Test Code Specific At Method", 404, "method1Test", Method1NotFoundException.class},
+ {"Test Code Specific At Method", 401, "method1Test",
+ UnauthenticatedOrUnauthorizedException.class},
+ {"Test Code Specific At Method", 404, "method2Test", Method2NotFoundException.class},
+ {"Test Code Specific At Method", 500, "method2Test", ServeErrorException.class},
+ {"Test Code Specific At Method", 503, "method2Test", ServeErrorException.class},
+ {"Test Code Specific At Class", 403, "method1Test",
+ UnauthenticatedOrUnauthorizedException.class},
+ {"Test Code Specific At Class", 403, "method2Test",
+ UnauthenticatedOrUnauthorizedException.class},
+ {"Test Code Specific At Class", 404, "method3Test", ClassLevelNotFoundException.class},
+ {"Test Code Specific At Class", 403, "method3Test",
+ UnauthenticatedOrUnauthorizedException.class},
+ {"Test Default At Method", 504, "method1Test", Method1DefaultException.class},
+ {"Test Default At Method", 504, "method3Test", Method3DefaultException.class},
+ {"Test Default At Class", 504, "method2Test", ClassLevelDefaultException.class},
+ });
+ } // first data value (0) is default
+
+ public String testType;
+ public int errorCode;
+ public String method;
+ public Class extends Exception> expectedExceptionClass;
+
+ @MethodSource("data")
+ @ParameterizedTest(
+ name = "{0}: When error code ({1}) on method ({2}) should return exception type ({3})")
+ void test(String testType,
+ int errorCode,
+ String method,
+ Class extends Exception> expectedExceptionClass)
+ throws Exception {
+ initAnnotationErrorDecoderClassInheritanceTest(testType, errorCode, method,
+ expectedExceptionClass);
+ AnnotationErrorDecoder decoder =
+ AnnotationErrorDecoder.builderFor(GrandChild.class).build();
+
+ assertThat(decoder.decode(feignConfigKey(method), testResponse(errorCode)).getClass())
+ .isEqualTo(expectedExceptionClass);
+ }
+
+ @ErrorHandling(codeSpecific = {
+ @ErrorCodes(codes = {404}, generate = ClassLevelNotFoundException.class),
+ @ErrorCodes(codes = {403}, generate = UnauthenticatedOrUnauthorizedException.class)
+ },
+ defaultException = ClassLevelDefaultException.class)
+ interface ParentInterfaceWithErrorHandling {
+ @ErrorHandling(codeSpecific = {
+ @ErrorCodes(codes = {404}, generate = Method1NotFoundException.class),
+ @ErrorCodes(codes = {401}, generate = UnauthenticatedOrUnauthorizedException.class)
+ },
+ defaultException = Method1DefaultException.class)
+ void method1Test();
+
+ @ErrorHandling(codeSpecific = {
+ @ErrorCodes(codes = {404}, generate = Method2NotFoundException.class),
+ @ErrorCodes(codes = {500, 503}, generate = ServeErrorException.class)
+ })
+ void method2Test();
+
+ @ErrorHandling(
+ defaultException = Method3DefaultException.class)
+ void method3Test();
+
+ class ClassLevelDefaultException extends Exception {
+ }
+ class Method1DefaultException extends Exception {
+ }
+ class Method3DefaultException extends Exception {
+ }
+ class Method1NotFoundException extends Exception {
+ }
+ class Method2NotFoundException extends Exception {
+ }
+ class ClassLevelNotFoundException extends Exception {
+ }
+ class UnauthenticatedOrUnauthorizedException extends Exception {
+ }
+ class ServeErrorException extends Exception {
+ }
+ }
+
+ abstract class Child implements ParentInterfaceWithErrorHandling {
+ }
+
+ abstract class GrandChild extends Child {
+ }
+
+ public void initAnnotationErrorDecoderClassInheritanceTest(String testType,
+ int errorCode,
+ String method,
+ Class extends Exception> expectedExceptionClass) {
+ this.testType = testType;
+ this.errorCode = errorCode;
+ this.method = method;
+ this.expectedExceptionClass = expectedExceptionClass;
+ }
+}
diff --git a/annotation-error-decoder/src/test/java/feign/error/AnnotationErrorDecoderExceptionConstructorsTest.java b/annotation-error-decoder/src/test/java/feign/error/AnnotationErrorDecoderExceptionConstructorsTest.java
new file mode 100644
index 000000000..5bf6a0111
--- /dev/null
+++ b/annotation-error-decoder/src/test/java/feign/error/AnnotationErrorDecoderExceptionConstructorsTest.java
@@ -0,0 +1,580 @@
+/*
+ * Copyright 2012-2023 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.error;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+import feign.Request;
+import feign.codec.Decoder;
+import feign.error.AnnotationErrorDecoderExceptionConstructorsTest.TestClientInterfaceWithDifferentExceptionConstructors;
+import feign.error.AnnotationErrorDecoderExceptionConstructorsTest.TestClientInterfaceWithDifferentExceptionConstructors.DeclaredDefaultConstructorException;
+import feign.error.AnnotationErrorDecoderExceptionConstructorsTest.TestClientInterfaceWithDifferentExceptionConstructors.DeclaredDefaultConstructorWithOtherConstructorsException;
+import feign.error.AnnotationErrorDecoderExceptionConstructorsTest.TestClientInterfaceWithDifferentExceptionConstructors.DefaultConstructorException;
+import feign.error.AnnotationErrorDecoderExceptionConstructorsTest.TestClientInterfaceWithDifferentExceptionConstructors.DefinedConstructorWithAnnotationForBody;
+import feign.error.AnnotationErrorDecoderExceptionConstructorsTest.TestClientInterfaceWithDifferentExceptionConstructors.DefinedConstructorWithAnnotationForBodyAndHeaders;
+import feign.error.AnnotationErrorDecoderExceptionConstructorsTest.TestClientInterfaceWithDifferentExceptionConstructors.DefinedConstructorWithAnnotationForBodyAndHeadersSecondOrder;
+import feign.error.AnnotationErrorDecoderExceptionConstructorsTest.TestClientInterfaceWithDifferentExceptionConstructors.DefinedConstructorWithAnnotationForHeaders;
+import feign.error.AnnotationErrorDecoderExceptionConstructorsTest.TestClientInterfaceWithDifferentExceptionConstructors.DefinedConstructorWithAnnotationForHeadersButNotForBody;
+import feign.error.AnnotationErrorDecoderExceptionConstructorsTest.TestClientInterfaceWithDifferentExceptionConstructors.DefinedConstructorWithAnnotationForNonSupportedBody;
+import feign.error.AnnotationErrorDecoderExceptionConstructorsTest.TestClientInterfaceWithDifferentExceptionConstructors.DefinedConstructorWithAnnotationForOptionalBody;
+import feign.error.AnnotationErrorDecoderExceptionConstructorsTest.TestClientInterfaceWithDifferentExceptionConstructors.DefinedConstructorWithNoAnnotationForBody;
+import feign.error.AnnotationErrorDecoderExceptionConstructorsTest.TestClientInterfaceWithDifferentExceptionConstructors.DefinedConstructorWithRequest;
+import feign.error.AnnotationErrorDecoderExceptionConstructorsTest.TestClientInterfaceWithDifferentExceptionConstructors.DefinedConstructorWithRequestAndAnnotationForResponseBody;
+import feign.error.AnnotationErrorDecoderExceptionConstructorsTest.TestClientInterfaceWithDifferentExceptionConstructors.DefinedConstructorWithRequestAndResponseBody;
+import feign.error.AnnotationErrorDecoderExceptionConstructorsTest.TestClientInterfaceWithDifferentExceptionConstructors.DefinedConstructorWithRequestAndResponseHeadersAndOptionalResponseBody;
+import feign.error.AnnotationErrorDecoderExceptionConstructorsTest.TestClientInterfaceWithDifferentExceptionConstructors.DefinedConstructorWithRequestAndResponseHeadersAndResponseBody;
+import feign.error.AnnotationErrorDecoderExceptionConstructorsTest.TestClientInterfaceWithDifferentExceptionConstructors.ParametersException;
+import feign.optionals.OptionalDecoder;
+
+public class AnnotationErrorDecoderExceptionConstructorsTest extends
+ AbstractAnnotationErrorDecoderTest {
+
+
+ private static final String NO_BODY = "NO BODY";
+ private static final Object NULL_BODY = null;
+ private static final String NON_NULL_BODY = "A GIVEN BODY";
+ private static final feign.Request REQUEST = feign.Request.create(feign.Request.HttpMethod.GET,
+ "http://test", Collections.emptyMap(), Request.Body.empty(), null);
+ private static final feign.Request NO_REQUEST = null;
+ private static final Map> NON_NULL_HEADERS =
+ new HashMap<>();
+ private static final Map> NO_HEADERS = null;
+
+
+ @Override
+ public Class interfaceAtTest() {
+ return TestClientInterfaceWithDifferentExceptionConstructors.class;
+ }
+
+ public static Iterable data() {
+ return Arrays.asList(new Object[][] {
+ {"Test Default Constructor", 500, DefaultConstructorException.class, NO_REQUEST, NO_BODY,
+ NO_HEADERS},
+ {"test Default Constructor", 501, DeclaredDefaultConstructorException.class, NO_REQUEST,
+ NO_BODY,
+ NO_HEADERS},
+ {"test Default Constructor", 502,
+ DeclaredDefaultConstructorWithOtherConstructorsException.class, NO_REQUEST, NO_BODY,
+ NO_HEADERS},
+ {"test Declared Constructor", 503, DefinedConstructorWithNoAnnotationForBody.class,
+ NO_REQUEST,
+ NON_NULL_BODY, NO_HEADERS},
+ {"test Declared Constructor", 504, DefinedConstructorWithAnnotationForBody.class,
+ NO_REQUEST, NON_NULL_BODY, NO_HEADERS},
+ {"test Declared Constructor", 505, DefinedConstructorWithAnnotationForBodyAndHeaders.class,
+ NO_REQUEST, NON_NULL_BODY, NON_NULL_HEADERS},
+ {"test Declared Constructor", 506,
+ DefinedConstructorWithAnnotationForBodyAndHeadersSecondOrder.class, NO_REQUEST,
+ NON_NULL_BODY,
+ NON_NULL_HEADERS},
+ {"test Declared Constructor", 507, DefinedConstructorWithAnnotationForHeaders.class,
+ NO_REQUEST, NO_BODY, NON_NULL_HEADERS},
+ {"test Declared Constructor", 508,
+ DefinedConstructorWithAnnotationForHeadersButNotForBody.class, NO_REQUEST,
+ NON_NULL_BODY,
+ NON_NULL_HEADERS},
+ {"test Declared Constructor", 509,
+ DefinedConstructorWithAnnotationForNonSupportedBody.class, NO_REQUEST, NULL_BODY,
+ NO_HEADERS},
+ {"test Declared Constructor", 510,
+ DefinedConstructorWithAnnotationForOptionalBody.class, NO_REQUEST,
+ Optional.of(NON_NULL_BODY),
+ NO_HEADERS},
+ {"test Declared Constructor", 511,
+ DefinedConstructorWithRequest.class, REQUEST, NO_BODY,
+ NO_HEADERS},
+ {"test Declared Constructor", 512,
+ DefinedConstructorWithRequestAndResponseBody.class, REQUEST, NON_NULL_BODY,
+ NO_HEADERS},
+ {"test Declared Constructor", 513,
+ DefinedConstructorWithRequestAndAnnotationForResponseBody.class, REQUEST, NON_NULL_BODY,
+ NO_HEADERS},
+ {"test Declared Constructor", 514,
+ DefinedConstructorWithRequestAndResponseHeadersAndResponseBody.class, REQUEST,
+ NON_NULL_BODY,
+ NON_NULL_HEADERS},
+ {"test Declared Constructor", 515,
+ DefinedConstructorWithRequestAndResponseHeadersAndOptionalResponseBody.class, REQUEST,
+ Optional.of(NON_NULL_BODY),
+ NON_NULL_HEADERS}
+ });
+ } // first data value (0) is default
+
+ public String testName;
+ public int errorCode;
+ public Class extends Exception> expectedExceptionClass;
+ public Object expectedRequest;
+ public Object expectedBody;
+ public Map> expectedHeaders;
+
+ @MethodSource("data")
+ @ParameterizedTest(
+ name = "{0}: When error code ({1}) on method ({2}) should return exception type ({3})")
+ void test(String testName,
+ int errorCode,
+ Class extends Exception> expectedExceptionClass,
+ Object expectedRequest,
+ Object expectedBody,
+ Map> expectedHeaders)
+ throws Exception {
+ initAnnotationErrorDecoderExceptionConstructorsTest(testName, errorCode, expectedExceptionClass,
+ expectedRequest, expectedBody, expectedHeaders);
+ AnnotationErrorDecoder decoder = AnnotationErrorDecoder
+ .builderFor(TestClientInterfaceWithDifferentExceptionConstructors.class)
+ .withResponseBodyDecoder(new OptionalDecoder(new Decoder.Default()))
+ .build();
+
+ Exception genericException = decoder.decode(feignConfigKey("method1Test"),
+ testResponse(errorCode, NON_NULL_BODY, NON_NULL_HEADERS));
+
+ assertThat(genericException).isInstanceOf(expectedExceptionClass);
+
+ ParametersException exception = (ParametersException) genericException;
+ assertThat(exception.body()).isEqualTo(expectedBody);
+ assertThat(exception.headers()).isEqualTo(expectedHeaders);
+ }
+
+ @MethodSource("data")
+ @ParameterizedTest(
+ name = "{0}: When error code ({1}) on method ({2}) should return exception type ({3})")
+ void ifExceptionIsNotInTheList(String testName,
+ int errorCode,
+ Class extends Exception> expectedExceptionClass,
+ Object expectedRequest,
+ Object expectedBody,
+ Map> expectedHeaders)
+ throws Exception {
+ initAnnotationErrorDecoderExceptionConstructorsTest(testName, errorCode, expectedExceptionClass,
+ expectedRequest, expectedBody, expectedHeaders);
+ AnnotationErrorDecoder decoder = AnnotationErrorDecoder
+ .builderFor(TestClientInterfaceWithDifferentExceptionConstructors.class)
+ .withResponseBodyDecoder(new OptionalDecoder(new Decoder.Default()))
+ .build();
+
+ Exception genericException = decoder.decode(feignConfigKey("method1Test"),
+ testResponse(-1, NON_NULL_BODY, NON_NULL_HEADERS));
+
+ assertThat(genericException)
+ .isInstanceOf(ErrorHandling.NO_DEFAULT.class)
+ .hasMessage("Endpoint responded with -1, reason: null");
+ }
+
+ interface TestClientInterfaceWithDifferentExceptionConstructors {
+
+ @ErrorHandling(codeSpecific = {
+ @ErrorCodes(codes = {500}, generate = DefaultConstructorException.class),
+ @ErrorCodes(codes = {501}, generate = DeclaredDefaultConstructorException.class),
+ @ErrorCodes(codes = {502},
+ generate = DeclaredDefaultConstructorWithOtherConstructorsException.class),
+ @ErrorCodes(codes = {503}, generate = DefinedConstructorWithNoAnnotationForBody.class),
+ @ErrorCodes(codes = {504}, generate = DefinedConstructorWithAnnotationForBody.class),
+ @ErrorCodes(codes = {505},
+ generate = DefinedConstructorWithAnnotationForBodyAndHeaders.class),
+ @ErrorCodes(codes = {506},
+ generate = DefinedConstructorWithAnnotationForBodyAndHeadersSecondOrder.class),
+ @ErrorCodes(codes = {507}, generate = DefinedConstructorWithAnnotationForHeaders.class),
+ @ErrorCodes(codes = {508},
+ generate = DefinedConstructorWithAnnotationForHeadersButNotForBody.class),
+ @ErrorCodes(codes = {509},
+ generate = DefinedConstructorWithAnnotationForNonSupportedBody.class),
+ @ErrorCodes(codes = {510},
+ generate = DefinedConstructorWithAnnotationForOptionalBody.class),
+ @ErrorCodes(codes = {511},
+ generate = DefinedConstructorWithRequest.class),
+ @ErrorCodes(codes = {512},
+ generate = DefinedConstructorWithRequestAndResponseBody.class),
+ @ErrorCodes(codes = {513},
+ generate = DefinedConstructorWithRequestAndAnnotationForResponseBody.class),
+ @ErrorCodes(codes = {514},
+ generate = DefinedConstructorWithRequestAndResponseHeadersAndResponseBody.class),
+ @ErrorCodes(codes = {515},
+ generate = DefinedConstructorWithRequestAndResponseHeadersAndOptionalResponseBody.class)
+ })
+ void method1Test();
+
+ class ParametersException extends Exception {
+ public Object body() {
+ return NO_BODY;
+ }
+
+ public feign.Request request() {
+ return null;
+ }
+
+ public Map> headers() {
+ return null;
+ }
+ }
+ class DefaultConstructorException extends ParametersException {
+ }
+
+ class DeclaredDefaultConstructorException extends ParametersException {
+ public DeclaredDefaultConstructorException() {}
+ }
+
+ class DeclaredDefaultConstructorWithOtherConstructorsException extends ParametersException {
+ public DeclaredDefaultConstructorWithOtherConstructorsException() {}
+
+ public DeclaredDefaultConstructorWithOtherConstructorsException(TestPojo testPojo) {
+ throw new UnsupportedOperationException("Should not be called");
+ }
+
+ public DeclaredDefaultConstructorWithOtherConstructorsException(Throwable cause) {
+ throw new UnsupportedOperationException("Should not be called");
+ }
+
+ public DeclaredDefaultConstructorWithOtherConstructorsException(String message,
+ Throwable cause) {
+ throw new UnsupportedOperationException("Should not be called");
+ }
+ }
+
+ class DefinedConstructorWithNoAnnotationForBody extends ParametersException {
+ String body;
+
+ public DefinedConstructorWithNoAnnotationForBody() {
+ throw new UnsupportedOperationException("Should not be called");
+ }
+
+ @FeignExceptionConstructor
+ public DefinedConstructorWithNoAnnotationForBody(String body) {
+ this.body = body;
+ }
+
+ public DefinedConstructorWithNoAnnotationForBody(TestPojo testPojo) {
+ throw new UnsupportedOperationException("Should not be called");
+ }
+
+ @Override
+ public Object body() {
+ return body;
+ }
+
+ }
+
+ class DefinedConstructorWithRequest extends ParametersException {
+ feign.Request request;
+
+ public DefinedConstructorWithRequest() {
+ throw new UnsupportedOperationException("Should not be called");
+ }
+
+ @FeignExceptionConstructor
+ public DefinedConstructorWithRequest(feign.Request request) {
+ this.request = request;
+ }
+
+ public DefinedConstructorWithRequest(TestPojo testPojo) {
+ throw new UnsupportedOperationException("Should not be called");
+ }
+
+ @Override
+ public feign.Request request() {
+ return request;
+ }
+
+ }
+
+ class DefinedConstructorWithRequestAndResponseBody extends ParametersException {
+ feign.Request request;
+ String body;
+
+ public DefinedConstructorWithRequestAndResponseBody() {
+ throw new UnsupportedOperationException("Should not be called");
+ }
+
+ @FeignExceptionConstructor
+ public DefinedConstructorWithRequestAndResponseBody(feign.Request request, String body) {
+ this.request = request;
+ this.body = body;
+ }
+
+ public DefinedConstructorWithRequestAndResponseBody(TestPojo testPojo) {
+ throw new UnsupportedOperationException("Should not be called");
+ }
+
+ @Override
+ public feign.Request request() {
+ return request;
+ }
+
+ @Override
+ public String body() {
+ return body;
+ }
+ }
+
+ class DefinedConstructorWithRequestAndAnnotationForResponseBody extends ParametersException {
+ feign.Request request;
+ String body;
+
+ public DefinedConstructorWithRequestAndAnnotationForResponseBody() {
+ throw new UnsupportedOperationException("Should not be called");
+ }
+
+ @FeignExceptionConstructor
+ public DefinedConstructorWithRequestAndAnnotationForResponseBody(feign.Request request,
+ @ResponseBody String body) {
+ this.request = request;
+ this.body = body;
+ }
+
+ public DefinedConstructorWithRequestAndAnnotationForResponseBody(TestPojo testPojo) {
+ throw new UnsupportedOperationException("Should not be called");
+ }
+
+ @Override
+ public feign.Request request() {
+ return request;
+ }
+
+ @Override
+ public String body() {
+ return body;
+ }
+ }
+
+ class DefinedConstructorWithRequestAndResponseHeadersAndResponseBody
+ extends ParametersException {
+ feign.Request request;
+ String body;
+ Map headers;
+
+ public DefinedConstructorWithRequestAndResponseHeadersAndResponseBody() {
+ throw new UnsupportedOperationException("Should not be called");
+ }
+
+ @FeignExceptionConstructor
+ public DefinedConstructorWithRequestAndResponseHeadersAndResponseBody(feign.Request request,
+ @ResponseHeaders Map headers,
+ @ResponseBody String body) {
+ this.request = request;
+ this.body = body;
+ this.headers = headers;
+ }
+
+ public DefinedConstructorWithRequestAndResponseHeadersAndResponseBody(TestPojo testPojo) {
+ throw new UnsupportedOperationException("Should not be called");
+ }
+
+ @Override
+ public feign.Request request() {
+ return request;
+ }
+
+ @Override
+ public Map headers() {
+ return headers;
+ }
+
+ @Override
+ public String body() {
+ return body;
+ }
+ }
+
+ class DefinedConstructorWithRequestAndResponseHeadersAndOptionalResponseBody
+ extends ParametersException {
+ feign.Request request;
+ Optional body;
+ Map headers;
+
+ public DefinedConstructorWithRequestAndResponseHeadersAndOptionalResponseBody() {
+ throw new UnsupportedOperationException("Should not be called");
+ }
+
+ @FeignExceptionConstructor
+ public DefinedConstructorWithRequestAndResponseHeadersAndOptionalResponseBody(
+ feign.Request request,
+ @ResponseHeaders Map headers,
+ @ResponseBody Optional body) {
+ this.request = request;
+ this.body = body;
+ this.headers = headers;
+ }
+
+ public DefinedConstructorWithRequestAndResponseHeadersAndOptionalResponseBody(
+ TestPojo testPojo) {
+ throw new UnsupportedOperationException("Should not be called");
+ }
+
+ @Override
+ public feign.Request request() {
+ return request;
+ }
+
+ @Override
+ public Map headers() {
+ return headers;
+ }
+
+ @Override
+ public Object body() {
+ return body;
+ }
+ }
+
+ class DefinedConstructorWithAnnotationForBody extends ParametersException {
+ String body;
+
+ public DefinedConstructorWithAnnotationForBody() {
+ throw new UnsupportedOperationException("Should not be called");
+ }
+
+ @FeignExceptionConstructor
+ public DefinedConstructorWithAnnotationForBody(@ResponseBody String body) {
+ this.body = body;
+ }
+
+ @Override
+ public Object body() {
+ return body;
+ }
+ }
+
+ class DefinedConstructorWithAnnotationForOptionalBody extends ParametersException {
+ Optional body;
+
+ public DefinedConstructorWithAnnotationForOptionalBody() {
+ throw new UnsupportedOperationException("Should not be called");
+ }
+
+ @FeignExceptionConstructor
+ public DefinedConstructorWithAnnotationForOptionalBody(@ResponseBody Optional body) {
+ this.body = body;
+ }
+
+ @Override
+ public Object body() {
+ return body;
+ }
+ }
+
+ class DefinedConstructorWithAnnotationForNonSupportedBody extends ParametersException {
+ TestPojo body;
+
+ @FeignExceptionConstructor
+ public DefinedConstructorWithAnnotationForNonSupportedBody(@ResponseBody TestPojo body) {
+ this.body = body;
+ }
+
+ @Override
+ public Object body() {
+ return body;
+ }
+ }
+
+ class DefinedConstructorWithAnnotationForBodyAndHeaders extends ParametersException {
+ String body;
+ Map> headers;
+
+ @FeignExceptionConstructor
+ public DefinedConstructorWithAnnotationForBodyAndHeaders(@ResponseBody String body,
+ @ResponseHeaders Map> headers) {
+ this.body = body;
+ this.headers = headers;
+ }
+
+ @Override
+ public Object body() {
+ return body;
+ }
+
+ @Override
+ public Map> headers() {
+ return headers;
+ }
+ }
+
+ class DefinedConstructorWithAnnotationForBodyAndHeadersSecondOrder extends ParametersException {
+ String body;
+ Map> headers;
+
+ @FeignExceptionConstructor
+ public DefinedConstructorWithAnnotationForBodyAndHeadersSecondOrder(
+ @ResponseHeaders Map> headers, @ResponseBody String body) {
+ this.body = body;
+ this.headers = headers;
+ }
+
+ @Override
+ public Object body() {
+ return body;
+ }
+
+ @Override
+ public Map> headers() {
+ return headers;
+ }
+ }
+
+ class DefinedConstructorWithAnnotationForHeaders extends ParametersException {
+ Map> headers;
+
+ @FeignExceptionConstructor
+ public DefinedConstructorWithAnnotationForHeaders(
+ @ResponseHeaders Map> headers) {
+ this.headers = headers;
+ }
+
+ @Override
+ public Map> headers() {
+ return headers;
+ }
+ }
+
+ class DefinedConstructorWithAnnotationForHeadersButNotForBody extends ParametersException {
+ String body;
+ Map> headers;
+
+ @FeignExceptionConstructor
+ public DefinedConstructorWithAnnotationForHeadersButNotForBody(
+ @ResponseHeaders Map> headers, String body) {
+ this.body = body;
+ this.headers = headers;
+ }
+
+ @Override
+ public Object body() {
+ return body;
+ }
+
+ @Override
+ public Map> headers() {
+ return headers;
+ }
+ }
+ }
+
+ public void initAnnotationErrorDecoderExceptionConstructorsTest(String testName,
+ int errorCode,
+ Class extends Exception> expectedExceptionClass,
+ Object expectedRequest,
+ Object expectedBody,
+ Map> expectedHeaders) {
+ this.testName = testName;
+ this.errorCode = errorCode;
+ this.expectedExceptionClass = expectedExceptionClass;
+ this.expectedRequest = expectedRequest;
+ this.expectedBody = expectedBody;
+ this.expectedHeaders = expectedHeaders;
+ }
+}
diff --git a/annotation-error-decoder/src/test/java/feign/error/AnnotationErrorDecoderIllegalInterfacesTest.java b/annotation-error-decoder/src/test/java/feign/error/AnnotationErrorDecoderIllegalInterfacesTest.java
new file mode 100644
index 000000000..52c89af11
--- /dev/null
+++ b/annotation-error-decoder/src/test/java/feign/error/AnnotationErrorDecoderIllegalInterfacesTest.java
@@ -0,0 +1,183 @@
+/*
+ * Copyright 2012-2023 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.error;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.fail;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Map;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+import feign.Request;
+
+public class AnnotationErrorDecoderIllegalInterfacesTest {
+
+ public static Iterable data() {
+ return Arrays.asList(new Object[][] {
+ {IllegalTestClientInterfaceWithTooManyMappingsToSingleCode.class,
+ IllegalStateException.class, "Status Code [404] has already been declared"},
+ {IllegalTestClientInterfaceWithExceptionWithTooManyConstructors.class,
+ IllegalStateException.class, "Too many constructors"},
+ {IllegalTestClientInterfaceWithExceptionWithTooManyBodyParams.class,
+ IllegalStateException.class, "Cannot have two parameters either without"},
+ {IllegalTestClientInterfaceWithExceptionWithTooManyHeaderParams.class,
+ IllegalStateException.class, "Cannot have two parameters tagged"},
+ {IllegalTestClientInterfaceWithExceptionWithBadHeaderParams.class,
+ IllegalStateException.class, "Cannot generate exception - check constructor"},
+ {IllegalTestClientInterfaceWithExceptionWithBadHeaderObjectParam.class,
+ IllegalStateException.class, "Response Header map must be of type Map"},
+ {IllegalTestClientInterfaceWithExceptionWithTooManyRequestParams.class,
+ IllegalStateException.class, "Cannot have two parameters either without"}
+ });
+ } // first data value (0) is default
+
+ public Class testInterface;
+ public Class extends Exception> expectedExceptionClass;
+ public String messageStart;
+
+ @MethodSource("data")
+ @ParameterizedTest(
+ name = "{index}: When building interface ({0}) should return exception type ({1}) with message ({2})")
+ void test(Class testInterface,
+ Class extends Exception> expectedExceptionClass,
+ String messageStart)
+ throws Exception {
+ initAnnotationErrorDecoderIllegalInterfacesTest(testInterface, expectedExceptionClass,
+ messageStart);
+ try {
+ AnnotationErrorDecoder.builderFor(testInterface).build();
+ fail("Should have thrown exception");
+ } catch (Exception e) {
+ assertThat(e.getClass()).isEqualTo(expectedExceptionClass);
+ assertThat(e.getMessage()).startsWith(messageStart);
+ }
+ }
+
+ interface IllegalTestClientInterfaceWithTooManyMappingsToSingleCode {
+ @ErrorHandling(codeSpecific = {
+ @ErrorCodes(codes = {404}, generate = Exception.class),
+ @ErrorCodes(codes = {404}, generate = Exception.class)
+ })
+ void method1Test();
+ }
+
+ interface IllegalTestClientInterfaceWithExceptionWithTooManyConstructors {
+ @ErrorHandling(codeSpecific = {
+ @ErrorCodes(codes = {404}, generate = BadException.class)
+ })
+ void method1Test();
+
+ class BadException extends Exception {
+
+ @FeignExceptionConstructor
+ public BadException(String body) {
+
+ }
+
+ @FeignExceptionConstructor
+ public BadException(String body, @ResponseHeaders Map> headers) {
+
+ }
+ }
+ }
+
+ interface IllegalTestClientInterfaceWithExceptionWithTooManyBodyParams {
+ @ErrorHandling(codeSpecific = {
+ @ErrorCodes(codes = {404}, generate = BadException.class)
+ })
+ void method1Test();
+
+ class BadException extends Exception {
+
+ @FeignExceptionConstructor
+ public BadException(String body, @ResponseBody String otherBody) {
+
+ }
+ }
+ }
+
+ interface IllegalTestClientInterfaceWithExceptionWithTooManyRequestParams {
+ @ErrorHandling(codeSpecific = {
+ @ErrorCodes(codes = {404}, generate = BadException.class)
+ })
+ void method1Test();
+
+ class BadException extends Exception {
+
+ @FeignExceptionConstructor
+ public BadException(Request request1, Request request2) {
+
+ }
+ }
+ }
+
+ interface IllegalTestClientInterfaceWithExceptionWithTooManyHeaderParams {
+
+ @ErrorHandling(codeSpecific = {
+ @ErrorCodes(codes = {404}, generate = BadException.class)
+ })
+ void method1Test();
+
+ class BadException extends Exception {
+
+ @FeignExceptionConstructor
+ public BadException(@ResponseHeaders Map> headers1,
+ @ResponseHeaders Map> headers2) {
+
+ }
+ }
+ }
+
+ interface IllegalTestClientInterfaceWithExceptionWithBadHeaderParams {
+
+ @ErrorHandling(codeSpecific = {
+ @ErrorCodes(codes = {404}, generate = BadException.class)
+ })
+ void method1Test();
+
+ class BadException extends Exception {
+
+ @FeignExceptionConstructor
+ public BadException(@ResponseHeaders Map headers1) {
+ headers1.get(3);
+ }
+ }
+ }
+
+ interface IllegalTestClientInterfaceWithExceptionWithBadHeaderObjectParam {
+
+ @ErrorHandling(codeSpecific = {
+ @ErrorCodes(codes = {404}, generate = BadException.class)
+ })
+ void method1Test();
+
+ class BadException extends Exception {
+
+ @FeignExceptionConstructor
+ public BadException(@ResponseHeaders TestPojo headers1) {}
+ }
+ }
+
+ public void initAnnotationErrorDecoderIllegalInterfacesTest(Class testInterface,
+ Class extends Exception> expectedExceptionClass,
+ String messageStart) {
+ this.testInterface = testInterface;
+ this.expectedExceptionClass = expectedExceptionClass;
+ this.messageStart = messageStart;
+ }
+
+
+
+}
diff --git a/annotation-error-decoder/src/test/java/feign/error/AnnotationErrorDecoderInheritanceClassLevelAnnotationTest.java b/annotation-error-decoder/src/test/java/feign/error/AnnotationErrorDecoderInheritanceClassLevelAnnotationTest.java
new file mode 100644
index 000000000..11a3903e0
--- /dev/null
+++ b/annotation-error-decoder/src/test/java/feign/error/AnnotationErrorDecoderInheritanceClassLevelAnnotationTest.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2012-2023 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.error;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Arrays;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+
+public class AnnotationErrorDecoderInheritanceClassLevelAnnotationTest extends
+ AbstractAnnotationErrorDecoderTest {
+ @Override
+ public Class interfaceAtTest() {
+ return SecondLevelInterface.class;
+ }
+
+ public static Iterable data() {
+ return Arrays.asList(new Object[][] {
+ {"Test Code Specific At Method", 403, "topLevelMethod",
+ SecondLevelClassDefaultException.class},
+ {"Test Code Specific At Method", 403, "secondLevelMethod",
+ SecondLevelMethodDefaultException.class},
+ {"Test Code Specific At Method", 404, "topLevelMethod",
+ SecondLevelClassAnnotationException.class},
+ {"Test Code Specific At Method", 404, "secondLevelMethod",
+ SecondLevelMethodErrorHandlingException.class},
+ });
+ } // first data value (0) is default
+
+ public String testType;
+ public int errorCode;
+ public String method;
+ public Class extends Exception> expectedExceptionClass;
+
+ @MethodSource("data")
+ @ParameterizedTest(
+ name = "{0}: When error code ({1}) on method ({2}) should return exception type ({3})")
+ void test(String testType,
+ int errorCode,
+ String method,
+ Class extends Exception> expectedExceptionClass)
+ throws Exception {
+ initAnnotationErrorDecoderInheritanceClassLevelAnnotationTest(testType, errorCode, method,
+ expectedExceptionClass);
+ AnnotationErrorDecoder decoder =
+ AnnotationErrorDecoder.builderFor(SecondLevelInterface.class).build();
+
+ assertThat(decoder.decode(feignConfigKey(method), testResponse(errorCode)).getClass())
+ .isEqualTo(expectedExceptionClass);
+ }
+
+ @TopLevelClassError
+ interface TopLevelInterface {
+ @ErrorHandling
+ void topLevelMethod();
+ }
+
+ @SecondLevelClassError
+ interface SecondLevelInterface extends TopLevelInterface {
+ @ErrorHandling(
+ codeSpecific = {
+ @ErrorCodes(codes = {404}, generate = SecondLevelMethodErrorHandlingException.class)},
+ defaultException = SecondLevelMethodDefaultException.class)
+ void secondLevelMethod();
+ }
+
+ @ErrorHandling(
+ codeSpecific = {
+ @ErrorCodes(codes = {403, 404}, generate = TopLevelClassAnnotationException.class),},
+ defaultException = TopLevelClassDefaultException.class)
+ @Retention(RetentionPolicy.RUNTIME)
+ @interface TopLevelClassError {
+ }
+
+ @ErrorHandling(
+ codeSpecific = {
+ @ErrorCodes(codes = {404}, generate = SecondLevelClassAnnotationException.class),},
+ defaultException = SecondLevelClassDefaultException.class)
+ @Retention(RetentionPolicy.RUNTIME)
+ @interface SecondLevelClassError {
+ }
+
+ static class TopLevelClassDefaultException extends Exception {
+ public TopLevelClassDefaultException() {}
+ }
+ static class TopLevelClassAnnotationException extends Exception {
+ public TopLevelClassAnnotationException() {}
+ }
+ static class SecondLevelClassDefaultException extends Exception {
+ public SecondLevelClassDefaultException() {}
+ }
+ static class SecondLevelMethodDefaultException extends Exception {
+ public SecondLevelMethodDefaultException() {}
+ }
+ static class SecondLevelClassAnnotationException extends Exception {
+ public SecondLevelClassAnnotationException() {}
+ }
+ static class SecondLevelMethodErrorHandlingException extends Exception {
+ public SecondLevelMethodErrorHandlingException() {}
+ }
+
+ public void initAnnotationErrorDecoderInheritanceClassLevelAnnotationTest(String testType,
+ int errorCode,
+ String method,
+ Class extends Exception> expectedExceptionClass) {
+ this.testType = testType;
+ this.errorCode = errorCode;
+ this.method = method;
+ this.expectedExceptionClass = expectedExceptionClass;
+ }
+}
diff --git a/annotation-error-decoder/src/test/java/feign/error/AnnotationErrorDecoderInheritanceMethodLevelAnnotationTest.java b/annotation-error-decoder/src/test/java/feign/error/AnnotationErrorDecoderInheritanceMethodLevelAnnotationTest.java
new file mode 100644
index 000000000..abf8aa448
--- /dev/null
+++ b/annotation-error-decoder/src/test/java/feign/error/AnnotationErrorDecoderInheritanceMethodLevelAnnotationTest.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright 2012-2023 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.error;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Arrays;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+
+public class AnnotationErrorDecoderInheritanceMethodLevelAnnotationTest extends
+ AbstractAnnotationErrorDecoderTest {
+ @Override
+ public Class interfaceAtTest() {
+ return SecondLevelInterface.class;
+ }
+
+ public static Iterable data() {
+ return Arrays.asList(new Object[][] {
+ {"Test Code Specific At Method", 403, "topLevelMethod1",
+ MethodTopLevelDefaultException.class},
+ {"Test Code Specific At Method", 404, "topLevelMethod1",
+ MethodTopLevelAnnotationException.class},
+ {"Test Code Specific At Method", 403, "topLevelMethod2",
+ MethodSecondLevelDefaultException.class},
+ {"Test Code Specific At Method", 404, "topLevelMethod2",
+ MethodSecondLevelAnnotationException.class},
+ {"Test Code Specific At Method", 403, "topLevelMethod3",
+ MethodSecondLevelDefaultException.class},
+ {"Test Code Specific At Method", 404, "topLevelMethod3",
+ MethodSecondLevelErrorHandlingException.class},
+ {"Test Code Specific At Method", 403, "topLevelMethod4",
+ MethodSecondLevelDefaultException.class},
+ {"Test Code Specific At Method", 404, "topLevelMethod4",
+ MethodSecondLevelAnnotationException.class},
+ });
+ } // first data value (0) is default
+
+ public String testType;
+ public int errorCode;
+ public String method;
+ public Class extends Exception> expectedExceptionClass;
+
+ @MethodSource("data")
+ @ParameterizedTest(
+ name = "{0}: When error code ({1}) on method ({2}) should return exception type ({3})")
+ void test(String testType,
+ int errorCode,
+ String method,
+ Class extends Exception> expectedExceptionClass)
+ throws Exception {
+ initAnnotationErrorDecoderInheritanceMethodLevelAnnotationTest(testType, errorCode, method,
+ expectedExceptionClass);
+ AnnotationErrorDecoder decoder =
+ AnnotationErrorDecoder.builderFor(SecondLevelInterface.class).build();
+
+ assertThat(decoder.decode(feignConfigKey(method), testResponse(errorCode)).getClass())
+ .isEqualTo(expectedExceptionClass);
+ }
+
+ interface TopLevelInterface {
+ @TopLevelMethodErrorHandling
+ void topLevelMethod1();
+
+ @TopLevelMethodErrorHandling
+ void topLevelMethod2();
+
+ @TopLevelMethodErrorHandling
+ void topLevelMethod3();
+
+ @ErrorHandling(codeSpecific = @ErrorCodes(codes = {404},
+ generate = TopLevelMethodErrorHandlingException.class))
+ void topLevelMethod4();
+ }
+
+ interface SecondLevelInterface extends TopLevelInterface {
+ @Override
+ @SecondLevelMethodErrorHandling
+ void topLevelMethod2();
+
+ @Override
+ @ErrorHandling(
+ codeSpecific = {
+ @ErrorCodes(codes = {404}, generate = MethodSecondLevelErrorHandlingException.class)},
+ defaultException = MethodSecondLevelDefaultException.class)
+ void topLevelMethod3();
+
+ @Override
+ @SecondLevelMethodErrorHandling
+ void topLevelMethod4();
+ }
+
+ @ErrorHandling(
+ codeSpecific = {
+ @ErrorCodes(codes = {404}, generate = MethodTopLevelAnnotationException.class),},
+ defaultException = MethodTopLevelDefaultException.class)
+ @Retention(RetentionPolicy.RUNTIME)
+ @interface TopLevelMethodErrorHandling {
+ }
+
+ @ErrorHandling(
+ codeSpecific = {
+ @ErrorCodes(codes = {404}, generate = MethodSecondLevelAnnotationException.class),},
+ defaultException = MethodSecondLevelDefaultException.class)
+ @Retention(RetentionPolicy.RUNTIME)
+ @interface SecondLevelMethodErrorHandling {
+ }
+
+ static class MethodTopLevelDefaultException extends Exception {
+ public MethodTopLevelDefaultException() {}
+ }
+ static class TopLevelMethodErrorHandlingException extends Exception {
+ public TopLevelMethodErrorHandlingException() {}
+ }
+ static class MethodTopLevelAnnotationException extends Exception {
+ public MethodTopLevelAnnotationException() {}
+ }
+ static class MethodSecondLevelDefaultException extends Exception {
+ public MethodSecondLevelDefaultException() {}
+ }
+ static class MethodSecondLevelErrorHandlingException extends Exception {
+ public MethodSecondLevelErrorHandlingException() {}
+ }
+ static class MethodSecondLevelAnnotationException extends Exception {
+ public MethodSecondLevelAnnotationException() {}
+ }
+
+ public void initAnnotationErrorDecoderInheritanceMethodLevelAnnotationTest(String testType,
+ int errorCode,
+ String method,
+ Class extends Exception> expectedExceptionClass) {
+ this.testType = testType;
+ this.errorCode = errorCode;
+ this.method = method;
+ this.expectedExceptionClass = expectedExceptionClass;
+ }
+}
diff --git a/annotation-error-decoder/src/test/java/feign/error/AnnotationErrorDecoderInheritanceTest.java b/annotation-error-decoder/src/test/java/feign/error/AnnotationErrorDecoderInheritanceTest.java
new file mode 100644
index 000000000..5b7688279
--- /dev/null
+++ b/annotation-error-decoder/src/test/java/feign/error/AnnotationErrorDecoderInheritanceTest.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright 2012-2023 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.error;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import java.util.Arrays;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+import feign.error.AnnotationErrorDecoderInheritanceTest.TopLevelInterface.ClassLevelDefaultException;
+import feign.error.AnnotationErrorDecoderInheritanceTest.TopLevelInterface.ClassLevelNotFoundException;
+import feign.error.AnnotationErrorDecoderInheritanceTest.TopLevelInterface.Method1DefaultException;
+import feign.error.AnnotationErrorDecoderInheritanceTest.TopLevelInterface.Method1NotFoundException;
+import feign.error.AnnotationErrorDecoderInheritanceTest.TopLevelInterface.Method2NotFoundException;
+import feign.error.AnnotationErrorDecoderInheritanceTest.TopLevelInterface.Method3DefaultException;
+import feign.error.AnnotationErrorDecoderInheritanceTest.TopLevelInterface.ServeErrorException;
+import feign.error.AnnotationErrorDecoderInheritanceTest.TopLevelInterface.UnauthenticatedOrUnauthorizedException;
+
+public class AnnotationErrorDecoderInheritanceTest extends
+ AbstractAnnotationErrorDecoderTest {
+
+ @Override
+ public Class interfaceAtTest() {
+ return TestClientInterfaceWithExceptionPriority.class;
+ }
+
+ public static Iterable data() {
+ return Arrays.asList(new Object[][] {
+ {"Test Code Specific At Method", 404, "method1Test", Method1NotFoundException.class},
+ {"Test Code Specific At Method", 401, "method1Test",
+ UnauthenticatedOrUnauthorizedException.class},
+ {"Test Code Specific At Method", 404, "method2Test", Method2NotFoundException.class},
+ {"Test Code Specific At Method", 500, "method2Test", ServeErrorException.class},
+ {"Test Code Specific At Method", 503, "method2Test", ServeErrorException.class},
+ {"Test Code Specific At Class", 403, "method1Test",
+ UnauthenticatedOrUnauthorizedException.class},
+ {"Test Code Specific At Class", 403, "method2Test",
+ UnauthenticatedOrUnauthorizedException.class},
+ {"Test Code Specific At Class", 404, "method3Test", ClassLevelNotFoundException.class},
+ {"Test Code Specific At Class", 403, "method3Test",
+ UnauthenticatedOrUnauthorizedException.class},
+ {"Test Default At Method", 504, "method1Test", Method1DefaultException.class},
+ {"Test Default At Method", 504, "method3Test", Method3DefaultException.class},
+ {"Test Default At Class", 504, "method2Test", ClassLevelDefaultException.class},
+ });
+ } // first data value (0) is default
+
+ public String testType;
+ public int errorCode;
+ public String method;
+ public Class extends Exception> expectedExceptionClass;
+
+ @MethodSource("data")
+ @ParameterizedTest(
+ name = "{0}: When error code ({1}) on method ({2}) should return exception type ({3})")
+ void test(String testType,
+ int errorCode,
+ String method,
+ Class extends Exception> expectedExceptionClass)
+ throws Exception {
+ initAnnotationErrorDecoderInheritanceTest(testType, errorCode, method, expectedExceptionClass);
+ AnnotationErrorDecoder decoder =
+ AnnotationErrorDecoder.builderFor(TestClientInterfaceWithExceptionPriority.class).build();
+
+ assertThat(decoder.decode(feignConfigKey(method), testResponse(errorCode)).getClass())
+ .isEqualTo(expectedExceptionClass);
+ }
+
+
+ @ErrorHandling(codeSpecific = {
+ @ErrorCodes(codes = {404}, generate = ClassLevelNotFoundException.class),
+ @ErrorCodes(codes = {403}, generate = UnauthenticatedOrUnauthorizedException.class)
+ },
+ defaultException = ClassLevelDefaultException.class)
+ interface TopLevelInterface {
+ @ErrorHandling(codeSpecific = {
+ @ErrorCodes(codes = {404}, generate = Method1NotFoundException.class),
+ @ErrorCodes(codes = {401}, generate = UnauthenticatedOrUnauthorizedException.class)
+ },
+ defaultException = Method1DefaultException.class)
+ void method1Test();
+
+ class ClassLevelDefaultException extends Exception {
+ }
+ class Method1DefaultException extends Exception {
+ }
+ class Method3DefaultException extends Exception {
+ }
+ class Method1NotFoundException extends Exception {
+ }
+ class Method2NotFoundException extends Exception {
+ }
+ class ClassLevelNotFoundException extends Exception {
+ }
+ class UnauthenticatedOrUnauthorizedException extends Exception {
+ }
+ class ServeErrorException extends Exception {
+ }
+
+ }
+
+ interface SecondTopLevelInterface {
+ }
+ interface SecondLevelInterface extends SecondTopLevelInterface, TopLevelInterface {
+ }
+
+ interface TestClientInterfaceWithExceptionPriority extends SecondLevelInterface {
+
+ @ErrorHandling(codeSpecific = {
+ @ErrorCodes(codes = {404}, generate = Method2NotFoundException.class),
+ @ErrorCodes(codes = {500, 503}, generate = ServeErrorException.class)
+ })
+ void method2Test();
+
+ @ErrorHandling(
+ defaultException = Method3DefaultException.class)
+ void method3Test();
+ }
+
+ public void initAnnotationErrorDecoderInheritanceTest(String testType,
+ int errorCode,
+ String method,
+ Class extends Exception> expectedExceptionClass) {
+ this.testType = testType;
+ this.errorCode = errorCode;
+ this.method = method;
+ this.expectedExceptionClass = expectedExceptionClass;
+ }
+}
diff --git a/annotation-error-decoder/src/test/java/feign/error/AnnotationErrorDecoderNoAnnotationTest.java b/annotation-error-decoder/src/test/java/feign/error/AnnotationErrorDecoderNoAnnotationTest.java
new file mode 100644
index 000000000..397910cd0
--- /dev/null
+++ b/annotation-error-decoder/src/test/java/feign/error/AnnotationErrorDecoderNoAnnotationTest.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2012-2023 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.error;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import org.junit.jupiter.api.Test;
+import feign.codec.ErrorDecoder;
+import feign.error.AnnotationErrorDecoderNoAnnotationTest.TestClientInterfaceWithNoAnnotations;
+
+public class AnnotationErrorDecoderNoAnnotationTest
+ extends AbstractAnnotationErrorDecoderTest {
+
+ @Override
+ public Class interfaceAtTest() {
+ return TestClientInterfaceWithNoAnnotations.class;
+ }
+
+ @Test
+ void delegatesToDefaultErrorDecoder() throws Exception {
+
+ ErrorDecoder defaultErrorDecoder = (methodKey, response) -> new DefaultErrorDecoderException();
+
+ AnnotationErrorDecoder decoder =
+ AnnotationErrorDecoder.builderFor(TestClientInterfaceWithNoAnnotations.class)
+ .withDefaultDecoder(defaultErrorDecoder)
+ .build();
+
+ assertThat(decoder.decode(feignConfigKey("method1Test"), testResponse(502)).getClass())
+ .isEqualTo(DefaultErrorDecoderException.class);
+ }
+
+ interface TestClientInterfaceWithNoAnnotations {
+ void method1Test();
+ }
+
+ static class DefaultErrorDecoderException extends Exception {
+ }
+
+}
diff --git a/annotation-error-decoder/src/test/java/feign/error/AnnotationErrorDecoderPriorityTest.java b/annotation-error-decoder/src/test/java/feign/error/AnnotationErrorDecoderPriorityTest.java
new file mode 100644
index 000000000..cfda8e937
--- /dev/null
+++ b/annotation-error-decoder/src/test/java/feign/error/AnnotationErrorDecoderPriorityTest.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright 2012-2023 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.error;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import java.util.Arrays;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+import feign.error.AnnotationErrorDecoderPriorityTest.TestClientInterfaceWithExceptionPriority.ClassLevelDefaultException;
+import feign.error.AnnotationErrorDecoderPriorityTest.TestClientInterfaceWithExceptionPriority.ClassLevelNotFoundException;
+import feign.error.AnnotationErrorDecoderPriorityTest.TestClientInterfaceWithExceptionPriority.Method1DefaultException;
+import feign.error.AnnotationErrorDecoderPriorityTest.TestClientInterfaceWithExceptionPriority.Method1NotFoundException;
+import feign.error.AnnotationErrorDecoderPriorityTest.TestClientInterfaceWithExceptionPriority.Method2NotFoundException;
+import feign.error.AnnotationErrorDecoderPriorityTest.TestClientInterfaceWithExceptionPriority.Method3DefaultException;
+import feign.error.AnnotationErrorDecoderPriorityTest.TestClientInterfaceWithExceptionPriority.ServeErrorException;
+import feign.error.AnnotationErrorDecoderPriorityTest.TestClientInterfaceWithExceptionPriority.UnauthenticatedOrUnauthorizedException;
+
+public class AnnotationErrorDecoderPriorityTest extends
+ AbstractAnnotationErrorDecoderTest {
+
+ @Override
+ public Class interfaceAtTest() {
+ return TestClientInterfaceWithExceptionPriority.class;
+ }
+
+ public static Iterable data() {
+ return Arrays.asList(new Object[][] {
+ {"Test Code Specific At Method", 404, "method1Test", Method1NotFoundException.class},
+ {"Test Code Specific At Method", 401, "method1Test",
+ UnauthenticatedOrUnauthorizedException.class},
+ {"Test Code Specific At Method", 404, "method2Test", Method2NotFoundException.class},
+ {"Test Code Specific At Method", 500, "method2Test", ServeErrorException.class},
+ {"Test Code Specific At Method", 503, "method2Test", ServeErrorException.class},
+ {"Test Code Specific At Class", 403, "method1Test",
+ UnauthenticatedOrUnauthorizedException.class},
+ {"Test Code Specific At Class", 403, "method2Test",
+ UnauthenticatedOrUnauthorizedException.class},
+ {"Test Code Specific At Class", 404, "method3Test", ClassLevelNotFoundException.class},
+ {"Test Code Specific At Class", 403, "method3Test",
+ UnauthenticatedOrUnauthorizedException.class},
+ {"Test Default At Method", 504, "method1Test", Method1DefaultException.class},
+ {"Test Default At Method", 504, "method3Test", Method3DefaultException.class},
+ {"Test Default At Class", 504, "method2Test", ClassLevelDefaultException.class},
+ });
+ } // first data value (0) is default
+
+ public String testType;
+ public int errorCode;
+ public String method;
+ public Class extends Exception> expectedExceptionClass;
+
+ @MethodSource("data")
+ @ParameterizedTest(
+ name = "{0}: When error code ({1}) on method ({2}) should return exception type ({3})")
+ void test(String testType,
+ int errorCode,
+ String method,
+ Class extends Exception> expectedExceptionClass)
+ throws Exception {
+ initAnnotationErrorDecoderPriorityTest(testType, errorCode, method, expectedExceptionClass);
+ AnnotationErrorDecoder decoder =
+ AnnotationErrorDecoder.builderFor(TestClientInterfaceWithExceptionPriority.class).build();
+
+ assertThat(decoder.decode(feignConfigKey(method), testResponse(errorCode)).getClass())
+ .isEqualTo(expectedExceptionClass);
+ }
+
+
+ @ErrorHandling(codeSpecific = {
+ @ErrorCodes(codes = {404}, generate = ClassLevelNotFoundException.class),
+ @ErrorCodes(codes = {403}, generate = UnauthenticatedOrUnauthorizedException.class)
+ },
+ defaultException = ClassLevelDefaultException.class)
+ interface TestClientInterfaceWithExceptionPriority {
+ @ErrorHandling(codeSpecific = {
+ @ErrorCodes(codes = {404}, generate = Method1NotFoundException.class),
+ @ErrorCodes(codes = {401}, generate = UnauthenticatedOrUnauthorizedException.class)
+ },
+ defaultException = Method1DefaultException.class)
+ void method1Test();
+
+ @ErrorHandling(codeSpecific = {
+ @ErrorCodes(codes = {404}, generate = Method2NotFoundException.class),
+ @ErrorCodes(codes = {500, 503}, generate = ServeErrorException.class)
+ })
+ void method2Test();
+
+ @ErrorHandling(
+ defaultException = Method3DefaultException.class)
+ void method3Test();
+
+
+
+ class ClassLevelDefaultException extends Exception {
+ }
+ class Method1DefaultException extends Exception {
+ }
+ class Method3DefaultException extends Exception {
+ }
+ class Method1NotFoundException extends Exception {
+ }
+ class Method2NotFoundException extends Exception {
+ }
+ class ClassLevelNotFoundException extends Exception {
+ }
+ class UnauthenticatedOrUnauthorizedException extends Exception {
+ }
+ class ServeErrorException extends Exception {
+ }
+ }
+
+ public void initAnnotationErrorDecoderPriorityTest(String testType,
+ int errorCode,
+ String method,
+ Class extends Exception> expectedExceptionClass) {
+ this.testType = testType;
+ this.errorCode = errorCode;
+ this.method = method;
+ this.expectedExceptionClass = expectedExceptionClass;
+ }
+}
diff --git a/annotation-error-decoder/src/test/java/feign/error/TestPojo.java b/annotation-error-decoder/src/test/java/feign/error/TestPojo.java
new file mode 100644
index 000000000..af1e725f0
--- /dev/null
+++ b/annotation-error-decoder/src/test/java/feign/error/TestPojo.java
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2012-2023 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.error;
+
+public class TestPojo {
+ public String aString;
+ public int anInt;
+}
diff --git a/apt-test-generator/README.md b/apt-test-generator/README.md
new file mode 100644
index 000000000..35d998452
--- /dev/null
+++ b/apt-test-generator/README.md
@@ -0,0 +1,66 @@
+# Feign APT test generator
+This module generates mock clients for tests based on feign interfaces
+
+## Usage
+
+Just need to add this module to dependency list and Java [Annotation Processing Tool](https://docs.oracle.com/javase/7/docs/technotes/guides/apt/GettingStarted.html) will automatically pick up the jar and generate test clients.
+
+There are 2 main alternatives to include this to a project:
+
+1. Just add to classpath and java compiler should automaticaly detect and run code generation. On maven this is done like this:
+
+```xml
+
+ io.github.openfeign.experimental
+ feign-apt-test-generator
+ ${feign.version}
+ test
+
+```
+
+1. Use a purpose build tool that allow to pick output location and don't mix dependencies onto classpath
+
+```xml
+
+ com.mysema.maven
+ apt-maven-plugin
+ 1.1.3
+
+
+
+ process
+
+
+ target/generated-test-sources/feign
+ feign.apttestgenerator.GenerateTestStubAPT
+
+
+
+
+
+ io.github.openfeign.experimental
+ feign-apt-test-generator
+ ${feign.version}
+
+
+
+
+ org.codehaus.mojo
+ build-helper-maven-plugin
+ 3.0.0
+
+
+ feign-stubs-source
+ generate-test-sources
+
+ add-test-source
+
+
+
+ target/generated-test-sources/feign
+
+
+
+
+
+```
diff --git a/apt-test-generator/pom.xml b/apt-test-generator/pom.xml
new file mode 100644
index 000000000..8ca20444d
--- /dev/null
+++ b/apt-test-generator/pom.xml
@@ -0,0 +1,196 @@
+
+
+
+ 4.0.0
+
+
+ io.github.openfeign
+ parent
+ 13.5-SNAPSHOT
+
+
+ io.github.openfeign.experimental
+ feign-apt-test-generator
+ Feign APT test generator
+ Feign code generation tool for mocked clients
+
+
+ ${project.basedir}/..
+
+ true
+
+
+
+
+ com.github.jknack
+ handlebars
+ 4.3.1
+
+
+
+ io.github.openfeign
+ feign-example-github
+ ${project.version}
+
+
+
+ com.google.testing.compile
+ compile-testing
+ 0.21.0
+ test
+
+
+ junit
+ junit
+
+
+
+
+ org.slf4j
+ slf4j-jdk14
+ ${slf4j.version}
+ test
+
+
+
+ com.google.guava
+ guava
+ ${guava.version}
+
+
+ com.google.auto.service
+ auto-service-annotations
+ 1.1.1
+ provided
+
+
+
+
+
+
+ docker
+ true
+
+ ${project.basedir}/docker
+
+
+
+ ${basedir}/src/main/resources
+
+
+ src/main/java
+
+ **/*.java
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-shade-plugin
+ 3.6.0
+
+
+ package
+
+ shade
+
+
+
+
+ feign.aptgenerator.github.GitHubFactoryExample
+
+
+ false
+
+
+
+
+
+ org.skife.maven
+ really-executable-jar-maven-plugin
+ 2.1.1
+
+ github
+
+
+
+ package
+
+ really-executable-jar
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-failsafe-plugin
+
+
+
+ integration-test
+ verify
+
+
+
+
+
+
+
+ com.spotify
+ docker-maven-plugin
+ ${docker-maven-plugin.version}
+
+
+ ${project.build.directory}/classes/docker/
+
+
+ true
+
+ docker-hub
+ https://index.docker.io/v1/
+ feign-apt-generator/test
+
+
+ /
+ ${project.build.directory}
+ ${project.artifactId}-${project.version}.jar
+
+
+
+
+
+
+ post-integration-test
+
+ build
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+
+ --add-opens jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED --add-opens jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED --add-opens jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED
+
+
+
+
+
diff --git a/apt-test-generator/src/main/java/feign/apttestgenerator/ArgumentDefinition.java b/apt-test-generator/src/main/java/feign/apttestgenerator/ArgumentDefinition.java
new file mode 100644
index 000000000..4ec725a52
--- /dev/null
+++ b/apt-test-generator/src/main/java/feign/apttestgenerator/ArgumentDefinition.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2012-2023 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.apttestgenerator;
+
+public class ArgumentDefinition {
+
+ public final String name;
+ public final String type;
+
+ public ArgumentDefinition(String name, String type) {
+ super();
+ this.name = name;
+ this.type = type;
+ }
+
+}
diff --git a/apt-test-generator/src/main/java/feign/apttestgenerator/ClientDefinition.java b/apt-test-generator/src/main/java/feign/apttestgenerator/ClientDefinition.java
new file mode 100644
index 000000000..3696795d0
--- /dev/null
+++ b/apt-test-generator/src/main/java/feign/apttestgenerator/ClientDefinition.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2012-2023 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.apttestgenerator;
+
+public class ClientDefinition {
+
+ public final String jpackage;
+ public final String className;
+ public final String fullQualifiedName;
+
+ public ClientDefinition(String jpackage, String className, String fullQualifiedName) {
+ super();
+ this.jpackage = jpackage;
+ this.className = className;
+ this.fullQualifiedName = fullQualifiedName;
+ }
+
+}
diff --git a/apt-test-generator/src/main/java/feign/apttestgenerator/GenerateTestStubAPT.java b/apt-test-generator/src/main/java/feign/apttestgenerator/GenerateTestStubAPT.java
new file mode 100644
index 000000000..98267436f
--- /dev/null
+++ b/apt-test-generator/src/main/java/feign/apttestgenerator/GenerateTestStubAPT.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright 2012-2023 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.apttestgenerator;
+
+import com.github.jknack.handlebars.*;
+import com.github.jknack.handlebars.context.FieldValueResolver;
+import com.github.jknack.handlebars.context.JavaBeanValueResolver;
+import com.github.jknack.handlebars.context.MapValueResolver;
+import com.github.jknack.handlebars.io.URLTemplateSource;
+import com.google.auto.service.AutoService;
+import com.google.common.collect.ImmutableList;
+import java.io.IOError;
+import java.io.IOException;
+import java.lang.reflect.Type;
+import java.util.*;
+import java.util.stream.Collectors;
+import javax.annotation.processing.*;
+import javax.lang.model.element.*;
+import javax.lang.model.type.TypeKind;
+import javax.lang.model.type.TypeMirror;
+import javax.lang.model.type.WildcardType;
+import javax.tools.Diagnostic.Kind;
+import javax.tools.JavaFileObject;
+
+@SupportedAnnotationTypes({
+ "feign.RequestLine"
+})
+@AutoService(Processor.class)
+public class GenerateTestStubAPT extends AbstractProcessor {
+
+ @Override
+ public boolean process(Set extends TypeElement> annotations, RoundEnvironment roundEnv) {
+ System.out.println(annotations);
+ System.out.println(roundEnv);
+
+ final Map> clientsToGenerate = annotations.stream()
+ .map(roundEnv::getElementsAnnotatedWith)
+ .flatMap(Set::stream)
+ .map(ExecutableElement.class::cast)
+ .collect(Collectors.toMap(
+ annotatedMethod -> TypeElement.class.cast(annotatedMethod.getEnclosingElement()),
+ ImmutableList::of,
+ (list1, list2) -> ImmutableList.builder()
+ .addAll(list1)
+ .addAll(list2)
+ .build()));
+
+ System.out.println("Count: " + clientsToGenerate.size());
+ System.out.println("clientsToGenerate: " + clientsToGenerate);
+
+ final Handlebars handlebars = new Handlebars();
+
+ final URLTemplateSource source =
+ new URLTemplateSource("stub.mustache", getClass().getResource("/stub.mustache"));
+ Template template;
+ try {
+ template = handlebars.with(EscapingStrategy.JS).compile(source);
+ } catch (final IOException e) {
+ throw new IOError(e);
+ }
+
+
+ clientsToGenerate.forEach((type, executables) -> {
+ try {
+ final String jPackage = readPackage(type);
+ final String className = type.getSimpleName().toString();
+ final JavaFileObject builderFile = processingEnv.getFiler()
+ .createSourceFile(jPackage + "." + className + "Stub");
+
+ final ClientDefinition client = new ClientDefinition(
+ jPackage,
+ className,
+ type.toString());
+
+ final List methods = executables.stream()
+ .map(method -> {
+ final String methodName = method.getSimpleName().toString();
+
+ final List args = method.getParameters()
+ .stream()
+ .map(var -> new ArgumentDefinition(var.getSimpleName().toString(),
+ var.asType().toString()))
+ .collect(Collectors.toList());
+ return new MethodDefinition(
+ methodName,
+ method.getReturnType().toString(),
+ method.getReturnType().getKind() == TypeKind.VOID,
+ args);
+ })
+ .collect(Collectors.toList());
+
+ final Context context = Context.newBuilder(template)
+ .combine("client", client)
+ .combine("methods", methods)
+ .resolver(JavaBeanValueResolver.INSTANCE, MapValueResolver.INSTANCE,
+ FieldValueResolver.INSTANCE)
+ .build();
+ final String stubSource = template.apply(context);
+ System.out.println(stubSource);
+
+ builderFile.openWriter().append(stubSource).close();
+ } catch (final Exception e) {
+ e.printStackTrace();
+ processingEnv.getMessager().printMessage(Kind.ERROR,
+ "Unable to generate factory for " + type);
+ }
+ });
+
+ return true;
+ }
+
+
+
+ private Type toJavaType(TypeMirror type) {
+ outType(type.getClass());
+ if (type instanceof WildcardType) {
+
+ }
+ return Object.class;
+ }
+
+ private void outType(Class> class1) {
+ if (Object.class.equals(class1) || class1 == null) {
+ return;
+ }
+ System.out.println(class1);
+ outType(class1.getSuperclass());
+ Arrays.stream(class1.getInterfaces()).forEach(this::outType);
+ }
+
+
+
+ private String readPackage(Element type) {
+ if (type.getKind() == ElementKind.PACKAGE) {
+ return type.toString();
+ }
+
+ if (type.getKind() == ElementKind.CLASS
+ || type.getKind() == ElementKind.INTERFACE) {
+ return readPackage(type.getEnclosingElement());
+ }
+
+ return null;
+ }
+
+}
+
diff --git a/apt-test-generator/src/main/java/feign/apttestgenerator/MethodDefinition.java b/apt-test-generator/src/main/java/feign/apttestgenerator/MethodDefinition.java
new file mode 100644
index 000000000..5870f8f19
--- /dev/null
+++ b/apt-test-generator/src/main/java/feign/apttestgenerator/MethodDefinition.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2012-2023 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.apttestgenerator;
+
+import com.google.common.base.CaseFormat;
+import com.google.common.base.Converter;
+import java.util.List;
+
+public class MethodDefinition {
+
+ private static final Converter TO_UPPER_CASE =
+ CaseFormat.LOWER_CAMEL.converterTo(CaseFormat.UPPER_CAMEL);
+ private final String name;
+ private final String uname;
+ private final String returnType;
+ private final boolean isVoid;
+ private final List args;
+
+ public MethodDefinition(String name, String returnType, boolean isVoid,
+ List args) {
+ super();
+ this.name = name;
+ this.uname = TO_UPPER_CASE.convert(name);
+ this.returnType = returnType;
+ this.isVoid = isVoid;
+ this.args = args;
+ }
+
+}
diff --git a/apt-test-generator/src/main/resources/stub.mustache b/apt-test-generator/src/main/resources/stub.mustache
new file mode 100644
index 000000000..015fbbad7
--- /dev/null
+++ b/apt-test-generator/src/main/resources/stub.mustache
@@ -0,0 +1,62 @@
+package {{client.jpackage}};
+
+import java.util.concurrent.atomic.AtomicInteger;
+import feign.Experimental;
+
+public class {{client.className}}Stub
+ implements {{client.fullQualifiedName}} {
+
+ @Experimental
+ public class {{client.className}}Invokations {
+
+{{#each methods as |method|}}
+
+ private final AtomicInteger {{method.name}} = new AtomicInteger(0);
+
+ public int {{method.name}}() {
+ return {{method.name}}.get();
+ }
+
+{{/each}}
+
+ }
+
+ @Experimental
+ public class {{client.className}}Anwsers {
+
+{{#each methods as |method|}}
+ {{#unless method.isVoid}}
+ private {{method.returnType}} {{method.name}}Default;
+ {{/unless}}
+{{/each}}
+
+ }
+
+ public {{client.className}}Invokations invokations;
+ public {{client.className}}Anwsers answers;
+
+ public {{client.className}}Stub() {
+ this.invokations = new {{client.className}}Invokations();
+ this.answers = new {{client.className}}Anwsers();
+ }
+
+{{#each methods as |method|}}
+ {{#unless method.isVoid}}
+ @Experimental
+ public {{client.className}}Stub with{{method.uname}}({{method.returnType}} {{method.name}}) {
+ answers.{{method.name}}Default = {{method.name}};
+ return this;
+ }
+ {{/unless}}
+
+ @Override
+ public {{method.returnType}} {{method.name}}({{#each method.args as |arg|}}{{arg.type}} {{arg.name}}{{#unless @last}},{{/unless}}{{/each}}) {
+ invokations.{{method.name}}.incrementAndGet();
+{{#unless method.isVoid}}
+ return answers.{{method.name}}Default;
+{{/unless}}
+ }
+
+{{/each}}
+
+}
diff --git a/apt-test-generator/src/test/java/example/github/GitHubStub.java b/apt-test-generator/src/test/java/example/github/GitHubStub.java
new file mode 100644
index 000000000..3656447bb
--- /dev/null
+++ b/apt-test-generator/src/test/java/example/github/GitHubStub.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2012-2023 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 example.github;
+
+import java.util.concurrent.atomic.AtomicInteger;
+import feign.Experimental;
+
+public class GitHubStub
+ implements example.github.GitHubExample.GitHub {
+
+ @Experimental
+ public class GitHubInvokations {
+
+ private final AtomicInteger repos = new AtomicInteger(0);
+
+ public int repos() {
+ return repos.get();
+ }
+
+ private final AtomicInteger contributors = new AtomicInteger(0);
+
+ public int contributors() {
+ return contributors.get();
+ }
+
+ private final AtomicInteger createIssue = new AtomicInteger(0);
+
+ public int createIssue() {
+ return createIssue.get();
+ }
+
+ }
+
+ @Experimental
+ public class GitHubAnwsers {
+
+ private java.util.List reposDefault;
+
+ private java.util.List contributorsDefault;
+
+ }
+
+ public GitHubInvokations invokations;
+ public GitHubAnwsers answers;
+
+ public GitHubStub() {
+ this.invokations = new GitHubInvokations();
+ this.answers = new GitHubAnwsers();
+ }
+
+ @Experimental
+ public GitHubStub withRepos(java.util.List repos) {
+ answers.reposDefault = repos;
+ return this;
+ }
+
+ @Override
+ public java.util.List repos(java.lang.String owner) {
+ invokations.repos.incrementAndGet();
+
+ return answers.reposDefault;
+ }
+
+ @Experimental
+ public GitHubStub withContributors(java.util.List contributors) {
+ answers.contributorsDefault = contributors;
+ return this;
+ }
+
+
+ @Override
+ public java.util.List contributors(java.lang.String owner,
+ java.lang.String repo) {
+ invokations.contributors.incrementAndGet();
+
+ return answers.contributorsDefault;
+ }
+
+ @Override
+ public void createIssue(example.github.GitHubExample.GitHub.Issue issue,
+ java.lang.String owner,
+ java.lang.String repo) {
+ invokations.createIssue.incrementAndGet();
+
+ }
+
+}
diff --git a/apt-test-generator/src/test/java/feign/apttestgenerator/GenerateTestStubAPTTest.java b/apt-test-generator/src/test/java/feign/apttestgenerator/GenerateTestStubAPTTest.java
new file mode 100644
index 000000000..5874da451
--- /dev/null
+++ b/apt-test-generator/src/test/java/feign/apttestgenerator/GenerateTestStubAPTTest.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2012-2023 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.apttestgenerator;
+
+import static com.google.testing.compile.CompilationSubject.assertThat;
+import static com.google.testing.compile.Compiler.javac;
+import java.io.File;
+import org.junit.jupiter.api.Test;
+import com.google.testing.compile.Compilation;
+import com.google.testing.compile.JavaFileObjects;
+
+/**
+ * Test for {@link GenerateTestStubAPT}
+ */
+class GenerateTestStubAPTTest {
+
+ private final File main = new File("../example-github/src/main/java/").getAbsoluteFile();
+
+ @Test
+ void test() throws Exception {
+ final Compilation compilation =
+ javac()
+ .withProcessors(new GenerateTestStubAPT())
+ .compile(JavaFileObjects.forResource(
+ new File(main, "example/github/GitHubExample.java")
+ .toURI()
+ .toURL()));
+ assertThat(compilation).succeeded();
+ assertThat(compilation)
+ .generatedSourceFile("example.github.GitHubStub")
+ .hasSourceEquivalentTo(JavaFileObjects.forResource(
+ new File("src/test/java/example/github/GitHubStub.java")
+ .toURI()
+ .toURL()));
+ }
+
+}
diff --git a/benchmark/README.md b/benchmark/README.md
new file mode 100644
index 000000000..43decf782
--- /dev/null
+++ b/benchmark/README.md
@@ -0,0 +1,10 @@
+Feign Benchmarks
+===================
+
+This module includes [JMH](http://openjdk.java.net/projects/code-tools/jmh/) benchmarks for Feign.
+
+=== Building the benchmark
+Install and run `mvn -Dfeign.version=8.1.0` to produce `target/benchmark` pinned to version `8.1.0`
+
+=== Running the benchmark
+Execute `target/benchmark`
diff --git a/benchmark/pom.xml b/benchmark/pom.xml
new file mode 100644
index 000000000..4015239ac
--- /dev/null
+++ b/benchmark/pom.xml
@@ -0,0 +1,176 @@
+
+
+
+ 4.0.0
+
+
+ io.github.openfeign
+ parent
+ 13.5-SNAPSHOT
+
+
+ feign-benchmark
+ Feign Benchmark (JMH)
+
+
+ 1.37
+ 0.5.3
+ 1.3.8
+ 4.1.113.Final
+ ${project.basedir}/..
+
+ true
+
+
+
+
+
+ io.netty
+ netty-bom
+ ${netty.version}
+ pom
+ import
+
+
+
+
+
+
+ ${project.groupId}
+ feign-core
+ ${project.version}
+
+
+ ${project.groupId}
+ feign-okhttp
+ ${project.version}
+
+
+ ${project.groupId}
+ feign-jackson
+ ${project.version}
+
+
+ com.squareup.okhttp3
+ mockwebserver
+
+
+ io.netty
+ netty-handler
+ ${netty.version}
+
+
+ io.netty
+ netty-codec-http
+ ${netty.version}
+
+
+ io.reactivex
+ rxnetty-http
+ ${rx.netty.version}
+
+
+ io.reactivex
+ rxnetty-spectator-http
+ ${rx.netty.version}
+
+
+ io.reactivex
+ rxnetty-common
+ ${rx.netty.version}
+
+
+ io.reactivex
+ rxnetty-tcp
+ ${rx.netty.version}
+
+
+ io.netty
+ netty-buffer
+ compile
+
+
+ io.reactivex
+ rxjava
+ ${rx.java.version}
+
+
+ org.openjdk.jmh
+ jmh-core
+ ${jmh.version}
+
+
+ org.openjdk.jmh
+ jmh-generator-annprocess
+ ${jmh.version}
+ provided
+
+
+ org.slf4j
+ slf4j-api
+ ${slf4j.version}
+
+
+ org.slf4j
+ slf4j-nop
+ ${slf4j.version}
+
+
+
+
+ package
+
+
+ org.apache.maven.plugins
+ maven-shade-plugin
+ 3.6.0
+
+
+ package
+
+ shade
+
+
+
+
+ org.openjdk.jmh.Main
+
+
+ false
+
+
+
+
+
+ org.skife.maven
+ really-executable-jar-maven-plugin
+ 2.1.1
+
+ benchmark
+
+
+
+ package
+
+ really-executable-jar
+
+
+
+
+
+
+
diff --git a/benchmark/src/main/java/feign/benchmark/DecoderIteratorsBenchmark.java b/benchmark/src/main/java/feign/benchmark/DecoderIteratorsBenchmark.java
new file mode 100644
index 000000000..562054ba8
--- /dev/null
+++ b/benchmark/src/main/java/feign/benchmark/DecoderIteratorsBenchmark.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2012-2023 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.benchmark;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import feign.Request;
+import feign.Request.HttpMethod;
+import feign.Response;
+import feign.Util;
+import feign.codec.Decoder;
+import feign.jackson.JacksonDecoder;
+import feign.jackson.JacksonIteratorDecoder;
+import feign.stream.StreamDecoder;
+import org.openjdk.jmh.annotations.*;
+import java.lang.reflect.Type;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Stream;
+
+/**
+ * This test shows up how fast different json array response processing implementations are.
+ */
+@State(Scope.Thread)
+public class DecoderIteratorsBenchmark {
+
+ @Param({"list", "iterator", "stream"})
+ private String api;
+
+ @Param({"10", "100"})
+ private String size;
+
+ private Response response;
+
+ private Decoder decoder;
+ private Type type;
+
+ @Benchmark
+ @Warmup(iterations = 5, time = 1)
+ @Measurement(iterations = 10, time = 1)
+ @Fork(3)
+ @BenchmarkMode(Mode.AverageTime)
+ @OutputTimeUnit(TimeUnit.NANOSECONDS)
+ public void decode() throws Exception {
+ fetch(decoder.decode(response, type));
+ }
+
+ @SuppressWarnings("unchecked")
+ private void fetch(Object o) {
+ Iterator cars;
+
+ if (o instanceof Collection) {
+ cars = ((Collection) o).iterator();
+ } else if (o instanceof Stream) {
+ cars = ((Stream) o).iterator();
+ } else {
+ cars = (Iterator) o;
+ }
+
+ while (cars.hasNext()) {
+ cars.next();
+ }
+ }
+
+ @SuppressWarnings("deprecation")
+ @Setup(Level.Invocation)
+ public void buildResponse() {
+ response = Response.builder()
+ .status(200)
+ .reason("OK")
+ .request(Request.create(HttpMethod.GET, "/", Collections.emptyMap(), null, Util.UTF_8))
+ .headers(Collections.emptyMap())
+ .body(carsJson(Integer.parseInt(size)), Util.UTF_8)
+ .build();
+ }
+
+ @Setup(Level.Trial)
+ public void buildDecoder() {
+ switch (api) {
+ case "list":
+ decoder = new JacksonDecoder();
+ type = new TypeReference>() {}.getType();
+ break;
+ case "iterator":
+ decoder = JacksonIteratorDecoder.create();
+ type = new TypeReference>() {}.getType();
+ break;
+ case "stream":
+ decoder = StreamDecoder.create(JacksonIteratorDecoder.create());
+ type = new TypeReference>() {}.getType();
+ break;
+ default:
+ throw new IllegalStateException("Unknown api: " + api);
+ }
+ }
+
+ private String carsJson(int count) {
+ String car = "{\"name\":\"c4\",\"manufacturer\":\"CitroÃĢn\"}";
+ StringBuilder builder = new StringBuilder("[");
+ builder.append(car);
+ for (int i = 1; i < count; i++) {
+ builder.append(",").append(car);
+ }
+ return builder.append("]").toString();
+ }
+
+ static class Car {
+ public String name;
+ public String manufacturer;
+ }
+}
diff --git a/benchmark/src/main/java/feign/benchmark/FeignTestInterface.java b/benchmark/src/main/java/feign/benchmark/FeignTestInterface.java
new file mode 100644
index 000000000..7a5b54866
--- /dev/null
+++ b/benchmark/src/main/java/feign/benchmark/FeignTestInterface.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2012-2023 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.benchmark;
+
+import java.util.List;
+import feign.Body;
+import feign.Headers;
+import feign.Param;
+import feign.RequestLine;
+import feign.Response;
+
+@Headers("Accept: application/json")
+interface FeignTestInterface {
+
+ @RequestLine("GET /?Action=GetUser&Version=2010-05-08&limit=1")
+ Response query();
+
+ @RequestLine("GET /domains/{domainId}/records?name={name}&type={type}")
+ Response mixedParams(@Param("domainId") int id,
+ @Param("name") String nameFilter,
+ @Param("type") String typeFilter);
+
+ @RequestLine("PATCH /")
+ Response customMethod();
+
+ @RequestLine("PUT /")
+ @Headers("Content-Type: application/json")
+ void bodyParam(List body);
+
+ @RequestLine("POST /")
+ @Body("%7B\"customer_name\": \"{customer_name}\", \"user_name\": \"{user_name}\", \"password\": \"{password}\"%7D")
+ void form(@Param("customer_name") String customer,
+ @Param("user_name") String user,
+ @Param("password") String password);
+
+ @RequestLine("POST /")
+ @Headers({"Happy: sad", "Auth-Token: {authToken}"})
+ void headers(@Param("authToken") String token);
+}
diff --git a/benchmark/src/main/java/feign/benchmark/RealRequestBenchmarks.java b/benchmark/src/main/java/feign/benchmark/RealRequestBenchmarks.java
new file mode 100644
index 000000000..48efe2f28
--- /dev/null
+++ b/benchmark/src/main/java/feign/benchmark/RealRequestBenchmarks.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2012-2023 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.benchmark;
+
+
+
+import feign.Feign;
+import feign.Logger;
+import feign.Logger.Level;
+import feign.Response;
+import feign.Retryer;
+import io.reactivex.netty.protocol.http.server.HttpServer;
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+import io.netty.buffer.ByteBuf;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.TearDown;
+import org.openjdk.jmh.annotations.Warmup;
+
+@Measurement(iterations = 5, time = 1)
+@Warmup(iterations = 10, time = 1)
+@Fork(3)
+@BenchmarkMode(Mode.Throughput)
+@OutputTimeUnit(TimeUnit.SECONDS)
+@State(Scope.Benchmark)
+public class RealRequestBenchmarks {
+
+ private static final int SERVER_PORT = 8765;
+ private HttpServer server;
+ private OkHttpClient client;
+ private FeignTestInterface okFeign;
+ private Request queryRequest;
+
+ @Setup
+ public void setup() {
+
+ server = HttpServer.newServer(SERVER_PORT)
+ .start((request, response) -> null);
+ client = new OkHttpClient();
+ client.retryOnConnectionFailure();
+ okFeign = Feign.builder()
+ .client(new feign.okhttp.OkHttpClient(client))
+ .logLevel(Level.NONE)
+ .logger(new Logger.ErrorLogger())
+ .retryer(new Retryer.Default())
+ .target(FeignTestInterface.class, "http://localhost:" + SERVER_PORT);
+ queryRequest = new Request.Builder()
+ .url("http://localhost:" + SERVER_PORT + "/?Action=GetUser&Version=2010-05-08&limit=1")
+ .build();
+ }
+
+ @TearDown
+ public void tearDown() throws InterruptedException {
+ server.shutdown();
+ }
+
+ /**
+ * How fast can we execute get commands synchronously?
+ */
+ @Benchmark
+ public okhttp3.Response query_baseCaseUsingOkHttp() throws IOException {
+ okhttp3.Response result = client.newCall(queryRequest).execute();
+ result.body().close();
+ return result;
+ }
+
+ /**
+ * How fast can we execute get commands synchronously using Feign?
+ */
+ @Benchmark
+ public boolean query_feignUsingOkHttp() {
+ /* auto close the response */
+ try (Response ignored = okFeign.query()) {
+ return true;
+ }
+ }
+}
diff --git a/benchmark/src/main/java/feign/benchmark/WhatShouldWeCacheBenchmarks.java b/benchmark/src/main/java/feign/benchmark/WhatShouldWeCacheBenchmarks.java
new file mode 100644
index 000000000..fe3490aa3
--- /dev/null
+++ b/benchmark/src/main/java/feign/benchmark/WhatShouldWeCacheBenchmarks.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2012-2023 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.benchmark;
+
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.Warmup;
+import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import feign.Client;
+import feign.Contract;
+import feign.Feign;
+import feign.MethodMetadata;
+import feign.Request;
+import feign.Response;
+import feign.Target.HardCodedTarget;
+
+@Measurement(iterations = 5, time = 1)
+@Warmup(iterations = 10, time = 1)
+@Fork(3)
+@BenchmarkMode(Mode.Throughput)
+@OutputTimeUnit(TimeUnit.SECONDS)
+@State(Scope.Thread)
+public class WhatShouldWeCacheBenchmarks {
+
+ private Contract feignContract;
+ private Contract cachedContact;
+ private Client fakeClient;
+ private Feign cachedFakeFeign;
+ private FeignTestInterface cachedFakeApi;
+
+ @Setup
+ public void setup() {
+ feignContract = new Contract.Default();
+ cachedContact = new Contract() {
+ private final List cached =
+ new Default().parseAndValidateMetadata(FeignTestInterface.class);
+
+ public List parseAndValidateMetadata(Class> declaring) {
+ return cached;
+ }
+ };
+ fakeClient = (request, options) -> {
+ Map> headers = new LinkedHashMap>();
+ return Response.builder()
+ .body((byte[]) null)
+ .status(200)
+ .headers(headers)
+ .reason("ok")
+ .request(request)
+ .build();
+ };
+ cachedFakeFeign = Feign.builder().client(fakeClient).build();
+ cachedFakeApi = cachedFakeFeign.newInstance(
+ new HardCodedTarget(FeignTestInterface.class, "http://localhost"));
+ }
+
+ /**
+ * How fast is parsing an api interface?
+ */
+ @Benchmark
+ public List parseFeignContract() {
+ return feignContract.parseAndValidateMetadata(FeignTestInterface.class);
+ }
+
+ /**
+ * How fast is creating a feign instance for each http request, without considering network?
+ */
+ @Benchmark
+ public Response buildAndQuery_fake() {
+ return Feign.builder().client(fakeClient)
+ .target(FeignTestInterface.class, "http://localhost").query();
+ }
+
+ /**
+ * How fast is creating a feign instance for each http request, without considering network, and
+ * without re-parsing the annotated http api?
+ */
+ @Benchmark
+ public Response buildAndQuery_fake_cachedContract() {
+ return Feign.builder().contract(cachedContact).client(fakeClient)
+ .target(FeignTestInterface.class, "http://localhost").query();
+ }
+
+ /**
+ * How fast re-parsing the annotated http api for each http request, without considering network?
+ */
+ @Benchmark
+ public Response buildAndQuery_fake_cachedFeign() {
+ return cachedFakeFeign.newInstance(
+ new HardCodedTarget(FeignTestInterface.class, "http://localhost"))
+ .query();
+ }
+
+ /**
+ * How fast is our advice to use a cached api for each http request, without considering network?
+ */
+ @Benchmark
+ public Response buildAndQuery_fake_cachedApi() {
+ return cachedFakeApi.query();
+ }
+}
diff --git a/codequality/checkstyle.xml b/codequality/checkstyle.xml
new file mode 100644
index 000000000..0abeadbbf
--- /dev/null
+++ b/codequality/checkstyle.xml
@@ -0,0 +1,203 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/core/pom.xml b/core/pom.xml
new file mode 100644
index 000000000..a17888622
--- /dev/null
+++ b/core/pom.xml
@@ -0,0 +1,133 @@
+
+
+
+ 4.0.0
+
+
+ io.github.openfeign
+ parent
+ 13.5-SNAPSHOT
+
+
+ feign-core
+ Feign Core
+ Feign Core
+
+
+ ${project.basedir}/..
+
+
+
+
+ com.squareup.okhttp3
+ mockwebserver
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter
+ test
+
+
+
+ com.google.code.gson
+ gson
+ test
+
+
+
+ org.springframework
+ spring-context
+ 6.1.13
+ test
+
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+ test
+
+
+
+ org.mockito
+ mockito-core
+ ${mockito.version}
+ test
+
+
+
+
+
+
+ maven-jar-plugin
+
+
+
+ test-jar
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-enforcer-plugin
+
+
+ enforce-banned-dependencies
+
+ enforce
+
+
+
+
+ feign-core should never include any dependencies, this is a design choice to keep core light and extend functionality using modules
+
+ *:*:*:*:*:*
+
+
+ *:*:*:*:test:*
+
+
+
+ true
+
+
+
+
+
+
+
+
+
+ active-on-jdk-11
+
+ 11
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+
+ --illegal-access=deny
+
+
+
+
+
+
+
diff --git a/core/src/main/java/feign/AlwaysEncodeBodyContract.java b/core/src/main/java/feign/AlwaysEncodeBodyContract.java
new file mode 100644
index 000000000..9c7ce282c
--- /dev/null
+++ b/core/src/main/java/feign/AlwaysEncodeBodyContract.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2012-2023 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;
+
+/**
+ * {@link DeclarativeContract} extension that allows user provided custom encoders to define the
+ * request message payload using only the request template and the method parameters, not requiring
+ * a specific and unique body object.
+ *
+ * This type of contract is useful when an application needs a Feign client whose request payload is
+ * defined entirely by a custom Feign encoder regardless of how many parameters are declared at the
+ * client method. In this case, even with no presence of body parameter the provided encoder will
+ * have to know how to define the request payload (for example, based on the method name, method
+ * return type, and other metadata provided by custom annotations, all available via the provided
+ * {@link RequestTemplate} object).
+ *
+ * @author fabiocarvalho777@gmail.com
+ */
+public abstract class AlwaysEncodeBodyContract extends DeclarativeContract {
+}
diff --git a/core/src/main/java/feign/AsyncClient.java b/core/src/main/java/feign/AsyncClient.java
new file mode 100644
index 000000000..2cfe63d83
--- /dev/null
+++ b/core/src/main/java/feign/AsyncClient.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2012-2023 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.util.Optional;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+import feign.Request.Options;
+
+/**
+ * Submits HTTP {@link Request requests} asynchronously, with an optional context.
+ */
+@Experimental
+public interface AsyncClient {
+
+ /**
+ * Executes the request asynchronously. Calling {@link CompletableFuture#cancel(boolean)} on the
+ * result may cause the execution to be cancelled / aborted, but this is not guaranteed.
+ *
+ * @param request safe to replay
+ * @param options options to apply to this request
+ * @param requestContext - the optional context, for example for storing session cookies. The
+ * client should update this appropriately based on the received response before completing
+ * the result.
+ * @return a {@link CompletableFuture} to be completed with the response, or completed
+ * exceptionally otherwise, for example with an {@link java.io.IOException} on a network
+ * error connecting to {@link Request#url()}.
+ */
+ CompletableFuture execute(Request request, Options options, Optional requestContext);
+
+ class Default implements AsyncClient {
+
+ private final Client client;
+ private final ExecutorService executorService;
+
+ public Default(Client client, ExecutorService executorService) {
+ this.client = client;
+ this.executorService = executorService;
+ }
+
+ @Override
+ public CompletableFuture execute(Request request,
+ Options options,
+ Optional requestContext) {
+ final CompletableFuture result = new CompletableFuture<>();
+ final Future> future = executorService.submit(() -> {
+ try {
+ result.complete(client.execute(request, options));
+ } catch (final Exception e) {
+ result.completeExceptionally(e);
+ }
+ });
+ result.whenComplete((response, throwable) -> {
+ if (result.isCancelled()) {
+ future.cancel(true);
+ }
+ });
+ return result;
+ }
+ }
+
+ /**
+ * A synchronous implementation of {@link AsyncClient}
+ *
+ * @param - unused context; synchronous clients handle context internally
+ */
+ class Pseudo implements AsyncClient {
+
+ private final Client client;
+
+ public Pseudo(Client client) {
+ this.client = client;
+ }
+
+ @Override
+ public CompletableFuture execute(Request request,
+ Options options,
+ Optional requestContext) {
+ final CompletableFuture result = new CompletableFuture<>();
+ try {
+ result.complete(client.execute(request, options));
+ } catch (final Exception e) {
+ result.completeExceptionally(e);
+ }
+
+ return result;
+ }
+ }
+}
diff --git a/core/src/main/java/feign/AsyncContextSupplier.java b/core/src/main/java/feign/AsyncContextSupplier.java
new file mode 100644
index 000000000..865244f0c
--- /dev/null
+++ b/core/src/main/java/feign/AsyncContextSupplier.java
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2012-2023 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 interface AsyncContextSupplier {
+
+ C newContext();
+}
diff --git a/core/src/main/java/feign/AsyncFeign.java b/core/src/main/java/feign/AsyncFeign.java
new file mode 100644
index 000000000..8006b05ed
--- /dev/null
+++ b/core/src/main/java/feign/AsyncFeign.java
@@ -0,0 +1,234 @@
+/*
+ * Copyright 2012-2024 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 feign.Logger.Level;
+import feign.Request.Options;
+import feign.Target.HardCodedTarget;
+import feign.codec.Decoder;
+import feign.codec.Encoder;
+import feign.codec.ErrorDecoder;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.function.Supplier;
+
+/**
+ * Enhances {@link Feign} to provide support for asynchronous clients. Context (for example for
+ * session cookies or tokens) is explicit, as calls for the same session may be done across several
+ * threads.
+ *
+ * {@link Retryer} is not supported in this model, as that is a blocking API.
+ * {@link ExceptionPropagationPolicy} is made redundant as {@link RetryableException} is never
+ * thrown.
+ * Alternative approaches to retrying can be handled through {@link AsyncClient clients}.
+ *
+ * Target interface methods must return {@link CompletableFuture} with a non-wildcard type. As the
+ * completion is done by the {@link AsyncClient}, it is important that any subsequent processing on
+ * the thread be short - generally, this should involve notifying some other thread of the work to
+ * be done (for example, creating and submitting a task to an {@link ExecutorService}).
+ */
+@Experimental
+public final class AsyncFeign {
+ public static AsyncBuilder builder() {
+ return new AsyncBuilder<>();
+ }
+
+ /**
+ * @deprecated use {@link #builder()} instead.
+ */
+ @Deprecated()
+ public static AsyncBuilder asyncBuilder() {
+ return builder();
+ }
+
+ private static class LazyInitializedExecutorService {
+
+ private static final ExecutorService instance =
+ Executors.newCachedThreadPool(
+ r -> {
+ final Thread result = new Thread(r);
+ result.setDaemon(true);
+ return result;
+ });
+ }
+
+ public static class AsyncBuilder extends BaseBuilder, AsyncFeign> {
+
+ private AsyncContextSupplier defaultContextSupplier = () -> null;
+ private AsyncClient client = new AsyncClient.Default<>(
+ new Client.Default(null, null), LazyInitializedExecutorService.instance);
+ private MethodInfoResolver methodInfoResolver = MethodInfo::new;
+
+ @Deprecated
+ public AsyncBuilder defaultContextSupplier(Supplier supplier) {
+ this.defaultContextSupplier = supplier::get;
+ return this;
+ }
+
+ public AsyncBuilder client(AsyncClient client) {
+ this.client = client;
+ return this;
+ }
+
+ public AsyncBuilder methodInfoResolver(MethodInfoResolver methodInfoResolver) {
+ this.methodInfoResolver = methodInfoResolver;
+ return this;
+ }
+
+ @Override
+ public AsyncBuilder mapAndDecode(ResponseMapper mapper, Decoder decoder) {
+ return super.mapAndDecode(mapper, decoder);
+ }
+
+ @Override
+ public AsyncBuilder decoder(Decoder decoder) {
+ return super.decoder(decoder);
+ }
+
+ @Override
+ @Deprecated
+ public AsyncBuilder decode404() {
+ return super.decode404();
+ }
+
+ @Override
+ public AsyncBuilder dismiss404() {
+ return super.dismiss404();
+ }
+
+ @Override
+ public AsyncBuilder errorDecoder(ErrorDecoder errorDecoder) {
+ return super.errorDecoder(errorDecoder);
+ }
+
+ @Override
+ public AsyncBuilder doNotCloseAfterDecode() {
+ return super.doNotCloseAfterDecode();
+ }
+
+ @Override
+ public AsyncBuilder decodeVoid() {
+ return super.decodeVoid();
+ }
+
+ public AsyncBuilder defaultContextSupplier(AsyncContextSupplier supplier) {
+ this.defaultContextSupplier = supplier;
+ return this;
+ }
+
+ public T target(Class apiType, String url) {
+ return target(new HardCodedTarget<>(apiType, url));
+ }
+
+ public T target(Class apiType, String url, C context) {
+ return target(new HardCodedTarget<>(apiType, url), context);
+ }
+
+ public T target(Target target) {
+ return build().newInstance(target);
+ }
+
+ public T target(Target target, C context) {
+ return build().newInstance(target, context);
+ }
+
+ @Override
+ public AsyncBuilder logLevel(Level logLevel) {
+ return super.logLevel(logLevel);
+ }
+
+ @Override
+ public AsyncBuilder contract(Contract contract) {
+ return super.contract(contract);
+ }
+
+ @Override
+ public AsyncBuilder logger(Logger logger) {
+ return super.logger(logger);
+ }
+
+ @Override
+ public AsyncBuilder encoder(Encoder encoder) {
+ return super.encoder(encoder);
+ }
+
+ @Override
+ public AsyncBuilder queryMapEncoder(QueryMapEncoder queryMapEncoder) {
+ return super.queryMapEncoder(queryMapEncoder);
+ }
+
+ @Override
+ public AsyncBuilder options(Options options) {
+ return super.options(options);
+ }
+
+ @Override
+ public AsyncBuilder requestInterceptor(RequestInterceptor requestInterceptor) {
+ return super.requestInterceptor(requestInterceptor);
+ }
+
+ @Override
+ public AsyncBuilder requestInterceptors(Iterable requestInterceptors) {
+ return super.requestInterceptors(requestInterceptors);
+ }
+
+ @Override
+ public AsyncBuilder invocationHandlerFactory(InvocationHandlerFactory invocationHandlerFactory) {
+ return super.invocationHandlerFactory(invocationHandlerFactory);
+ }
+
+ @Override
+ public AsyncFeign internalBuild() {
+ AsyncResponseHandler responseHandler =
+ (AsyncResponseHandler) Capability.enrich(
+ new AsyncResponseHandler(
+ logLevel,
+ logger,
+ decoder,
+ errorDecoder,
+ dismiss404,
+ closeAfterDecode, decodeVoid, responseInterceptorChain()),
+ AsyncResponseHandler.class,
+ capabilities);
+
+ final MethodHandler.Factory methodHandlerFactory =
+ new AsynchronousMethodHandler.Factory<>(
+ client, retryer, requestInterceptors,
+ responseHandler, logger, logLevel,
+ propagationPolicy, methodInfoResolver,
+ new RequestTemplateFactoryResolver(encoder, queryMapEncoder),
+ options);
+ final ReflectiveFeign feign =
+ new ReflectiveFeign<>(contract, methodHandlerFactory, invocationHandlerFactory,
+ defaultContextSupplier);
+ return new AsyncFeign<>(feign);
+ }
+ }
+
+ private final ReflectiveFeign feign;
+
+ private AsyncFeign(ReflectiveFeign feign) {
+ this.feign = feign;
+ }
+
+ public T newInstance(Target target) {
+ return feign.newInstance(target);
+ }
+
+ public T newInstance(Target target, C context) {
+ return feign.newInstance(target, context);
+ }
+}
diff --git a/core/src/main/java/feign/AsyncResponseHandler.java b/core/src/main/java/feign/AsyncResponseHandler.java
new file mode 100644
index 000000000..242c83aee
--- /dev/null
+++ b/core/src/main/java/feign/AsyncResponseHandler.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2012-2023 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.Logger.Level;
+import feign.codec.Decoder;
+import feign.codec.ErrorDecoder;
+import java.lang.reflect.Type;
+import java.util.concurrent.CompletableFuture;
+
+/**
+ * The response handler that is used to provide asynchronous support on top of standard response
+ * handling
+ */
+@Experimental
+class AsyncResponseHandler {
+ private final ResponseHandler responseHandler;
+
+ AsyncResponseHandler(Level logLevel, Logger logger, Decoder decoder, ErrorDecoder errorDecoder,
+ boolean dismiss404, boolean closeAfterDecode, boolean decodeVoid,
+ ResponseInterceptor.Chain executionChain) {
+ this.responseHandler = new ResponseHandler(logLevel, logger, decoder, errorDecoder, dismiss404,
+ closeAfterDecode, decodeVoid, executionChain);
+ }
+
+ public CompletableFuture handleResponse(String configKey,
+ Response response,
+ Type returnType,
+ long elapsedTime) {
+ CompletableFuture resultFuture = new CompletableFuture<>();
+ handleResponse(resultFuture, configKey, response, returnType, elapsedTime);
+ return resultFuture;
+ }
+
+ /**
+ * @deprecated use {@link #handleResponse(String, Response, Type, long)} instead.
+ */
+ @Deprecated()
+ void handleResponse(CompletableFuture resultFuture,
+ String configKey,
+ Response response,
+ Type returnType,
+ long elapsedTime) {
+ try {
+ resultFuture.complete(
+ this.responseHandler.handleResponse(configKey, response, returnType, elapsedTime));
+ } catch (Exception e) {
+ resultFuture.completeExceptionally(e);
+ }
+ }
+}
diff --git a/core/src/main/java/feign/AsynchronousMethodHandler.java b/core/src/main/java/feign/AsynchronousMethodHandler.java
new file mode 100644
index 000000000..56e7ebb63
--- /dev/null
+++ b/core/src/main/java/feign/AsynchronousMethodHandler.java
@@ -0,0 +1,269 @@
+/*
+ * Copyright 2012-2024 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 feign.Request.Options;
+import java.io.IOException;
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
+import java.util.concurrent.TimeUnit;
+import java.util.function.BiConsumer;
+import java.util.stream.Stream;
+import static feign.ExceptionPropagationPolicy.UNWRAP;
+import static feign.FeignException.errorExecuting;
+import static feign.Util.checkNotNull;
+
+final class AsynchronousMethodHandler implements MethodHandler {
+
+ private final AsyncClient client;
+ private final C requestContext;
+ private final AsyncResponseHandler asyncResponseHandler;
+ private final MethodInfo methodInfo;
+ private final MethodHandlerConfiguration methodHandlerConfiguration;
+
+ private AsynchronousMethodHandler(MethodHandlerConfiguration methodHandlerConfiguration,
+ AsyncClient client, AsyncResponseHandler asyncResponseHandler, C requestContext,
+ MethodInfo methodInfo) {
+ this.methodHandlerConfiguration =
+ checkNotNull(methodHandlerConfiguration, "methodHandlerConfiguration");
+ this.client = checkNotNull(client, "client for %s", methodHandlerConfiguration.getTarget());
+ this.requestContext = requestContext;
+ this.asyncResponseHandler = asyncResponseHandler;
+ this.methodInfo = methodInfo;
+ }
+
+ @Override
+ public Object invoke(Object[] argv) throws Throwable {
+ RequestTemplate template = methodHandlerConfiguration.getBuildTemplateFromArgs().create(argv);
+ Options options = findOptions(argv);
+ Retryer retryer = this.methodHandlerConfiguration.getRetryer().clone();
+ try {
+ if (methodInfo.isAsyncReturnType()) {
+ return executeAndDecode(template, options, retryer);
+ } else {
+ return executeAndDecode(template, options, retryer).join();
+ }
+ } catch (CompletionException e) {
+ throw e.getCause();
+ }
+ }
+
+ private CompletableFuture executeAndDecode(RequestTemplate template,
+ Options options,
+ Retryer retryer) {
+ CancellableFuture resultFuture = new CancellableFuture<>();
+
+ executeAndDecode(template, options)
+ .whenComplete((response, throwable) -> {
+ if (throwable != null) {
+ if (!resultFuture.isDone() && shouldRetry(retryer, throwable, resultFuture)) {
+ if (methodHandlerConfiguration.getLogLevel() != Logger.Level.NONE) {
+ methodHandlerConfiguration.getLogger().logRetry(
+ methodHandlerConfiguration.getMetadata().configKey(),
+ methodHandlerConfiguration.getLogLevel());
+ }
+
+ resultFuture.setInner(
+ executeAndDecode(template, options, retryer));
+ }
+ } else {
+ resultFuture.complete(response);
+ }
+ });
+
+ return resultFuture;
+ }
+
+ private static class CancellableFuture extends CompletableFuture {
+ private CompletableFuture inner = null;
+
+ public void setInner(CompletableFuture value) {
+ inner = value;
+ inner.whenComplete(pipeTo(this));
+ }
+
+ @Override
+ public boolean cancel(boolean mayInterruptIfRunning) {
+ final boolean result = super.cancel(mayInterruptIfRunning);
+ if (inner != null) {
+ inner.cancel(mayInterruptIfRunning);
+ }
+ return result;
+ }
+
+ private static BiConsumer super T, ? super Throwable> pipeTo(CompletableFuture completableFuture) {
+ return (value, throwable) -> {
+ if (completableFuture.isDone()) {
+ return;
+ }
+
+ if (throwable != null) {
+ completableFuture.completeExceptionally(throwable);
+ } else {
+ completableFuture.complete(value);
+ }
+ };
+ }
+ }
+
+ private boolean shouldRetry(Retryer retryer,
+ Throwable throwable,
+ CompletableFuture resultFuture) {
+ if (throwable instanceof CompletionException) {
+ throwable = throwable.getCause();
+ }
+
+ if (!(throwable instanceof RetryableException)) {
+ resultFuture.completeExceptionally(throwable);
+ return false;
+ }
+
+ RetryableException retryableException = (RetryableException) throwable;
+ try {
+ retryer.continueOrPropagate(retryableException);
+ return true;
+ } catch (RetryableException th) {
+ Throwable cause = th.getCause();
+ if (methodHandlerConfiguration.getPropagationPolicy() == UNWRAP && cause != null) {
+ resultFuture.completeExceptionally(cause);
+ } else {
+ resultFuture.completeExceptionally(th);
+ }
+ return false;
+ }
+ }
+
+ private CompletableFuture executeAndDecode(RequestTemplate template, Options options) {
+ Request request = targetRequest(template);
+
+ if (methodHandlerConfiguration.getLogLevel() != Logger.Level.NONE) {
+ methodHandlerConfiguration.getLogger().logRequest(
+ methodHandlerConfiguration.getMetadata().configKey(),
+ methodHandlerConfiguration.getLogLevel(), request);
+ }
+
+ long start = System.nanoTime();
+ return client.execute(request, options, Optional.ofNullable(requestContext))
+ .thenApply(response ->
+ // TODO: remove in Feign 12
+ ensureRequestIsSet(response, template, request))
+ .exceptionally(throwable -> {
+ CompletionException completionException = throwable instanceof CompletionException
+ ? (CompletionException) throwable
+ : new CompletionException(throwable);
+ if (completionException.getCause() instanceof IOException) {
+ IOException ioException = (IOException) completionException.getCause();
+ if (methodHandlerConfiguration.getLogLevel() != Logger.Level.NONE) {
+ methodHandlerConfiguration.getLogger().logIOException(
+ methodHandlerConfiguration.getMetadata().configKey(),
+ methodHandlerConfiguration.getLogLevel(), ioException,
+ elapsedTime(start));
+ }
+
+ throw errorExecuting(request, ioException);
+ } else {
+ throw completionException;
+ }
+ })
+ .thenCompose(response -> handleResponse(response, elapsedTime(start)));
+ }
+
+ private static Response ensureRequestIsSet(Response response,
+ RequestTemplate template,
+ Request request) {
+ return response.toBuilder()
+ .request(request)
+ .requestTemplate(template)
+ .build();
+ }
+
+ private CompletableFuture handleResponse(Response response, long elapsedTime) {
+ return asyncResponseHandler.handleResponse(
+ methodHandlerConfiguration.getMetadata().configKey(), response,
+ methodInfo.underlyingReturnType(), elapsedTime);
+ }
+
+ private long elapsedTime(long start) {
+ return TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
+ }
+
+ private Request targetRequest(RequestTemplate template) {
+ for (RequestInterceptor interceptor : methodHandlerConfiguration.getRequestInterceptors()) {
+ interceptor.apply(template);
+ }
+ return methodHandlerConfiguration.getTarget().apply(template);
+ }
+
+ private Options findOptions(Object[] argv) {
+ if (argv == null || argv.length == 0) {
+ return this.methodHandlerConfiguration.getOptions();
+ }
+ return Stream.of(argv)
+ .filter(Options.class::isInstance)
+ .map(Options.class::cast)
+ .findFirst()
+ .orElse(this.methodHandlerConfiguration.getOptions());
+ }
+
+ static class Factory implements MethodHandler.Factory