Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
URL-encoded fields use the request's `CollectionFormat`. In multipart requests, numeric and
boolean arrays create repeated parts, and `byte[]` values stay as one binary part. Multipart
`char[]` values are still passed to the delegate encoder (#3607).
* Values supplied through `@HeaderMap` are sent as literals instead of being parsed as URI template
expressions. This bypasses URI expression parsing entirely, including the URI expression length
limit, so a long literal value such as a large JSON payload reaches the wire unchanged.

### Version 13.16

Expand Down
6 changes: 5 additions & 1 deletion core/src/main/java/feign/RequestTemplate.java
Original file line number Diff line number Diff line change
Expand Up @@ -821,8 +821,12 @@ private RequestTemplate appendHeader(String name, Iterable<String> values, boole
// a client can only produce content of one single type, so always override Content-Type and
// only add a single type
this.headers.remove(name);
List<String> contentTypes = Collections.singletonList(values.iterator().next());
this.headers.put(
name, HeaderTemplate.create(name, Collections.singletonList(values.iterator().next())));
name,
literal
? HeaderTemplate.literal(name, contentTypes)
: HeaderTemplate.create(name, contentTypes));
return this;
}
this.headers.compute(
Expand Down
3 changes: 2 additions & 1 deletion core/src/main/java/feign/RequestTemplateFactoryResolver.java
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,8 @@ private RequestTemplate addHeaderMapHeaders(
values.add(currValue == null ? null : currValue.toString());
}

mutable.header(currEntry.getKey(), values);
/* runtime @HeaderMap values are literals, not URI template expressions */
mutable.headerLiteral(currEntry.getKey(), values);
}
return mutable;
}
Expand Down
71 changes: 71 additions & 0 deletions core/src/test/java/feign/FeignTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,77 @@ void headerMap() throws Exception {
entry("Custom-Header", Arrays.asList("fooValue")));
}

@Test
void headerMapWithJsonValueIsTreatedAsLiteral() throws Exception {
server.enqueue(new MockResponse());

TestInterface api = new TestInterfaceBuilder().target("http://localhost:" + server.getPort());

String json = "{\"test\":[\"123:861\"]}";
Map<String, Object> headerMap = new LinkedHashMap<>();
headerMap.put("Custom-Header", json);
api.headerMap(headerMap);

assertThat(server.takeRequest())
.hasHeaders(entry("Custom-Header", Collections.singletonList(json)));
}

@Test
void headerMapAcceptsLargeLiteralJson() throws Exception {
server.enqueue(new MockResponse());

TestInterface api = new TestInterfaceBuilder().target("http://localhost:" + server.getPort());

String value = "{\"payload\":\"" + "a".repeat(10001) + "\"}";
Map<String, Object> headerMap = new LinkedHashMap<>();
headerMap.put("Custom-Header", value);
api.headerMap(headerMap);

assertThat(server.takeRequest())
.hasHeaders(entry("Custom-Header", Collections.singletonList(value)));
}

@Test
void headerMapDoesNotStopAnnotatedHeaderExpansion() throws Exception {
server.enqueue(new MockResponse());

TestInterface api = new TestInterfaceBuilder().target("http://localhost:" + server.getPort());

api.supportComplexHttpHeaders("value");

assertThat(server.takeRequest())
.hasHeaders(entry("Custom", Collections.singletonList("value")));
}

@Test
void headerMapAcceptsLargeQuotedProfileMediaContentType() throws Exception {
server.enqueue(new MockResponse());

TestInterface api = new TestInterfaceBuilder().target("http://localhost:" + server.getPort());

String value = "application/json; note=\"{" + "a".repeat(10001) + "}\"";
Map<String, Object> headerMap = new LinkedHashMap<>();
headerMap.put("Content-Type", value);
api.headerMap(headerMap);

assertThat(server.takeRequest())
.hasHeaders(entry("Content-Type", Collections.singletonList(value)));
}

@Test
void headerMapContentTypeOnlyFirstValueSent() throws Exception {
server.enqueue(new MockResponse());

TestInterface api = new TestInterfaceBuilder().target("http://localhost:" + server.getPort());

Map<String, Object> headerMap = new LinkedHashMap<>();
headerMap.put("Content-Type", Arrays.asList("application/json", "text/plain"));
api.headerMap(headerMap);

assertThat(server.takeRequest())
.hasHeaders(entry("Content-Type", Collections.singletonList("application/json")));
}

@Test
void HeaderMapUserObject() throws Exception {
server.enqueue(new MockResponse());
Expand Down