Skip to content

πŸ’‘ Feature Request: Honour CollectionFormat in UrlencodedFormContentProcessorΒ #3283

@yvasyliev

Description

@yvasyliev

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

  1. Thread template.collectionFormat() from process down into createKeyValuePair.
  2. 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)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions