Skip to content

Part.name is not used with REST HTTP Interface #33423

@Hansanto

Description

@Hansanto

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]
Loading

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"

Metadata

Metadata

Assignees

No one assigned

    Labels

    in: webIssues in web modules (web, webmvc, webflux, websocket)

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions