javaspringspring-boot

Spring boot - method 'POST' is not supported (multipart/form-data)


I'm trying to send through multipart/form-data a post request from my products controller, where I upload a file of images and information of my product in json


@RestController
@CrossOrigin(origins = "*", maxAge = 3600)
@RequestMapping("/product")
public class ProductController {
    final ProductService productService;
    final CategoryService categoryService;
    final ProductMapper productMapper;
    final S3Client s3Client;

    private final String BUCKET_NAME = "awstockproducts" + System.currentTimeMillis();

    public ProductController(ProductService productService, ProductMapper productMapper, CategoryService categoryService, S3Client s3Client) {
        this.productService = productService;
        this.productMapper = productMapper;
        this.categoryService = categoryService;
        this.s3Client = s3Client;
    }
    @PostMapping(MediaType.MULTIPART_FORM_DATA_VALUE)
    public ResponseEntity<Object> saveProduct (@RequestPart("productDto") @Valid ProductDto productDto, @RequestPart(value = "file")MultipartFile file) {
        try {
            if (productService.existsByProduct(productDto.getProduct())) {
                return ResponseEntity.status(HttpStatus.CONFLICT).body("Product already exists!");
            }
            ProductModel productModel = productMapper.toProductModel(productDto);
            CategoryModel categoryModel = categoryService.findById(productDto.getProductCategory().getCategory_id())
                    .orElseThrow(() -> new RuntimeException("Category not found"));
            productModel.setProductCategory(categoryModel);

            String fileName = "/products/images/" + UUID.randomUUID().toString() + "-" + file.getOriginalFilename();

            s3Client.putObject(PutObjectRequest
                            .builder()
                            .bucket(BUCKET_NAME)
                            .key(fileName)
                            .build(),
                        software.amazon.awssdk.core.sync.RequestBody.fromString("Testing java sdk"));
            return ResponseEntity.status(HttpStatus.CREATED).body(productService.save(productModel));
        } catch (Exception e) {
            return ResponseEntity.status(HttpStatus.CONFLICT).body("Cannot create product. Check if the fields sent in your request are correct.");
        }
    }



in my postman I'm sending it like this, and getting the error 405 enter image description here

In my console I am getting the error:

Request method 'POST' is not supported]

I don't understand why since I am sending a postMapping

updated error in postman:


{
    "cause": null,
    "stackTrace": [
        {
            "classLoaderName": "app",
            "moduleName": null,
            "moduleVersion": null,
            "methodName": "from",
            "fileName": "UnrecognizedPropertyException.java",
            "lineNumber": 61,
            "nativeMethod": false,
            "className": "com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException"
        },
        {
            "classLoaderName": "app",
            "moduleName": null,
            "moduleVersion": null,
            "methodName": "handleUnknownProperty",
            "fileName": "DeserializationContext.java",
            "lineNumber": 1132,
            "nativeMethod": false,
            "className": "com.fasterxml.jackson.databind.DeserializationContext"
        },
        {
            "classLoaderName": "app",
            "moduleName": null,
            "moduleVersion": null,
            "methodName": "handleUnknownProperty",
            "fileName": "StdDeserializer.java",
            "lineNumber": 2202,
            "nativeMethod": false,
            "className": "com.fasterxml.jackson.databind.deser.std.StdDeserializer"
        }, ...

Solution

  • Change your @PostMapping(MediaType.MULTIPART_FORM_DATA_VALUE) because putting the media type there will actually map the URL to /product/multipart/form-data and not /product/

    change it to this

    @PostMapping(value = "/", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    

    also, you need to update the method signature to something like this (Change @RequestPart to @RequestParam)

    @PostMapping(value = "/", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    public ResponseEntity<Object> saveProduct (@RequestParam("productDto") String jsonString, @RequestParam("file") MultipartFile file) {
           ObjectMapper objectMapper = new ObjectMapper();
           ProductDto productDto = objectMapper.readValue(jsonString, ProductDto.class);
        }
    

    and make sure you have the relevant dependencies commons-fileupload and commons-io

    also I recomemend when Uploading set a maximum upload size for the file you can do it like this:

    @Bean
    public MultipartResolver multipartResolver() {
        CommonsMultipartResolver resolver = new CommonsMultipartResolver();
        resolver.setMaxUploadSize(1024 * 1024 * 5); // 5 MB
        return resolver;
    }
    

    and then in postman call http://localhost:8078/product/

    A second solution you can create the suitable converter in Spring: the solution is like

    public class ProductDtoConverter extends AbstractHttpMessageConverter<ProductDto> {
        
        public ProductDtoConverter() {
            super(MediaType.APPLICATION_JSON);
        }
        
        @Override
        protected boolean supports(Class<?> clazz) {
            return ProductDto.class.isAssignableFrom(clazz);
        }
        
        @Override
        protected ProductDto readInternal(Class<? extends ProductDto> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
            String productDtoString = IOUtils.toString(inputMessage.getBody(), StandardCharsets.UTF_8);
            ObjectMapper objectMapper = new ObjectMapper();
            return objectMapper.readValue(productDtoString, ProductDto.class);
        }
    }
    

    and in you AppConfig:

    @Configuration
    public class AppConfig {
        
        @Bean
        public ProductDtoConverter productDtoConverter() {
            return new ProductDtoConverter();
        }
    }