62

I want to upload a file inside a form to a Spring Boot API endpoint.

The UI is written in React:

export function createExpense(formData) {
  return dispatch => {
    axios.post(ENDPOINT,
      formData, 
      headers: {
        'Authorization': //...,
        'Content-Type': 'application/json'
      }
      ).then(({data}) => {
        //...
      })
      .catch(({response}) => {
        //...
      });
    };
}

  _onSubmit = values => {
    let formData = new FormData();
    formData.append('title', values.title);
    formData.append('description', values.description);
    formData.append('amount', values.amount);
    formData.append('image', values.image[0]);
    this.props.createExpense(formData);
  }

This is the java side code:

@RequestMapping(path = "/{groupId}", method = RequestMethod.POST)
public ExpenseSnippetGetDto create(@RequestBody ExpensePostDto expenseDto, @PathVariable long groupId, Principal principal, BindingResult result) throws IOException {
   //..
}

But I get this exception on the Java side:

org.springframework.web.HttpMediaTypeNotSupportedException: Content type 'multipart/form-data;boundary=----WebKitFormBoundaryapHVvBsdZYc6j4Af;charset=UTF-8' not supported

How should I resolve this issue? The similar API endpoints and JavaScript side code is already working.

Note

I've seen a solution where it suggests that the request body should have 2 attributes: one which the JSON section goes under, another for the image. I'd like to see if it is possible to have it automatically converted to DTO.


Update 1

The upload payload sent by the client should be converted to the following DTO:

public class ExpensePostDto extends ExpenseBaseDto {

    private MultipartFile image;

    private String description;

    private List<Long> sharers;

}

So you can say it's a mix of JSON and multipart.


Solution

The solution to the problem is to use FormData on the front-end and ModelAttribute on the backend:

@RequestMapping(path = "/{groupId}", method = RequestMethod.POST,
        consumes = {"multipart/form-data"})
public ExpenseSnippetGetDto create(@ModelAttribute ExpensePostDto expenseDto, @PathVariable long groupId, Principal principal) throws IOException {
   //...
}

and on the front-end, get rid of Content-Type as it should be determined by the browser itself, and use FormData (standard JavaScript). That should solve the problem.

informatik01
  • 16,038
  • 10
  • 74
  • 104
Arian
  • 7,397
  • 21
  • 89
  • 177

11 Answers11

83

Yes, you can simply do it via wrapper class.

1) Create a Class to hold form data:

public class FormWrapper {
    private MultipartFile image;
    private String title;
    private String description;
}

2) Create an HTML form for submitting data:

<form method="POST" enctype="multipart/form-data" id="fileUploadForm" action="link">
    <input type="text" name="title"/><br/>
    <input type="text" name="description"/><br/><br/>
    <input type="file" name="image"/><br/><br/>
    <input type="submit" value="Submit" id="btnSubmit"/>
</form>

3) Create a method to receive form's text data and multipart file:

@PostMapping("/api/upload/multi/model")
public ResponseEntity<?> multiUploadFileModel(@ModelAttribute FormWrapper model) {
    try {
        // Save as you want as per requiremens
        saveUploadedFile(model.getImage());
        formRepo.save(mode.getTitle(), model.getDescription());
    } catch (IOException e) {
        return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
    }

    return new ResponseEntity("Successfully uploaded!", HttpStatus.OK);
}

4) Method to save file:

private void saveUploadedFile(MultipartFile file) throws IOException {
    if (!file.isEmpty()) {
        byte[] bytes = file.getBytes();
        Path path = Paths.get(UPLOADED_FOLDER + file.getOriginalFilename());
        Files.write(path, bytes);
    }
}
informatik01
  • 16,038
  • 10
  • 74
  • 104
UsamaAmjad
  • 4,175
  • 3
  • 28
  • 35
  • Can you pls tell me how to get utf-8 characters if I passed here in "title" variable. Because currently, I'm getting ???? for this. English characters are working fine. – Vijay Shegokar Dec 04 '18 at 10:47
  • @VijayShegokar did you add `CharacterEncodingFilter` in `web.xml`? – UsamaAmjad Dec 04 '18 at 13:14
  • yes. Actually there two applications. First application forwarding the request to another application via Zuul Proxy. I'm also getting the title, description these values as duplicate(comma separated) in the second application controller. But If I copy paste the same code in First application Controller and access that then everything works fine. – Vijay Shegokar Dec 06 '18 at 03:12
  • I think you should create a separate question for it so people can understand the problem. Or if you can find similar questions then please tag me in. – UsamaAmjad Dec 06 '18 at 05:03
  • When I do this, all fields in "model" come up null. @ModelAttribute is somehow not able to map the form fields to the DTO fields – Lewis Munene Jul 01 '21 at 11:35
  • Just figured out my problem! My DTO had no getters and setters (all fields were public). For some reason, the fields will not be mapped to the DTO if getters and setters are absent. When I added the getters and setters, I was now able to receive the form fields in the DTO – Lewis Munene Jul 01 '21 at 12:23
  • Yes setters are required, spring use them for mapping. If you are using immutable objects then you neend to write custom WebArgumentResolver. – UsamaAmjad Jul 02 '21 at 13:01
  • how to test this via postman – Narendra Pandey Apr 12 '23 at 10:03
  • I consider the solution below much better because it is not necessary to use a DTO on the server. – Carlos Ribeiro Jul 22 '23 at 00:19
44

I had created a similar thing using pure JS and Spring Boot. Here is the Repo. I'm sending an User object as JSON and a File as part of the multipart/form-data request.

The relevant snippets are below

The Controller code

@RestController
public class FileUploadController {

    @RequestMapping(value = "/upload", method = RequestMethod.POST, consumes = { "multipart/form-data" })
    public void upload(@RequestPart("user") @Valid User user,
            @RequestPart("file") @Valid @NotNull @NotBlank MultipartFile file) {
            System.out.println(user);
            System.out.println("Uploaded File: ");
            System.out.println("Name : " + file.getName());
            System.out.println("Type : " + file.getContentType());
            System.out.println("Name : " + file.getOriginalFilename());
            System.out.println("Size : " + file.getSize());
    }

    static class User {
        @NotNull
        String firstName;
        @NotNull
        String lastName;

        public String getFirstName() {
            return firstName;
        }

        public void setFirstName(String firstName) {
            this.firstName = firstName;
        }

        public String getLastName() {
            return lastName;
        }

        public void setLastName(String lastName) {
            this.lastName = lastName;
        }

        @Override
        public String toString() {
            return "User [firstName=" + firstName + ", lastName=" + lastName + "]";
        }

    }
}

The HTML and JS code

<html>    
<head>
    <script>
        function onSubmit() {

            var formData = new FormData();

            formData.append("file", document.forms["userForm"].file.files[0]);
            formData.append('user', new Blob([JSON.stringify({
                "firstName": document.getElementById("firstName").value,
                "lastName": document.getElementById("lastName").value
            })], {
                    type: "application/json"
                }));
            var boundary = Math.random().toString().substr(2);
            fetch('/upload', {
                method: 'post',
                body: formData
            }).then(function (response) {
                if (response.status !== 200) {
                    alert("There was an error!");
                } else {
                    alert("Request successful");
                }
            }).catch(function (err) {
                alert("There was an error!");
            });;
        }
    </script>
</head>

<body>
    <form name="userForm">
        <label> File : </label>
        <br/>
        <input name="file" type="file">
        <br/>
        <label> First Name : </label>
        <br/>
        <input id="firstName" name="firstName" />
        <br/>
        <label> Last Name : </label>
        <br/>
        <input id="lastName" name="lastName" />
        <br/>
        <input type="button" value="Submit" id="submit" onclick="onSubmit(); return false;" />
    </form>
</body>    
</html>
GSSwain
  • 5,787
  • 2
  • 19
  • 24
9

I had a similar use case where I had some JSON data and image upload (Think of it as a user trying to register with a personal details and profile image).

Referring to @Stephan and @GSSwain answer I came up with a solution with Spring Boot and AngularJs.

Below is a snapshot of my code. Hope it helps someone.

    var url = "https://abcd.com/upload";
    var config = {
        headers : {
            'Content-Type': undefined
        }

    }
    var data = {
        name: $scope.name,
        email: $scope.email
    }
    $scope.fd.append("obj", new Blob([JSON.stringify(data)], {
                type: "application/json"
            }));

    $http.post(
        url, $scope.fd,config
    )
        .then(function (response) {
            console.log("success", response)
            // This function handles success

        }, function (response) {
            console.log("error", response)
            // this function handles error

        });

And SpringBoot controller:

@RequestMapping(value = "/upload", method = RequestMethod.POST, consumes = {   "multipart/form-data" })
@ResponseBody
public boolean uploadImage(@RequestPart("obj") YourDTO dto, @RequestPart("file") MultipartFile file) {
    // your logic
    return true;
}
visrahane
  • 327
  • 4
  • 5
2

I built my most recent file upload application in AngularJS and SpringBoot which are similar enough in syntax to help you here.

My client side request handler:

uploadFile=function(fileData){
    var formData=new FormData();
    formData.append('file',fileData);
    return $http({
        method: 'POST',
        url: '/api/uploadFile',
        data: formData,
        headers:{
            'Content-Type':undefined,
            'Accept':'application/json'
        }
    });
};

One thing to note is Angular automatically sets the multipart mime type and boundary on the 'Content-Type' header value for me. Yours may not, in which case you need to set it yourself.

My application expects a JSON response from the server, thus the 'Accept' header.

You are passing in the FormData object yourself, so you need to make sure that your form is setting the File to whatever attribute you map to on your Controller. In my case it is mapped to the 'file' parameter on the FormData object.

My controller endpoints look like this:

@POST
@RequestMapping("/upload")
public ResponseEntity<Object> upload(@RequestParam("file") MultipartFile file) 
{
    if (file.isEmpty()) {
        return new ResponseEntity<Object>(HttpStatus.BAD_REQUEST);
    } else {
        //...
    }
}

You can add as many other @RequestParam as you'd like, including your DTO that represents the rest of the form, just make sure its structured that way as a child of the FormData object.

The key take-away here is that each @RequestParam is an attribute on the FormData object body payload on the multipart request.

If I were to modify my code to accommodate your data, it would look something like this:

uploadFile=function(fileData, otherData){
    var formData=new FormData();
    formData.append('file',fileData);
    formData.append('expenseDto',otherData);
    return $http({
        method: 'POST',
        url: '/api/uploadFile',
        data: formData,
        headers:{
            'Content-Type':undefined,
            'Accept':'application/json'
        }
    });
};

Then your controller endpoint would look like this:

@POST
@RequestMapping("/upload")
public ResponseEntity<Object> upload(@RequestParam("file") MultipartFile file, @RequestParam("expenseDto") ExpensePostDto expenseDto)
{
    if (file.isEmpty()) {
        return new ResponseEntity<Object>(HttpStatus.BAD_REQUEST);
    } else {
        //...
    }
}
Stephan
  • 666
  • 8
  • 23
  • I still get the same error. Your endpoint takes only one param, which is `file` and of type multipart form data. Mine is a combinatio of json and multipart. I update my post to include the DTO. – Arian Apr 20 '18 at 06:38
  • You can't do that. If you look at a pure multipart packet, the body is reserved for the file data. You're allowed to put path parameters, but not additional body payloads. I should have noticed that when I answered. I'll correct my answer when I'm off mobile. – Stephan Apr 20 '18 at 11:38
  • What is this example on Axios github then: https://github.com/axios/axios/blob/master/examples/upload/index.html – Arian Apr 22 '18 at 00:16
  • They're doing exactly what I have in the example above with the formData.append(). I think you're misunderstanding how the packet gets built behind the scenes. If you have a running copy of their example, I suggest you watch the network traffic in chrome and look at the packet structure. – Stephan Apr 23 '18 at 12:54
  • how to populate `otherData` of `formData.append('expenseDto',otherData);`? I tried `var otherData = {'name':'a'}`, but it throws error `Cannot convert value of type 'java.lang.String' to required type 'ExpenseDto'`. – frank Oct 10 '19 at 08:32
  • does your `ExpenseDTO` object have one attribute `name`, and no other required attributes? Spring automatically tries to deserialize the payload into a POJO assuming the POJO contains and is annotated to be valid from the JSON provided. – Stephan Oct 21 '19 at 19:15
2
@RequestMapping(value = { "/test" }, method = { RequestMethod.POST })
@ResponseBody
public String create(@RequestParam("file") MultipartFile file, @RequestParam String description, @RequestParam ArrayList<Long> sharers) throws Exception {
    ExpensePostDto expensePostDto = new ExpensePostDto(file, description, sharers);
    // do your thing
    return "test";
}

This seems to be the easiest way out here, other ways could be to add your own messageConverter.

Raghvendra Garg
  • 425
  • 1
  • 4
  • 11
0

Add the consumer type to your request mapping .it should work fine.

@POST
@RequestMapping("/upload")
public ResponseEntity<Object> upload(@RequestParam("file") MultipartFile file,consumes = "multipart/form-data") 
{
    if (file.isEmpty()) {
        return new ResponseEntity<Object>(HttpStatus.BAD_REQUEST);
    } else {
        //...
    }
}
this_is_om_vm
  • 608
  • 5
  • 23
0

You have to tell spring you're consuming multipart/form-data by adding consumes = "multipart/form-data" to the RequestMapping annotation. Also remove the RequestBody annotation from the expenseDto parameter.

@RequestMapping(path = "/{groupId}", consumes = "multipart/form-data", method = RequestMethod.POST)
public ExpenseSnippetGetDto create(ExpensePostDto expenseDto, 
   @PathVariable long groupId, Principal principal, BindingResult result) 
   throws IOException {
   //..
}

With the posted ExpensePostDto the title in the request is ignored.

Edit

You'll also need to change the content type to multipart/form-data. Sounds like that's the default for post based on some other answers. Just to be safe, I would specify it:

'Content-Type': 'multipart/form-data'
ShaneCoder
  • 781
  • 4
  • 11
0

Remove this from the react front end:

 'Content-Type': 'application/json'

Modify the Java side controller:

   @PostMapping("/{groupId}")
   public Expense create(@RequestParam("image") MultipartFile image,  @RequestParam("amount") double amount, @RequestParam("description") String description, @RequestParam("title") String title) throws IOException {
         //storageService.store(file); ....
          //String imagePath = path.to.stored.image;
         return new Expense(amount, title, description, imagePath);
 }

This can be written better but tried keeping it as close to your original code as much as I could. I hope it helps.

Femi Nefa
  • 114
  • 1
  • 6
0

In my case the problem was the DTO class having public fields and not getter/setter methods

public class MyDTO {
    public MultipartFile[] files;
    public String user;
    public String text;
}

The DTO class MUST have getters/setters, otherwise it won't work.

lainatnavi
  • 1,453
  • 1
  • 14
  • 22
0
@PostMapping( path = "/upload")
public ResponseEntity uploadRT14(@RequestParam("file") MultipartFile multipartFile, @RequestPart("uploadedFileInfo") UploadFileDTO uploadFileDTO) {
       ....... 
    }

i needed to send a file and json reqBody both at the same time, I tried using @RequestBody for UploadFileDTO but didn't work. also tried using @RequestParam that also didn't work as the dto was not a string, it was json. Finally it worked using @RequestPart

below is the postman setting that i used to test enter image description here

0

it took me the whole morning to find out the answer

I am doing form upload images and content using NextJS and Spring boot

 @PostMapping(consumes = {   "multipart/form-data" })
    @SecurityRequirement(name = "bearerAuth")
    public Object createProduct(@RequestPart("product")  ProductDTO product,
        @RequestParam(value = "primaryImage", required = false) MultipartFile mainImageMultipart,
        @RequestParam(value = "extraImage", required = false) MultipartFile[] extraImageMultiparts
                                ) throws ParseException {
        return ResponseEntity.ok().body(productServiceImpl.createOne(product, mainImageMultipart,extraImageMultiparts ));

    }

and in nextjs api route


async function products(req, res) {
  if (req.method === "POST") {
    const { token } = cookie.parse(req.headers.cookie);

    const data = await new Promise((resolve, reject) => {
      const form = formidable();

      form.parse(req, (err, fields, files) => {
        if (err) reject({ err });
        resolve({ err, fields, files });
      });
    });

    const { err, fields, files } = data;

    const formData = new FormData();
    formData.append(
      "product",
      new Blob([fields.product[0]], {
        type: "application/json",
      })
    );
    formData.append("primaryImage", getDataFile(files.primaryImage[0]));

    for (let i = 0; i < files.extraImage.length; i++) {
      formData.append("extraImage", getDataFile(files.extraImage[i]));
    }

    const resPost = await fetch(`${API_URL}/products`, {
      method: "POST",
      headers: {
        Authorization: `Bearer ${token}`,
      },
      body: formData,
    });

    const dataPos = await resPost.json();

    if (!resPost.ok) {
      console.log("Loi la " + JSON.stringify(dataPos));
      res.status(500).json({ message: dataPos.message });
    } else {
      res.status(200).json({ products: dataPos });
    }
    res.status(200).json({ message: "Data processed successfully" });
  }
}


Long tran
  • 71
  • 1
  • 2