diff --git a/CHANGELOG.md b/CHANGELOG.md index d4c531a8b..7788bb51d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ ### Version 8.15 * 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. diff --git a/core/src/main/java/feign/Contract.java b/core/src/main/java/feign/Contract.java index c38e58bc1..1e6fc6853 100644 --- a/core/src/main/java/feign/Contract.java +++ b/core/src/main/java/feign/Contract.java @@ -233,7 +233,7 @@ protected boolean processAnnotationsOnParameter(MethodMetadata data, Annotation[ if (annotationType == Param.class) { String name = ((Param) annotation).value(); checkState(emptyToNull(name) != null, "Param annotation was empty on param %s.", - paramIndex); + paramIndex); nameParam(data, name, paramIndex); if (annotationType == Param.class) { Class expander = ((Param) annotation).expander(); @@ -244,8 +244,8 @@ protected boolean processAnnotationsOnParameter(MethodMetadata data, Annotation[ isHttpAnnotation = true; String varName = '{' + name + '}'; if (data.template().url().indexOf(varName) == -1 && - !searchMapValues(data.template().queries(), varName) && - !searchMapValues(data.template().headers(), varName)) { + !searchMapValuesContainsExact(data.template().queries(), varName) && + !searchMapValuesContainsSubstring(data.template().headers(), varName)) { data.formParams().add(name); } } @@ -253,7 +253,8 @@ protected boolean processAnnotationsOnParameter(MethodMetadata data, Annotation[ return isHttpAnnotation; } - private static boolean searchMapValues(Map> map, V search) { + private static boolean searchMapValuesContainsExact(Map> map, + V search) { Collection> values = map.values(); if (values == null) { return false; @@ -268,6 +269,24 @@ private static boolean searchMapValues(Map> map, V searc return false; } + private static boolean searchMapValuesContainsSubstring(Map> map, + String search) { + Collection> values = map.values(); + if (values == null) { + return false; + } + + for (Collection entry : values) { + for (String value : entry) { + if (value.indexOf(search) != -1) { + return true; + } + } + } + + return false; + } + private static Map> toMap(String[] input) { Map> result = diff --git a/core/src/main/java/feign/Headers.java b/core/src/main/java/feign/Headers.java index f7f413708..c00d9a996 100644 --- a/core/src/main/java/feign/Headers.java +++ b/core/src/main/java/feign/Headers.java @@ -8,7 +8,7 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME; /** - * Expands headers supplied in the {@code value}. Variables are permitted as values.
+ * Expands headers supplied in the {@code value}. Variables to the the right of the colon are expanded.
*
  * @Headers("Content-Type: application/xml")
  * interface SoapApi {
@@ -24,9 +24,13 @@
  * }) void post(@Param("token") String token);
  * ...
  * 
- *
Note: Headers do not overwrite each other. All headers with the same name - * will be included in the request.

Relationship to JAXRS

The following two - * forms are identical.
Feign: + *
Notes: + *
    + *
  • If you'd like curly braces literally in the header, urlencode them first.
  • + *
  • Headers do not overwrite each other. All headers with the same name will be included + * in the request.
  • + *
+ *
Relationship to JAXRS

The following two forms are identical.

Feign: *
  * @RequestLine("POST /")
  * @Headers({
diff --git a/core/src/main/java/feign/RequestTemplate.java b/core/src/main/java/feign/RequestTemplate.java
index 6cf047c39..5ce0d504f 100644
--- a/core/src/main/java/feign/RequestTemplate.java
+++ b/core/src/main/java/feign/RequestTemplate.java
@@ -215,15 +215,8 @@ public RequestTemplate resolve(Map unencoded) {
     for (String field : headers.keySet()) {
       Collection resolvedValues = new ArrayList();
       for (String value : valuesOrEmpty(headers, field)) {
-        String resolved;
-        if (value.indexOf('{') == 0) {
-          resolved = expand(value, unencoded);
-        } else {
-          resolved = value;
-        }
-        if (resolved != null) {
-          resolvedValues.add(resolved);
-        }
+        String resolved = urlDecode(expand(value, encoded));
+        resolvedValues.add(resolved);
       }
       resolvedHeaders.put(field, resolvedValues);
     }
diff --git a/core/src/test/java/feign/DefaultContractTest.java b/core/src/test/java/feign/DefaultContractTest.java
index 4d094c7fe..46f06c378 100644
--- a/core/src/test/java/feign/DefaultContractTest.java
+++ b/core/src/test/java/feign/DefaultContractTest.java
@@ -237,6 +237,19 @@ public void headerParamsParseIntoIndexToName() throws Exception {
 
     assertThat(md.indexToName())
         .containsExactly(entry(0, asList("authToken")));
+    assertThat(md.formParams()).isEmpty();
+  }
+
+  @Test
+  public void headerParamsParseIntoIndexToNameNotAtStart() throws Exception {
+    MethodMetadata md = parseAndValidateMetadata(HeaderParamsNotAtStart.class, "logout", String.class);
+
+    assertThat(md.template())
+        .hasHeaders(entry("Authorization", asList("Bearer {authToken}", "Foo")));
+
+    assertThat(md.indexToName())
+        .containsExactly(entry(0, asList("authToken")));
+    assertThat(md.formParams()).isEmpty();
   }
 
   @Test
@@ -358,6 +371,13 @@ interface HeaderParams {
     void logout(@Param("authToken") String token);
   }
 
+  interface HeaderParamsNotAtStart {
+
+    @RequestLine("POST /")
+    @Headers({"Authorization: Bearer {authToken}", "Authorization: Foo"})
+    void logout(@Param("authToken") String token);
+  }
+
   interface CustomExpander {
 
     @RequestLine("POST /?date={date}")
@@ -528,6 +548,34 @@ public void parameterizedHeaderExpandApi() throws Exception {
         .isEmpty();
   }
 
+  @Test
+  public void parameterizedHeaderNotStartingWithCurlyBraceExpandApi() throws Exception {
+    List
+        md =
+        contract.parseAndValidatateMetadata(
+            ParameterizedHeaderNotStartingWithCurlyBraceExpandApi.class);
+
+    assertThat(md).hasSize(1);
+
+    assertThat(md.get(0).configKey())
+        .isEqualTo("ParameterizedHeaderNotStartingWithCurlyBraceExpandApi#getZone(String,String)");
+    assertThat(md.get(0).returnType())
+        .isEqualTo(String.class);
+    assertThat(md.get(0).template())
+        .hasHeaders(entry("Authorization", asList("Bearer {authHdr}")),
+            entry("Accept", asList("application/json")));
+    // Ensure that the authHdr expansion was properly detected and did not create a formParam
+    assertThat(md.get(0).formParams())
+        .isEmpty();
+  }
+
+  @Headers("Authorization: Bearer {authHdr}")
+  interface ParameterizedHeaderNotStartingWithCurlyBraceExpandApi {
+    @RequestLine("GET /api/{zoneId}")
+    @Headers("Accept: application/json")
+    String getZone(@Param("zoneId") String vhost, @Param("authHdr") String authHdr);
+  }
+
   @Headers("Authorization: {authHdr}")
   interface ParameterizedHeaderBase {
   }
diff --git a/core/src/test/java/feign/RequestTemplateTest.java b/core/src/test/java/feign/RequestTemplateTest.java
index a4a4cbca8..ec606df3a 100644
--- a/core/src/test/java/feign/RequestTemplateTest.java
+++ b/core/src/test/java/feign/RequestTemplateTest.java
@@ -142,6 +142,39 @@ public void resolveTemplateWithHeaderSubstitutions() {
         .hasHeaders(entry("Auth-Token", asList("1234")));
   }
 
+  @Test
+  public void resolveTemplateWithHeaderSubstitutionsNotAtStart() {
+    RequestTemplate template = new RequestTemplate().method("GET")
+        .header("Authorization", "Bearer {token}");
+
+    template.resolve(mapOf("token", "1234"));
+
+    assertThat(template)
+        .hasHeaders(entry("Authorization", asList("Bearer 1234")));
+  }
+
+  @Test
+  public void resolveTemplateWithHeaderWithURLEncodedElements() {
+    RequestTemplate template = new RequestTemplate().method("GET")
+        .header("Encoded", "%7Bvar%7D");
+
+    template.resolve(mapOf("var", "1234"));
+
+    assertThat(template)
+        .hasHeaders(entry("Encoded", asList("{var}")));
+  }
+
+  @Test
+  public void resolveTemplateWithHeaderEmptyResult() {
+    RequestTemplate template = new RequestTemplate().method("GET")
+        .header("Encoded", "{var}");
+
+    template.resolve(mapOf("var", ""));
+
+    assertThat(template)
+        .hasHeaders(entry("Encoded", asList("")));
+  }
+
   @Test
   public void resolveTemplateWithMixedRequestLineParams() throws Exception {
     RequestTemplate template = new RequestTemplate().method("GET")//