Skip to content

Commit 4ba51e5

Browse files
authored
[GH-1464] Add appendHeader that supports Literals (#1781)
This change adds a new `appendHeader` internal method to `RequestTemplate` allowing for already resolved headers to be added to the resolved `RequestTemplate` preventing duplicate expression processing by using another new method `HeaderTemplate.literal` and `HeaderTemplate.appendLiteral` respectively. I chose this route as it isolates the change to be applied only after the original `HeaderTemplate` has been resolved. While it does expose new public `HeaderTemplate` APIs, I feel that is an OK trade off, allowing a new escape-hatch for situations where URI template processing is not acceptable for Header values.
1 parent ee63634 commit 4ba51e5

File tree

3 files changed

+104
-11
lines changed

3 files changed

+104
-11
lines changed

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

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,8 @@ public RequestTemplate resolve(Map<String, ?> variables) {
232232
String header = headerTemplate.expand(variables);
233233
if (!header.isEmpty()) {
234234
/* append the header as a new literal as the value has already been expanded. */
235-
resolved.header(headerTemplate.getName(), header);
235+
resolved.appendHeader(
236+
headerTemplate.getName(), Collections.singletonList(header), true);
236237
}
237238
}
238239
}
@@ -723,13 +724,26 @@ public RequestTemplate removeHeader(String name) {
723724
}
724725

725726
/**
726-
* Create a Header Template.
727+
* Append the Header. Will create a new Header if it doesn't already exist. Treats all values as
728+
* potentially expressions.
727729
*
728730
* @param name of the header
729731
* @param values for the header, may be expressions.
730732
* @return a RequestTemplate for chaining.
731733
*/
732734
private RequestTemplate appendHeader(String name, Iterable<String> values) {
735+
return this.appendHeader(name, values, false);
736+
}
737+
738+
/**
739+
* Append the Header. Will create a new Header if it doesn't already exist.
740+
*
741+
* @param name of the header
742+
* @param values for the header, may be expressions.
743+
* @param literal indicator, to treat the values as literals and not expressions
744+
* @return a RequestTemplate for chaining.
745+
*/
746+
private RequestTemplate appendHeader(String name, Iterable<String> values, boolean literal) {
733747
if (!values.iterator().hasNext()) {
734748
/* empty value, clear the existing values */
735749
this.headers.remove(name);
@@ -745,9 +759,17 @@ private RequestTemplate appendHeader(String name, Iterable<String> values) {
745759
}
746760
this.headers.compute(name, (headerName, headerTemplate) -> {
747761
if (headerTemplate == null) {
748-
return HeaderTemplate.create(headerName, values);
762+
if (literal) {
763+
return HeaderTemplate.literal(headerName, values);
764+
} else {
765+
return HeaderTemplate.create(headerName, values);
766+
}
749767
} else {
750-
return HeaderTemplate.append(headerTemplate, values);
768+
if (literal) {
769+
return HeaderTemplate.appendLiteral(headerTemplate, values);
770+
} else {
771+
return HeaderTemplate.append(headerTemplate, values);
772+
}
751773
}
752774
});
753775
return this;

core/src/main/java/feign/template/HeaderTemplate.java

Lines changed: 58 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,18 @@ public static HeaderTemplate create(String name, Iterable<String> values) {
4848
return new HeaderTemplate(name, values, Util.UTF_8);
4949
}
5050

51+
public static HeaderTemplate literal(String name, Iterable<String> values) {
52+
if (name == null || name.isEmpty()) {
53+
throw new IllegalArgumentException("name is required.");
54+
}
55+
56+
if (values == null) {
57+
throw new IllegalArgumentException("values are required");
58+
}
59+
60+
return new HeaderTemplate(name, values, Util.UTF_8, true);
61+
}
62+
5163
/**
5264
* Append values to a Header Template.
5365
*
@@ -63,6 +75,22 @@ public static HeaderTemplate append(HeaderTemplate headerTemplate, Iterable<Stri
6375
return create(headerTemplate.getName(), headerValues);
6476
}
6577

78+
/**
79+
* Append values to a Header Template, as literals
80+
*
81+
* @param headerTemplate to append to.
82+
* @param values to append.
83+
* @return a new Header Template with the values added.
84+
*/
85+
public static HeaderTemplate appendLiteral(HeaderTemplate headerTemplate,
86+
Iterable<String> values) {
87+
LinkedHashSet<String> headerValues = new LinkedHashSet<>(headerTemplate.getValues());
88+
headerValues.addAll(StreamSupport.stream(values.spliterator(), false)
89+
.filter(Util::isNotBlank)
90+
.collect(Collectors.toCollection(LinkedHashSet::new)));
91+
return literal(headerTemplate.getName(), headerValues);
92+
}
93+
6694
/**
6795
* Create a new Header Template.
6896
*
@@ -71,6 +99,18 @@ public static HeaderTemplate append(HeaderTemplate headerTemplate, Iterable<Stri
7199
* @param charset to use when encoding the values.
72100
*/
73101
private HeaderTemplate(String name, Iterable<String> values, Charset charset) {
102+
this(name, values, charset, false);
103+
}
104+
105+
/**
106+
* Create a new Header Template.
107+
*
108+
* @param name of the header
109+
* @param values of the header
110+
* @param charset for the header
111+
* @param literal indicator. Will treat all values as literals instead of possible expressions.
112+
*/
113+
private HeaderTemplate(String name, Iterable<String> values, Charset charset, boolean literal) {
74114
this.name = name;
75115

76116
for (String value : values) {
@@ -79,14 +119,25 @@ private HeaderTemplate(String name, Iterable<String> values, Charset charset) {
79119
continue;
80120
}
81121

82-
this.values.add(
83-
new Template(
84-
value,
85-
ExpansionOptions.REQUIRED,
86-
EncodingOptions.NOT_REQUIRED,
87-
false,
88-
charset));
122+
if (literal) {
123+
this.values.add(
124+
new Template(
125+
ExpansionOptions.ALLOW_UNRESOLVED,
126+
EncodingOptions.NOT_REQUIRED,
127+
false,
128+
charset,
129+
Collections.singletonList(Literal.create(value))));
130+
} else {
131+
this.values.add(
132+
new Template(
133+
value,
134+
ExpansionOptions.REQUIRED,
135+
EncodingOptions.NOT_REQUIRED,
136+
false,
137+
charset));
138+
}
89139
}
140+
90141
}
91142

92143
public Collection<String> getValues() {

core/src/test/java/feign/FeignTest.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import okhttp3.mockwebserver.MockWebServer;
2525
import okhttp3.mockwebserver.SocketPolicy;
2626
import okio.Buffer;
27+
import org.assertj.core.data.MapEntry;
2728
import org.assertj.core.util.Maps;
2829
import org.junit.Rule;
2930
import org.junit.Test;
@@ -986,6 +987,21 @@ public void matrixParametersAlsoSupportMaps() throws Exception {
986987

987988
}
988989

990+
@Test
991+
public void supportComplexHeaders() throws Exception {
992+
TestInterface api = new TestInterfaceBuilder()
993+
.target("http://localhost:" + server.getPort());
994+
995+
server.enqueue(new MockResponse());
996+
/* demonstrate that a complex header, like a JSON document, is supported */
997+
String complex = "{ \"object\": \"value\", \"second\": \"string\" }";
998+
999+
api.supportComplexHttpHeaders(complex);
1000+
assertThat(server.takeRequest())
1001+
.hasHeaders(MapEntry.entry("custom", Collections.singletonList(complex)))
1002+
.hasPath("/settings");
1003+
}
1004+
9891005
interface TestInterface {
9901006

9911007
@RequestLine("POST /")
@@ -1077,6 +1093,10 @@ void queryMapWithQueryParams(@Param("name") String name,
10771093
@RequestLine("GET /settings{;props}")
10781094
void matrixParametersWithMap(@Param("props") Map<String, Object> owners);
10791095

1096+
@RequestLine("GET /settings")
1097+
@Headers("Custom: {complex}")
1098+
void supportComplexHttpHeaders(@Param("complex") String complex);
1099+
10801100
class DateToMillis implements Param.Expander {
10811101

10821102
@Override

0 commit comments

Comments
 (0)