70

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.

3
  • Your content type is incorrect, FormData does not produce application/json Commented Apr 15, 2018 at 18:32
  • I changed it to multipart/form-data and I still get the same error. Commented Apr 16, 2018 at 7:17
  • This is what you exactly need: stackoverflow.com/questions/25699727/… Commented Apr 17, 2018 at 19:59

12 Answers 12

92
+25

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);
    }
}
Sign up to request clarification or add additional context in comments.

9 Comments

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.
@VijayShegokar did you add CharacterEncodingFilter in web.xml?
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.
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.
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
|
49

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>

5 Comments

It worked for me. Just adding, it was necessary to add: processData: false, contentType: false, cache: false, so that it works well. Spring Boot 2.1.7. and it was not necessary to add the "consumes".
@GSSwain it working fine for me. How to test the endpoint from POSTMAN.
Somebody got it to work in Postman?
For sending such a request with Postman (see this answer) do the following in the 'Body' section (the 'Params' section must be empty): First, select form-data as the "global" content type. For the 'file' key, hit the dropdown in the very right of the column and select File, not Text. For the 'user' key, set the value to a valid json representation and crucially set the Content type column to application/json (This column is hidden by default - click on the 3 dots to the very right of the 'Key'/'Value' table header to display it).
This works, however it's pretty difficult to test using MockMvc, when building a multipart request I could not figure out a way to set the "sub"-content type for the JSON parameter (the user in your example).
10

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;
}

Comments

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 {
        //...
    }
}

6 Comments

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.
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.
What is this example on Axios github then: github.com/axios/axios/blob/master/examples/upload/index.html
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.
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'.
|
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.

Comments

2
@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

Comments

2

this is how i implemented it.

in frontend - angular

// there is a sample model used to show the concept of sending files and model in same HTTP request this is the model class in Angular

class ReportRequestModel {
constructor(public appName: string, public version: string) {}
}

angular app.component.ts code

onUpload() {

const model = new ReportRequestModel('o support', '2.0');

const formData = new FormData();
formData.append('files', this.selectedFile);
formData.append('dto', JSON.stringify(model));

this.http
  .post('http://localhost:8088/', formData, {
    observe: 'response',
  })
  .subscribe((response) => {
    this.getImageNames();
  });

in java-spring API i used ObjectMapper to deserialize the String dto to Java class ReportRequestDTO

private final ObjectMapper objectMapper;

@PostMapping
public ResponseEntity<Report> saveReport(@RequestBody List<MultipartFile> files, 
                                         @RequestPart String dto){
    
    
    try {
      var data = objectMapper.readValue(dto, ReportRequestDTO.class);
      System.out.println(data);
    } catch (JsonProcessingException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    }
    
    Report savedReport = null;
    
    for (MultipartFile file : files) {
    
    Report report = null;       
    
    try {
        report = new Report();
        report.setFileData(file.getBytes());
        report.setFileName(file.getOriginalFilename());
        report.setFileContent(file.getContentType());
        
    } catch (IOException e) {
        e.getLocalizedMessage();
    }
    
    
    
    try {
        savedReport = reportsRepository.save(report);
    } catch (Exception e) {
        System.out.println(e.getMessage());
    }       
    
    }
    
    return ResponseEntity.ok(savedReport);
}

ReportRequestDTO class

@Data
public class ReportRequestDTO implements Serializable {

private static final long serialVersionUID = 1L;

@JsonProperty("appName")
private String appName;

@JsonProperty("version")
private String version;
}

Comments

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.

Comments

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'

Comments

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.

Comments

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" });
  }
}


Comments

-1

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 {
        //...
    }
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.