-
Notifications
You must be signed in to change notification settings - Fork 38.8k
Description
Affects:
- spring-boot-starter-webflux: 3.2.5
- spring-webflux: 6.1.6
Context
Hello
I'm trying to use HTTP Interface to send Multipart data. My goal is to send 2 part. So I write this function:
Service
public interface TestService {
@PostExchange(value = "/", accept = MediaType.APPLICATION_JSON_VALUE)
Mono<MyObject> create(
@RequestPart Part a,
@RequestPart Part b
);
}Part
I think we need to create our own Part (I didn't find any usable implementation), so I create
one for byte[] and other for json
public class BytesPart implements Part {
private final String name;
private final String filename;
private final byte[] data;
public BytesPart(String name, byte[] data) {
this(name, data, null);
}
public BytesPart(String name, byte[] data, String filename) {
this.name = name;
this.data = data;
this.filename = filename;
}
@Override
public String name() {
return name;
}
@Override
public HttpHeaders headers() {
MultiValueMap<String, String> headers = new LinkedMultiValueMap<>();
StringBuilder contentDispositionBuilder = new StringBuilder();
contentDispositionBuilder.append("form-data");
if(filename != null) {
contentDispositionBuilder.append("; filename=\"").append(filename).append("\"");
}
headers.add("Content-Disposition", contentDispositionBuilder.toString());
headers.add("Content-Type", "application/octet-stream");
headers.add("Content-Transfer-Encoding", "binary");
return HttpHeaders.readOnlyHttpHeaders(headers);
}
@Override
public Flux<DataBuffer> content() {
return Flux.just(data).map(bytes -> new DefaultDataBufferFactory().wrap(bytes));
}
}public class JsonPart implements Part {
private final String name;
private final String filename;
private final String json;
public JsonPart(String name, String json) {
this(name, json, null);
}
public JsonPart(String name, String json, String filename) {
this.name = name;
this.json = json;
this.filename = filename;
}
@Override
public String name() {
return name;
}
@Override
public HttpHeaders headers() {
MultiValueMap<String, String> headers = new LinkedMultiValueMap<>();
StringBuilder contentDispositionBuilder = new StringBuilder();
contentDispositionBuilder.append("form-data");
if(filename != null) {
contentDispositionBuilder.append("; filename=\"").append(filename).append("\"");
}
headers.add("Content-Disposition", contentDispositionBuilder.toString());
headers.add("Content-Type", "application/json; charset=UTF-8");
return HttpHeaders.readOnlyHttpHeaders(headers);
}
@Override
public Flux<DataBuffer> content() {
return Flux.just(json).map(json -> new DefaultDataBufferFactory().wrap(json.getBytes()));
}
}Service usage
I'm using the service and part like that:
testService.create(
new JsonPart("MyFileName1", "{\"test\": \"value\"}"), // Part a
new BytesPart("MyFileName2", content, "test.pdf") // Part b
)Requests
Result without annotation parameters
With the service's function with this definition:
Mono<MyObject> create(@RequestPart Part a, @RequestPart Part b);The following request will be generated
Content-Type: multipart/form-data; boundary=woUHyaWlaKTKt-kuazHfdfjEsB9pxg7k_eirw9Qp
Body:
--woUHyaWlaKTKt-kuazHfdfjEsB9pxg7k_eirw9Qp
Content-Disposition: form-data; name="a"
Content-Type: application/json; charset=UTF-8
{"test": "value"}
--woUHyaWlaKTKt-kuazHfdfjEsB9pxg7k_eirw9Qp
Content-Disposition: form-data; name="b"; filename="test.pdf"
Content-Type: application/octet-stream
Content-Transfer-Encoding: binary
azerzaerzerzerazrzaereaze
--woUHyaWlaKTKt-kuazHfdfjEsB9pxg7k_eirw9Qp--
Problem
As you can see in the request, in the Content-Disposition value, the name
is corresponding to the variable defined in the definition of the function and not
the name from the Part values.
The function Part.name() is never used to obtain the name of the Part.
Result with annotation parameters
With the service's function with this definition:
Mono<MyObject> create(@RequestPart("test1") Part a, @RequestPart("test2") Part b);The following request will be generated
Content-Type: multipart/form-data; boundary=woUHyaWlaKTKt-kuazHfdfjEsB9pxg7k_eirw9Qp
Body:
--woUHyaWlaKTKt-kuazHfdfjEsB9pxg7k_eirw9Qp
Content-Disposition: form-data; name="test1"
Content-Type: application/json; charset=UTF-8
{"test": "value"}
--woUHyaWlaKTKt-kuazHfdfjEsB9pxg7k_eirw9Qp
Content-Disposition: form-data; name="test2"; filename="test.pdf"
Content-Type: application/octet-stream
Content-Transfer-Encoding: binary
azerzaerzerzerazrzaereaze
--woUHyaWlaKTKt-kuazHfdfjEsB9pxg7k_eirw9Qp--
Problem
As you can see in the request, in the Content-Disposition value, the name
is corresponding to the value sent in @RequestPart("XXX") for each part, but
like the previous case the function Part.name() is never used to obtain the name of the Part.
Other tests
For my Part implementations, I replaced the:
public class JsonPart implements Part {
// ...
@Override
public String name() {
return name;
}
// ...
}by
public class JsonPart implements Part {
// ...
@Override
public String name() {
throw new RuntimeException("Not used");
}
// ...
}And as expected, no error has been thrown during the request.
(Same in debug mode, the breakpoint is never used)
Expectation
The priority to define the multipart name should be:
flowchart v
A[Part.name] -->|Is Present| B[Use Part.name]
A -->|Is Absent| C[@RequestPart.name]
C -->|Is Present| D[Use @RequestPart.name]
C -->|Is Absent| E[Use signature variable name]
Part name defined
If the name in Part implementation is defined, the name should be used as name in multipart content
testService.create(
new JsonPart("MyFileName1", "{\"test\": \"value\"}"), // Part a
new BytesPart("MyFileName2", content, "test.pdf") // Part b
)So the values in multipart content should be:
Content-Disposition: form-data; name="MyFileName1"
&
Content-Disposition: form-data; name="MyFileName2"; filename="test.pdf"
even if the @RequestPart.name is defined. The name from Part should override it.
Part name not defined
If the name in Part implementation is not defined, the @RequestPart.name should be used as name in multipart content
testService.create(
new JsonPart(null, "{\"test\": \"value\"}"), // Part a
new BytesPart(null, content, "test.pdf") // Part b
)And the service:
public interface TestService {
@PostExchange(value = "/", accept = MediaType.APPLICATION_JSON_VALUE)
Mono<MyObject> create(
@RequestPart("Name1") Part a,
@RequestPart("Name2") Part b
);
}So the values in multipart content should be:
Content-Disposition: form-data; name="Name1"
&
Content-Disposition: form-data; name="Name2"; filename="test.pdf"
Not name defined
If Part.name and @RequestPart.name are not defined, the name of the variable in the function signature should be used.
testService.create(
new JsonPart(null, "{\"test\": \"value\"}"), // Part a
new BytesPart(null, content, "test.pdf") // Part b
)public interface TestService {
@PostExchange(value = "/", accept = MediaType.APPLICATION_JSON_VALUE)
Mono<MyObject> create(
@RequestPart Part a,
@RequestPart Part b
);
}So the values in multipart content should be:
Content-Disposition: form-data; name="a"
&
Content-Disposition: form-data; name="b"; filename="test.pdf"