Skip to content

Commit 28bdf1b

Browse files
seonwooj0810claude
andcommitted
Preserve delegate content type for multipart parameters
`DelegateWriter` encodes a parameter through the delegate `Encoder` (e.g. the Jackson encoder), which sets the correct `Content-Type` (such as `application/json`) on the throwaway `RequestTemplate`. That header was discarded and `SingleParameterWriter` hard-coded `text/plain`, so JSON parts of a multipart request were emitted as `text/plain`. `DelegateWriter` now reads the `Content-Type` produced by the delegate and passes it through to `SingleParameterWriter.writeWithContentType`, falling back to `text/plain; charset=<charset>` when the delegate sets no content type. The previous behaviour for plain single parameters is unchanged. Fixes #2813 Signed-off-by: seonwoo_jung <79202163+seonwooj0810@users.noreply.github.com> Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 46a3830 commit 28bdf1b

3 files changed

Lines changed: 89 additions & 3 deletions

File tree

form/src/main/java/feign/form/multipart/DelegateWriter.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,18 @@ protected void write(Output output, String key, Object value) throws EncodeExcep
4848
delegate.encode(value, value.getClass(), fake);
4949
val bytes = fake.body();
5050
val string = new String(bytes, output.getCharset()).replaceAll("\n", "");
51-
parameterWriter.write(output, key, string);
51+
parameterWriter.writeWithContentType(output, key, string, contentType(fake));
52+
}
53+
54+
private static String contentType(RequestTemplate template) {
55+
val headers = template.headers().get("Content-Type");
56+
if (headers != null) {
57+
for (val header : headers) {
58+
if (header != null && !header.isEmpty()) {
59+
return header;
60+
}
61+
}
62+
}
63+
return null;
5264
}
5365
}

form/src/main/java/feign/form/multipart/SingleParameterWriter.java

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,31 @@ public boolean isApplicable(Object value) {
3434

3535
@Override
3636
protected void write(Output output, String key, Object value) throws EncodeException {
37+
writeWithContentType(output, key, value, null);
38+
}
39+
40+
/**
41+
* Writes a single parameter using the given content type.
42+
*
43+
* @param output output writer.
44+
* @param key name for piece of data.
45+
* @param value piece of data.
46+
* @param contentType the content type of the part. May be {@code null}, in which case {@code
47+
* text/plain} with the output charset is used.
48+
* @throws EncodeException in case of write errors
49+
*/
50+
protected void writeWithContentType(Output output, String key, Object value, String contentType)
51+
throws EncodeException {
52+
val contentTypeHeader =
53+
contentType != null ? contentType : "text/plain; charset=" + output.getCharset().name();
3754
val string =
3855
new StringBuilder()
3956
.append("Content-Disposition: form-data; name=\"")
4057
.append(key)
4158
.append('"')
4259
.append(CRLF)
43-
.append("Content-Type: text/plain; charset=")
44-
.append(output.getCharset().name())
60+
.append("Content-Type: ")
61+
.append(contentTypeHeader)
4562
.append(CRLF)
4663
.append(CRLF)
4764
.append(value.toString())
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
* Copyright © 2012 The Feign Authors (feign@commonhaus.dev)
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package feign.form.multipart;
17+
18+
import static org.assertj.core.api.Assertions.assertThat;
19+
20+
import feign.codec.Encoder;
21+
import java.nio.charset.StandardCharsets;
22+
import org.junit.jupiter.api.Test;
23+
24+
class DelegateWriterTest {
25+
26+
private static final String BOUNDARY = "boundary";
27+
28+
private static final String KEY = "metadata";
29+
30+
@Test
31+
void usesContentTypeFromDelegate() throws Exception {
32+
Encoder delegate =
33+
(object, bodyType, template) -> {
34+
template.header("Content-Type", "application/json");
35+
template.body("{\"hash\":\"somehash\"}");
36+
};
37+
38+
assertThat(write(delegate))
39+
.contains("Content-Type: application/json")
40+
.doesNotContain("Content-Type: text/plain");
41+
}
42+
43+
@Test
44+
void fallsBackToTextPlainWhenDelegateSetsNoContentType() throws Exception {
45+
Encoder delegate = (object, bodyType, template) -> template.body("plain");
46+
47+
assertThat(write(delegate)).contains("Content-Type: text/plain; charset=UTF-8");
48+
}
49+
50+
private static String write(Encoder delegate) throws Exception {
51+
DelegateWriter writer = new DelegateWriter(delegate);
52+
try (Output output = new Output(StandardCharsets.UTF_8)) {
53+
writer.write(output, BOUNDARY, KEY, new Object());
54+
return new String(output.toByteArray(), StandardCharsets.UTF_8);
55+
}
56+
}
57+
}

0 commit comments

Comments
 (0)