Skip to content

Commit 7e25bbb

Browse files
committed
make replacements headers work the same like in the body
1 parent 7464d5e commit 7e25bbb

6 files changed

Lines changed: 116 additions & 17 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
### Version 8.15
22
* Supports PUT without a body parameter
3+
* Supports substitutions in `@Headers` like in `@Body`. (#326)
4+
* **Note:** You might need to URL-encode literal values of `{` or `%` in your existing code.
35

46
### Version 8.14
57
* Add support for RxJava Observable and Single return types via the `HystrixFeign` builder.

core/src/main/java/feign/Contract.java

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ protected boolean processAnnotationsOnParameter(MethodMetadata data, Annotation[
233233
if (annotationType == Param.class) {
234234
String name = ((Param) annotation).value();
235235
checkState(emptyToNull(name) != null, "Param annotation was empty on param %s.",
236-
paramIndex);
236+
paramIndex);
237237
nameParam(data, name, paramIndex);
238238
if (annotationType == Param.class) {
239239
Class<? extends Param.Expander> expander = ((Param) annotation).expander();
@@ -244,16 +244,17 @@ protected boolean processAnnotationsOnParameter(MethodMetadata data, Annotation[
244244
isHttpAnnotation = true;
245245
String varName = '{' + name + '}';
246246
if (data.template().url().indexOf(varName) == -1 &&
247-
!searchMapValues(data.template().queries(), varName) &&
248-
!searchMapValues(data.template().headers(), varName)) {
247+
!searchMapValuesContainsExact(data.template().queries(), varName) &&
248+
!searchMapValuesContainsSubstring(data.template().headers(), varName)) {
249249
data.formParams().add(name);
250250
}
251251
}
252252
}
253253
return isHttpAnnotation;
254254
}
255255

256-
private static <K, V> boolean searchMapValues(Map<K, Collection<V>> map, V search) {
256+
private static <K, V> boolean searchMapValuesContainsExact(Map<K, Collection<V>> map,
257+
V search) {
257258
Collection<Collection<V>> values = map.values();
258259
if (values == null) {
259260
return false;
@@ -268,6 +269,24 @@ private static <K, V> boolean searchMapValues(Map<K, Collection<V>> map, V searc
268269
return false;
269270
}
270271

272+
private static <K, V> boolean searchMapValuesContainsSubstring(Map<K, Collection<String>> map,
273+
String search) {
274+
Collection<Collection<String>> values = map.values();
275+
if (values == null) {
276+
return false;
277+
}
278+
279+
for (Collection<String> entry : values) {
280+
for (String value : entry) {
281+
if (value.indexOf(search) != -1) {
282+
return true;
283+
}
284+
}
285+
}
286+
287+
return false;
288+
}
289+
271290
private static Map<String, Collection<String>> toMap(String[] input) {
272291
Map<String, Collection<String>>
273292
result =

core/src/main/java/feign/Headers.java

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import static java.lang.annotation.RetentionPolicy.RUNTIME;
99

1010
/**
11-
* Expands headers supplied in the {@code value}. Variables are permitted as values. <br>
11+
* Expands headers supplied in the {@code value}. Variables to the the right of the colon are expanded. <br>
1212
* <pre>
1313
* &#64;Headers("Content-Type: application/xml")
1414
* interface SoapApi {
@@ -24,9 +24,13 @@
2424
* }) void post(&#64;Param("token") String token);
2525
* ...
2626
* </pre>
27-
* <br> <strong>Note:</strong> Headers do not overwrite each other. All headers with the same name
28-
* will be included in the request. <br><br><b>Relationship to JAXRS</b><br> <br> The following two
29-
* forms are identical. <br> Feign:
27+
* <br> <strong>Notes:</strong>
28+
* <ul>
29+
* <li>If you'd like curly braces literally in the header, urlencode them first.</li>
30+
* <li>Headers do not overwrite each other. All headers with the same name will be included
31+
* in the request.</li>
32+
* </ul>
33+
* <br><b>Relationship to JAXRS</b><br> <br> The following two forms are identical. <br><br> Feign:
3034
* <pre>
3135
* &#64;RequestLine("POST /")
3236
* &#64;Headers({

core/src/main/java/feign/RequestTemplate.java

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -215,15 +215,8 @@ public RequestTemplate resolve(Map<String, ?> unencoded) {
215215
for (String field : headers.keySet()) {
216216
Collection<String> resolvedValues = new ArrayList<String>();
217217
for (String value : valuesOrEmpty(headers, field)) {
218-
String resolved;
219-
if (value.indexOf('{') == 0) {
220-
resolved = expand(value, unencoded);
221-
} else {
222-
resolved = value;
223-
}
224-
if (resolved != null) {
225-
resolvedValues.add(resolved);
226-
}
218+
String resolved = urlDecode(expand(value, encoded));
219+
resolvedValues.add(resolved);
227220
}
228221
resolvedHeaders.put(field, resolvedValues);
229222
}

core/src/test/java/feign/DefaultContractTest.java

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,19 @@ public void headerParamsParseIntoIndexToName() throws Exception {
237237

238238
assertThat(md.indexToName())
239239
.containsExactly(entry(0, asList("authToken")));
240+
assertThat(md.formParams()).isEmpty();
241+
}
242+
243+
@Test
244+
public void headerParamsParseIntoIndexToNameNotAtStart() throws Exception {
245+
MethodMetadata md = parseAndValidateMetadata(HeaderParamsNotAtStart.class, "logout", String.class);
246+
247+
assertThat(md.template())
248+
.hasHeaders(entry("Authorization", asList("Bearer {authToken}", "Foo")));
249+
250+
assertThat(md.indexToName())
251+
.containsExactly(entry(0, asList("authToken")));
252+
assertThat(md.formParams()).isEmpty();
240253
}
241254

242255
@Test
@@ -358,6 +371,13 @@ interface HeaderParams {
358371
void logout(@Param("authToken") String token);
359372
}
360373

374+
interface HeaderParamsNotAtStart {
375+
376+
@RequestLine("POST /")
377+
@Headers({"Authorization: Bearer {authToken}", "Authorization: Foo"})
378+
void logout(@Param("authToken") String token);
379+
}
380+
361381
interface CustomExpander {
362382

363383
@RequestLine("POST /?date={date}")
@@ -528,6 +548,34 @@ public void parameterizedHeaderExpandApi() throws Exception {
528548
.isEmpty();
529549
}
530550

551+
@Test
552+
public void parameterizedHeaderNotStartingWithCurlyBraceExpandApi() throws Exception {
553+
List<MethodMetadata>
554+
md =
555+
contract.parseAndValidatateMetadata(
556+
ParameterizedHeaderNotStartingWithCurlyBraceExpandApi.class);
557+
558+
assertThat(md).hasSize(1);
559+
560+
assertThat(md.get(0).configKey())
561+
.isEqualTo("ParameterizedHeaderNotStartingWithCurlyBraceExpandApi#getZone(String,String)");
562+
assertThat(md.get(0).returnType())
563+
.isEqualTo(String.class);
564+
assertThat(md.get(0).template())
565+
.hasHeaders(entry("Authorization", asList("Bearer {authHdr}")),
566+
entry("Accept", asList("application/json")));
567+
// Ensure that the authHdr expansion was properly detected and did not create a formParam
568+
assertThat(md.get(0).formParams())
569+
.isEmpty();
570+
}
571+
572+
@Headers("Authorization: Bearer {authHdr}")
573+
interface ParameterizedHeaderNotStartingWithCurlyBraceExpandApi {
574+
@RequestLine("GET /api/{zoneId}")
575+
@Headers("Accept: application/json")
576+
String getZone(@Param("zoneId") String vhost, @Param("authHdr") String authHdr);
577+
}
578+
531579
@Headers("Authorization: {authHdr}")
532580
interface ParameterizedHeaderBase {
533581
}

core/src/test/java/feign/RequestTemplateTest.java

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,39 @@ public void resolveTemplateWithHeaderSubstitutions() {
142142
.hasHeaders(entry("Auth-Token", asList("1234")));
143143
}
144144

145+
@Test
146+
public void resolveTemplateWithHeaderSubstitutionsNotAtStart() {
147+
RequestTemplate template = new RequestTemplate().method("GET")
148+
.header("Authorization", "Bearer {token}");
149+
150+
template.resolve(mapOf("token", "1234"));
151+
152+
assertThat(template)
153+
.hasHeaders(entry("Authorization", asList("Bearer 1234")));
154+
}
155+
156+
@Test
157+
public void resolveTemplateWithHeaderWithURLEncodedElements() {
158+
RequestTemplate template = new RequestTemplate().method("GET")
159+
.header("Encoded", "%7Bvar%7D");
160+
161+
template.resolve(mapOf("var", "1234"));
162+
163+
assertThat(template)
164+
.hasHeaders(entry("Encoded", asList("{var}")));
165+
}
166+
167+
@Test
168+
public void resolveTemplateWithHeaderEmptyResult() {
169+
RequestTemplate template = new RequestTemplate().method("GET")
170+
.header("Encoded", "{var}");
171+
172+
template.resolve(mapOf("var", ""));
173+
174+
assertThat(template)
175+
.hasHeaders(entry("Encoded", asList("")));
176+
}
177+
145178
@Test
146179
public void resolveTemplateWithMixedRequestLineParams() throws Exception {
147180
RequestTemplate template = new RequestTemplate().method("GET")//

0 commit comments

Comments
 (0)