Skip to content

Commit c8dcd4d

Browse files
Fix GH-201: Multiple @RequestPart not Working
Summary of changes follows: - Delegate ALL requestBody encoding where Content-Type is multipart/form-data to the SpringFormEncoder. - Introduce RequestPartParameterProcessor to deal with @RequestPart annotations, adds parameters to MethodMetadata.formParams(). - Wrap HttpMessageConversionException in EncodeException. - Add tests to verify expected behaviour. However there still exists a gap in functionality where any user defined pojo will be serialized as a Map<String,String>, as there is currently no way for SpringFormEncoder to know about the Content-Type of the individual parts beyond MultipartFile, boxed primitive types (which are treated as text/plain) and other built-in types inherited from FormEncoder.
1 parent 422dda0 commit c8dcd4d

File tree

6 files changed

+257
-15
lines changed

6 files changed

+257
-15
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
* Copyright 2013-2019 the original author or authors.
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+
* https://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+
17+
package org.springframework.cloud.openfeign.annotation;
18+
19+
import java.lang.annotation.Annotation;
20+
import java.lang.reflect.Method;
21+
import java.util.Collection;
22+
23+
import feign.MethodMetadata;
24+
25+
import org.springframework.cloud.openfeign.AnnotatedParameterProcessor;
26+
import org.springframework.web.bind.annotation.RequestPart;
27+
28+
import static feign.Util.checkState;
29+
import static feign.Util.emptyToNull;
30+
31+
/**
32+
* {@link RequestPart} parameter processor.
33+
*
34+
* @author Aaron Whiteside
35+
* @see AnnotatedParameterProcessor
36+
*/
37+
public class RequestPartParameterProcessor implements AnnotatedParameterProcessor {
38+
39+
private static final Class<RequestPart> ANNOTATION = RequestPart.class;
40+
41+
@Override
42+
public Class<? extends Annotation> getAnnotationType() {
43+
return ANNOTATION;
44+
}
45+
46+
@Override
47+
public boolean processArgument(AnnotatedParameterContext context,
48+
Annotation annotation, Method method) {
49+
int parameterIndex = context.getParameterIndex();
50+
MethodMetadata data = context.getMethodMetadata();
51+
52+
String name = ANNOTATION.cast(annotation).value();
53+
checkState(emptyToNull(name) != null,
54+
"RequestPart.value() was empty on parameter %s", parameterIndex);
55+
context.setParameterName(name);
56+
57+
data.formParams().add(name);
58+
Collection<String> names = context.setTemplateParameter(name,
59+
data.indexToName().get(parameterIndex));
60+
data.indexToName().put(parameterIndex, names);
61+
return true;
62+
}
63+
64+
}

spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/support/SpringEncoder.java

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
import org.springframework.http.MediaType;
4242
import org.springframework.http.converter.ByteArrayHttpMessageConverter;
4343
import org.springframework.http.converter.GenericHttpMessageConverter;
44+
import org.springframework.http.converter.HttpMessageConversionException;
4445
import org.springframework.http.converter.HttpMessageConverter;
4546
import org.springframework.http.converter.protobuf.ProtobufHttpMessageConverter;
4647
import org.springframework.web.multipart.MultipartFile;
@@ -52,14 +53,15 @@
5253
* @author Spencer Gibb
5354
* @author Scien Jus
5455
* @author Ahmad Mozafarnia
56+
* @author Aaron Whiteside
5557
*/
5658
public class SpringEncoder implements Encoder {
5759

5860
private static final Log log = LogFactory.getLog(SpringEncoder.class);
5961

6062
private final SpringFormEncoder springFormEncoder = new SpringFormEncoder();
6163

62-
private ObjectFactory<HttpMessageConverters> messageConverters;
64+
private final ObjectFactory<HttpMessageConverters> messageConverters;
6365

6466
public SpringEncoder(ObjectFactory<HttpMessageConverters> messageConverters) {
6567
this.messageConverters = messageConverters;
@@ -79,16 +81,15 @@ public void encode(Object requestBody, Type bodyType, RequestTemplate request)
7981
requestContentType = MediaType.valueOf(type);
8082
}
8183

82-
if (bodyType != null && bodyType.equals(MultipartFile.class)) {
83-
if (Objects.equals(requestContentType, MediaType.MULTIPART_FORM_DATA)) {
84-
this.springFormEncoder.encode(requestBody, bodyType, request);
85-
return;
86-
}
87-
else {
88-
String message = "Content-Type \"" + MediaType.MULTIPART_FORM_DATA
89-
+ "\" not set for request body of type "
90-
+ requestBody.getClass().getSimpleName();
91-
throw new EncodeException(message);
84+
if (Objects.equals(requestContentType, MediaType.MULTIPART_FORM_DATA)) {
85+
this.springFormEncoder.encode(requestBody, bodyType, request);
86+
return;
87+
}
88+
else {
89+
if (bodyType.equals(MultipartFile.class)) {
90+
log.warn(
91+
"For MultipartFile to be handled correctly, the 'consumes' parameter of @RequestMapping "
92+
+ "should be specified as MediaType.MULTIPART_FORM_DATA_VALUE");
9293
}
9394
}
9495

@@ -106,7 +107,7 @@ public void encode(Object requestBody, Type bodyType, RequestTemplate request)
106107
messageConverter, request);
107108
}
108109
}
109-
catch (IOException ex) {
110+
catch (IOException | HttpMessageConversionException ex) {
110111
throw new EncodeException("Error converting request body", ex);
111112
}
112113
if (outputMessage != null) {

spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/support/SpringMvcContract.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
import org.springframework.cloud.openfeign.annotation.QueryMapParameterProcessor;
4242
import org.springframework.cloud.openfeign.annotation.RequestHeaderParameterProcessor;
4343
import org.springframework.cloud.openfeign.annotation.RequestParamParameterProcessor;
44+
import org.springframework.cloud.openfeign.annotation.RequestPartParameterProcessor;
4445
import org.springframework.context.ConfigurableApplicationContext;
4546
import org.springframework.context.ResourceLoaderAware;
4647
import org.springframework.core.DefaultParameterNameDiscoverer;
@@ -69,6 +70,7 @@
6970
* @author Halvdan Hoem Grelland
7071
* @author Aram Peres
7172
* @author Olga Maciaszek-Sharma
73+
* @author Aaron Whiteside
7274
*/
7375
public class SpringMvcContract extends Contract.BaseContract
7476
implements ResourceLoaderAware {
@@ -357,6 +359,7 @@ private List<AnnotatedParameterProcessor> getDefaultAnnotatedArgumentsProcessors
357359
annotatedArgumentResolvers.add(new RequestParamParameterProcessor());
358360
annotatedArgumentResolvers.add(new RequestHeaderParameterProcessor());
359361
annotatedArgumentResolvers.add(new QueryMapParameterProcessor());
362+
annotatedArgumentResolvers.add(new RequestPartParameterProcessor());
360363

361364
return annotatedArgumentResolvers;
362365
}

spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/support/SpringEncoderTests.java

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -156,8 +156,6 @@ public void testMultipartFile1() {
156156
MultipartFile multipartFile = new MockMultipartFile("test_multipart_file",
157157
"hi".getBytes());
158158
encoder.encode(multipartFile, MultipartFile.class, request);
159-
160-
assertThat(request.requestCharset()).as("request charset is not null").isNull();
161159
}
162160

163161
// gh-105, gh-107

spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/support/SpringMvcContractTests.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@
5454
import org.springframework.web.bind.annotation.RequestMapping;
5555
import org.springframework.web.bind.annotation.RequestMethod;
5656
import org.springframework.web.bind.annotation.RequestParam;
57+
import org.springframework.web.bind.annotation.RequestPart;
58+
import org.springframework.web.multipart.MultipartFile;
5759

5860
import static com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility.ANY;
5961
import static com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility.NONE;
@@ -66,6 +68,7 @@
6668
* @author chadjaros
6769
* @author Halvdan Hoem Grelland
6870
* @author Aram Peres
71+
* @author Aaron Whiteside
6972
*/
7073
public class SpringMvcContractTests {
7174

@@ -566,6 +569,16 @@ public void testAddingTemplatedParameterWithTheSameKey()
566569
"{Accept}");
567570
}
568571

572+
@Test
573+
public void testMultipleRequestPartAnnotations() throws NoSuchMethodException {
574+
Method method = TestTemplate_RequestPart.class.getDeclaredMethod(
575+
"requestWithMultipleParts", MultipartFile.class, String.class);
576+
577+
MethodMetadata data = contract
578+
.parseAndValidateMetadata(method.getDeclaringClass(), method);
579+
assertThat(data.formParams()).contains("file", "id");
580+
}
581+
569582
public interface TestTemplate_Simple {
570583

571584
@RequestMapping(value = "/test/{id}", method = RequestMethod.GET,
@@ -670,6 +683,15 @@ String queryMapObject(@SpringQueryMap TestObject queryMap,
670683

671684
}
672685

686+
public interface TestTemplate_RequestPart {
687+
688+
@RequestMapping(path = "/requestPart", method = RequestMethod.POST,
689+
consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
690+
void requestWithMultipleParts(@RequestPart("file") MultipartFile file,
691+
@RequestPart("id") String identifier);
692+
693+
}
694+
673695
public interface TestTemplate_MatrixVariable {
674696

675697
@RequestMapping(path = "/matrixVariable/{params}")

0 commit comments

Comments
 (0)