Spring webflux multipart file upload with JSON input

Viewed 2401

I have a reactive web application and looking for file upload with input data.

Working code for File upload,

Router,

.route(POST("/api/v1/upload").and(contentType(MULTIPART_FORM_DATA)),
                    patientRequestHandler::uploadFile)

Handler,

public Mono<ServerResponse> uploadFile(ServerRequest request) {
        final String id = request.pathVariable("id");
            return request.body(BodyExtractors.toMultipartData()).flatMap(parts -> {
                Map<String, Part> partMap = parts.toSingleValueMap();

Working code for input data,

Router,

.andRoute(POST("/api/v1/profile").and(contentType(APPLICATION_JSON)),
                    requestHandler::createProfile)

Handler,

public Mono<ServerResponse> createProfile(ServerRequest request) {
            return request.bodyToMono(ProfileDto.class).flatMap(patient -> {

@JsonInclude(Include.NON_NULL)
public class ProfileDto {

    private Long id;
    private String name;

Now, I would like to have form data submission along with the file upload.

Not sure, How to do that?

I tried the below,

Route,

.route(PUT("/api/v1/upload").and(accept(MULTIPART_FORM_DATA, APPLICATION_JSON)), branchHandler::updateSetting);

DTO,

@JsonInclude(Include.NON_NULL)
public class ImageWrapper extends ProfileDto {

    private FilePart image;

    public FilePart getImage() {
        return image;
    }

public void setImage(FilePart image) {
    this.image = image;
}

}

Handler,

public Mono<ServerResponse> updateSetting(ServerRequest request) {
        final LocalDateTime updatedTime = LocalDateTime.now();
        request.bodyToMono(ImageWrapper.class).flatMap(profileDto -> {
                return null;
            });
    }

Here,

I tried with postman.

enter image description here

Postman Error,

{
    "timestamp": "2021-06-15T19:07:52.763+00:00",
    "path": "/api/v1/service/profile",
    "status": 415,
    "error": "Unsupported Media Type",
    "requestId": "ce7a2dc9-1"
}
3 Answers

Specifying the content type as multipart/form-data, I was able to call the endpoint from Postman with a file and a JSON without any error.

Code:

package io.github.devatherock.demoroute;

import java.util.Map;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.http.MediaType;
import org.springframework.http.codec.multipart.FilePart;
import org.springframework.http.codec.multipart.FormFieldPart;
import org.springframework.http.codec.multipart.Part;
import org.springframework.web.reactive.function.BodyExtractors;
import org.springframework.web.reactive.function.server.RequestPredicates;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerResponse;

import reactor.core.publisher.Mono;

@SpringBootApplication
public class DemoRouteApplication {
    private static final Logger LOGGER = LoggerFactory.getLogger(DemoRouteApplication.class);

    public static void main(String[] args) {
        SpringApplication.run(DemoRouteApplication.class, args);
    }

    @Bean
    public RouterFunction<ServerResponse> profileRoute() {
        return RouterFunctions.route(
                RequestPredicates.POST("/api/v1/profile")
                        .and(RequestPredicates.contentType(MediaType.MULTIPART_FORM_DATA)),
                request -> request.body(BodyExtractors.toMultipartData()).flatMap(parts -> {
                    Map<String, Part> partMap = parts.toSingleValueMap();

                    partMap.forEach((partName, value) -> {
                        LOGGER.info("Name: {}, value: {}", partName, value);
                    });

                    // Handle file
                    FilePart image = (FilePart) partMap.get("image");
                    LOGGER.info("File name: {}", image.filename());

                    // Handle profile
                    FormFieldPart profile = (FormFieldPart) partMap.get("profile");

                    return Mono.empty();
                })
                        .then(ServerResponse.ok().bodyValue("Profile updated")));
    }
}

Curl command generated from Postman:

curl --location --request POST 'http://localhost:8080/api/v1/profile' \
--form 'profile="{\"id\":1234,\"name\":\"Test\"}"' \
--form 'image=@"/path/to/upload.txt"'

Invoking the endpoint from Postman or using the above curl command, returns a HTTP 200 with the response body as Profile updated and produces the below logs:

2021-11-20 09:46:01.969  INFO 16365 --- [or-http-epoll-3] i.g.d.demoroute.DemoRouteApplication     : Name: image, value: DefaultFilePart{image (upload.txt)}
2021-11-20 09:46:01.970  INFO 16365 --- [or-http-epoll-3] i.g.d.demoroute.DemoRouteApplication     : Name: profile, value: DefaultFormFieldPart{profile}
2021-11-20 09:46:01.970  INFO 16365 --- [or-http-epoll-3] i.g.d.demoroute.DemoRouteApplication     : File name: upload.txt

From the logs, we can see that both the JSON body with form-data named profile and the upload.txt file with form-data named image made it to the route handler

Modify your route as:

.and(contentType(MediaType.parseMediaType("multipart/form-data"),MediaType.parseMediaType("application/json"))

Also, I would rather send json data as one of the parts of your multi-part request body. And therefore work with only one content type i.e multipart/form-data

create a new wrapper class like this

@JsonInclude(Include.NON_NULL)
public class ProfileDtoWrapper extends ProfileDto {

    private FilePart image;

    public FilePart getImage() {
        return image;
    }

    public void setImage(FilePart image) {
        this.image = image;
    }

also change the router to handle both multipart and json and it should work

Related