spring-bootmultipartform-dataspring-resttemplate

Using MultipartFile to upload image to the server causes HttpClientErrorException$BadRequest: 400 Bad Request:


Taken from ThumbSnap

ThumbSnap offers a free API that can be used to upload and host images and videos programmatically. This is the main API method used to upload images to ThumbSnap

Required fields:

media - Binary image/video data. Should be sent as an HTTP POST formatted as multipart/form-data

key - The API key provided by ThumbSnap Upon successfully posting an image, a JSON-formatted document will be returned with a 'url' which is the full URL to the uploaded photo's page at ThumbSnap. This URL must be linked to any ThumbSnap-hosted thumbnails or images used by your application.

Sample Response A successful image upload will return a JSON document as follows:

{
  "data": {
    "id": "soLHmGdX",
    "url": "https://thumbsnap.com/soLHmGdX",
    "media": "https://thumbsnap.com/i/soLHmGdX.png",
    "thumb": "https://thumbsnap.com/t/soLHmGdX.jpg",
    "width": 224,
    "height": 224
  },
  "success": true,
  "status": 200
}

I want to use this service in Spring boot powered application, I have converted this response:

{
  "data": {
    "id": "soLHmGdX",
    "url": "https://thumbsnap.com/soLHmGdX",
    "media": "https://thumbsnap.com/i/soLHmGdX.png",
    "thumb": "https://thumbsnap.com/t/soLHmGdX.jpg",
    "width": 224,
    "height": 224
  },
  "success": true,
  "status": 200
}

to DTO as:

public record ThumbSnapResponseDTO(
        @JsonProperty(value = "data")
        DataDTO data,
        boolean success,
        int status
) {
        public record DataDTO(
                String id,
                String url,
                String media,
                String thumb,
                int width,
                int height
        ) {}
}

Next there is Service class witch have following logic:

@Slf4j
@Service
@RequiredArgsConstructor
public class ThumbSnapServiceImpl implements ThumbSnapService {

     private final RestTemplate restTemplate;
     private final ObjectMapper objectMapper;

    @Async
    @Override
    public Future<ThumbSnapResponseDTO> uploadImage(byte[] imageData, String filename) throws IOException {
        HttpHeaders headers = new HttpHeaders();
        // headers.setContentType(MediaType.MULTIPART_FORM_DATA); // First tried this
        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); // Second this also does not work

        MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
        body.add("key", THUMB_SNAP_API_KEY);
        body.add("media", new ByteArrayResource(imageData) {
            @Override
            public String getFilename() {
                return filename;
            }
        });

        HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(body, headers);
        restTemplate.getMessageConverters().add(new MultiPartMessageConverter(objectMapper));

        ResponseEntity<ThumbSnapResponseDTO> response =
                restTemplate.exchange(
                        THUMB_SNAP_BASE_URL,
                        HttpMethod.POST,
                        requestEntity,
                        ThumbSnapResponseDTO.class
                );

        CompletableFuture<ThumbSnapResponseDTO> future = new CompletableFuture<>();
        log.info("REST RESPONSE {} ", response.getBody());
        future.complete(response.getBody());
        return future;
    }
}

last but not least there is Controller method where this Service is actually used:

// ThumbSnap
private final ThumbSnapService thumbSnapService;

@Override
@PostMapping("/{portfolioId}/upload")
public ResponseEntity<?> saveToThumbSnap(
        @PathVariable(name = "portfolioId") Long portfolioId,
        @RequestPart("media") MultipartFile file
) throws ExecutionException, InterruptedException, IOException {
    // Logign
    String fileName = StringUtils.cleanPath(Objects.requireNonNull(file.getOriginalFilename()));
    long size = file.getSize();

    log.info("File is {} - - and size is {} ", fileName, size);
    // Get Data as bytes
    byte[] imageData = file.getBytes();
    
    Future<ThumbSnapResponseDTO> response = thumbSnapService.uploadImage(imageData, fileName);
    ThumbSnapResponseDTO result = response.get();

    var portfolio = portfolioService.findById(portfolioId).orElseThrow(
            () -> new EntityNotFoundException(String.format("Record not found with id = %s", portfolioId))
    );

    // Check if Image Uploaded Successfully
    if (!result.success())
        throw new RuntimeException("Unable to upload image CONTROLLER");

    var portfolioItemImage = new PortfolioItemImage();
    //set img URL
    portfolioItemImage.setImgUrl(result.data().url());
    // Save mapping
    portfolioItemImage.setPortfolio(portfolio);

    return ResponseEntity.status(HttpStatus.OK).body(
            portfolioItemImageMapper.asDTO(portfolioItemImageService.save(portfolioItemImage))
    );
}

with that said when I try it out in PostMan REST Client for sending post request to above endpoint I get error:

{
    "code": "EXECUTION",
    "message": "org.springframework.web.client.HttpClientErrorException$BadRequest: 400 Bad Request: \"No API key specified. Upload stopped\""
}

The ScreenShot is below:

Snip of PostMan Showing Error

I have checked API_KEY a zillion times, but in-vain.

The Question is:

Is there any problem with following code:

MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
body.add("key", THUMB_SNAP_API_KEY);
body.add("media", new ByteArrayResource(imageData) {
    @Override
    public String getFilename() {
        return filename;
    }
});

HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(body, headers);
restTemplate.getMessageConverters().add(new MultiPartMessageConverter(objectMapper));

ResponseEntity<ThumbSnapResponseDTO> response =
        restTemplate.exchange(
                THUMB_SNAP_BASE_URL,
                HttpMethod.POST,
                requestEntity,
                ThumbSnapResponseDTO.class
        );

Or there is any other way to accomplish this. How can I send request correctly can you please suggest a solution to this.


Solution

  • As for as the above code is concerned, every thing looks fine except @PostMapping. You have to change that line as follows:

    @PostMapping(path = "/{userId}/upload",
                consumes =  MediaType.MULTIPART_FORM_DATA_VALUE,
                produces = MediaType.APPLICATION_JSON_VALUE
        )
    

    The consume attribute is used to specify the types of media that the method can process in the request body. The MediaType.MULTIPART_FORM_DATA_VALUE value indicates that the method can accept data in multipart/form data format, which is commonly used for file upload operations, where as the value MediaType.APPLICATION_JSON_VALUE indicates that the method can return data in application/json format.

    And now It should work as intended/expected.