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"
Comment From: Hansanto
Also, I tried to set name="..."
in header for the Content-Disposition
but it's override during the request to use the @RequestParam.name
or ,variable name from function signature
Comment From: rstoyanchev
I'm not able to address your description in detail, because the premise at the start that you need to create a Part
is incorrect. We do support Part
, and that's useful if you need to make a remote call from a controller, but we don't expect you to create one.
The supported types for @RequestPart
are listed in the reference docs.
Add a request part, which may be a String (form field), Resource (file part), Object (entity to be encoded, e.g. as JSON), HttpEntity (part content and headers), a Spring Part, or Reactive Streams Publisher of any of the above.
Generally I'm also not sure why using the variable name or the annotation to give the part a name is an issue.
Comment From: spring-projects-issues
If you would like us to look at this issue, please provide the requested information. If the information is not provided within the next 7 days this issue will be closed.
Comment From: spring-projects-issues
Closing due to lack of requested feedback. If you would like us to look at this issue, please provide the requested information and we will re-open the issue.