Summary
When encoding a form body with Content-Type: application/x-www-form-urlencoded,
UrlencodedFormContentProcessor always serialises Collection and array values as repeated
key-value pairs (EXPLODED format). It would be useful if it instead honoured the CollectionFormat
declared on the @RequestLine annotation, consistent with how query-parameter collections are
already handled in RequestTemplate.
Motivation
feign-core exposes CollectionFormat via @RequestLine and stores the chosen format on
RequestTemplate. This works correctly for query parameters, but the setting is silently ignored
when the same collection values appear in a URL-encoded form body.
For example, given:
@RequestLine(value = "POST /send", collectionFormat = CollectionFormat.CSV)
@Headers("Content-Type: application/x-www-form-urlencoded")
void send(@Param("tags") List<String> tags);
calling api.send(List.of("one", "two")) currently produces:
tags=one&tags=two β always EXPLODED, regardless of the CollectionFormat setting
Proposed behaviour
The processor should honour the CollectionFormat present on the RequestTemplate
(populated from @RequestLine), producing:
CollectionFormat |
Expected body |
EXPLODED (default) |
tags=one&tags=two |
CSV |
tags=one%2Ctwo |
SSV |
tags=one+two |
TSV |
tags=one%09two |
PIPES |
tags=one%7Ctwo |
Proposed implementation
- Thread
template.collectionFormat() from process down into createKeyValuePair.
- Unify array and
Collection handling into a single private overload that accepts a Stream<?>,
and delegate serialisation to collectionFormat.join():
@Override
public void process(RequestTemplate template, Charset charset, Map<String, Object> data) {
// ...
bodyData.append(createKeyValuePair(template.collectionFormat(), entry, charset));
// ...
}
private CharSequence createKeyValuePair(
CollectionFormat collectionFormat, Entry<String, Object> entry, Charset charset) {
String encodedKey = encode(entry.getKey(), charset);
Object value = entry.getValue();
if (value == null) {
return encodedKey;
} else if (value.getClass().isArray()) {
return createKeyValuePair(
collectionFormat, encodedKey, Arrays.stream((Object[]) value), charset);
} else if (value instanceof Collection) {
return createKeyValuePair(
collectionFormat, encodedKey, ((Collection<?>) value).stream(), charset);
}
return new StringBuilder()
.append(encodedKey)
.append(EQUAL_SIGN)
.append(encode(value, charset))
.toString();
}
private CharSequence createKeyValuePair(
CollectionFormat collectionFormat, String key, Stream<?> values, Charset charset) {
val stringValues =
values.filter(Objects::nonNull).map(Object::toString).collect(Collectors.toList());
return collectionFormat.join(key, stringValues, charset);
}
Both array and Collection values are funnelled through the same Stream<?> overload, which
converts non-null elements to strings and delegates entirely to CollectionFormat.join().
Compatibility
- No breaking change: the default
CollectionFormat is EXPLODED, so existing behaviour is
preserved for any code that does not explicitly set a CollectionFormat.
Out of scope
A similar improvement could be considered for MultipartFormContentProcessor. However, unlike
URL-encoded form data, multipart bodies do not URL-encode field values; CollectionFormat.join()
encodes values via UriUtils.encode() by default, so naively applying the same change to
MultipartFormContentProcessor would corrupt the multipart output. That case deserves a separate,
more careful investigation and is intentionally left out of this issue.
Related
feign.CollectionFormat β existing enum with join() helper
RequestTemplate#collectionFormat() β the configured format available at encode time
@RequestLine#collectionFormat() β the annotation attribute users set to choose a format
- Similar behaviour in
RequestTemplate for query-parameter collections (already correct)
Summary
When encoding a form body with
Content-Type: application/x-www-form-urlencoded,UrlencodedFormContentProcessoralways serialisesCollectionand array values as repeatedkey-value pairs (
EXPLODEDformat). It would be useful if it instead honoured theCollectionFormatdeclared on the
@RequestLineannotation, consistent with how query-parameter collections arealready handled in
RequestTemplate.Motivation
feign-coreexposesCollectionFormatvia@RequestLineand stores the chosen format onRequestTemplate. This works correctly for query parameters, but the setting is silently ignoredwhen the same collection values appear in a URL-encoded form body.
For example, given:
calling
api.send(List.of("one", "two"))currently produces:Proposed behaviour
The processor should honour the
CollectionFormatpresent on theRequestTemplate(populated from
@RequestLine), producing:CollectionFormatEXPLODED(default)tags=one&tags=twoCSVtags=one%2CtwoSSVtags=one+twoTSVtags=one%09twoPIPEStags=one%7CtwoProposed implementation
template.collectionFormat()fromprocessdown intocreateKeyValuePair.Collectionhandling into a single private overload that accepts aStream<?>,and delegate serialisation to
collectionFormat.join():Both array and
Collectionvalues are funnelled through the sameStream<?>overload, whichconverts non-null elements to strings and delegates entirely to
CollectionFormat.join().Compatibility
CollectionFormatisEXPLODED, so existing behaviour ispreserved for any code that does not explicitly set a
CollectionFormat.Out of scope
A similar improvement could be considered for
MultipartFormContentProcessor. However, unlikeURL-encoded form data, multipart bodies do not URL-encode field values;
CollectionFormat.join()encodes values via
UriUtils.encode()by default, so naively applying the same change toMultipartFormContentProcessorwould corrupt the multipart output. That case deserves a separate,more careful investigation and is intentionally left out of this issue.
Related
feign.CollectionFormatβ existing enum withjoin()helperRequestTemplate#collectionFormat()β the configured format available at encode time@RequestLine#collectionFormat()β the annotation attribute users set to choose a formatRequestTemplatefor query-parameter collections (already correct)