Tunneling MultipartFile - java

I have a spring controller that accepts a class named FileUploadBean on POST. The controller method looks like that:
First Controller:
#RequestMapping(value = "/upload", method = RequestMethod.POST)
#ResponseBody
public ResponseEntity<byte[]> uploadFile(final FileUploadBean fileUploadBean) throws IOException {
// Some code that works fine here
}
One of the FileUploadBean properties is of type MultipartFile.
Now, I'm trying to add some sort of wrapper controller (that will run on another server) that also accepts FileUploadBean and just forwards the request to the first controller:
Second (wrapper) Controller:
#RequestMapping(value="/upload", method = RequestMethod.POST)
#ResponseBody
public ResponseEntity<byte[]> uploadImage(final FileUploadBean fileUploadBean) throws IOException {
ResponseEntity<byte[]> response = restTemplate.postForEntity([first controller url here], fileUploadBean, byte[].class);
return response;
}
When I'm sending the request to the first controller I get:
org.springframework.http.converter.HttpMessageNotWritableException:
Could not write JSON: No serializer found for class
java.io.FileDescriptor and no properties discovered to create
BeanSerializer (to avoid exception, disable
SerializationFeature.FAIL_ON_EMPTY_BEANS) ) (through reference chain:
com.outbrain.images.beans.FileUploadBean["file"]->org.springframework.web.multipart.commons.CommonsMultipartFile["fileItem"]->org.apache.commons.fileupload.disk.DiskFileItem["inputStream"]->java.io.FileInputStream["fd"]);
nested exception is
com.fasterxml.jackson.databind.JsonMappingException: No serializer
found for class java.io.FileDescriptor and no properties discovered to
create BeanSerializer (to avoid exception, disable
SerializationFeature.FAIL_ON_EMPTY_BEANS) ) (through reference chain:
com.outbrain.images.beans.FileUploadBean["file"]->org.springframework.web.multipart.commons.CommonsMultipartFile["fileItem"]->org.apache.commons.fileupload.disk.DiskFileItem["inputStream"]->java.io.FileInputStream["fd"])
at org.springframework.http.converter.json.MappingJackson2HttpMessageConverter.writeInternal
How can I make this request work?

Well, after some struggling this is how I solved it. That's what I did in the second controller:
#RequestMapping(value = "/upload", method = RequestMethod.POST)
public #ResponseBody
ResponseEntity<byte[]> uploadImage(final FileUploadBean fileUploadBean) throws Exception {
File file = null;
try {
final MultiValueMap<String, Object> requestParts = new LinkedMultiValueMap<>();
final String tmpImageFileName = IMAGE_TMP_DIR + fileUploadBean.getFile().getOriginalFilename();
file = new File(tmpImageFileName);
fileUploadBean.getFile().transferTo(file);
requestParts.add("file", new FileSystemResource(tmpImageFileName));
HttpHeaders headers = new HttpHeaders();
headers.set("Content-Type", "multipart/form-data"); // Sending it like the client-form sends it
ResponseEntity<byte[]> response = restTemplate.exchange(ImageUrlUtils.getUploadUrl(), HttpMethod.POST, new HttpEntity<>(requestParts, headers),
byte[].class);
return new ResponseEntity<>(response.getBody(), response.getStatusCode());
} catch (Exception ex) {
return new ResponseEntity<>((ex.getMessage).getBytes("UTF-8"),
HttpStatus.INTERNAL_SERVER_ERROR);
} finally {
if (file != null && file.exists()) {
file.delete();
}
}
}

I debug previous answer, and found this solution without save file to file system
#PostMapping(value = "/upload")
public ResponseEntity<Object> upload(MultipartHttpServletRequest request) throws Exception {
final MultiValueMap<String, Object> requestParts = new LinkedMultiValueMap<>();
request.getParameterMap().forEach((name, value) -> requestParts.addAll(name, asList(value)));
request.getMultiFileMap().forEach((name, value) -> {
List<Resource> resources = value.stream().map(MultipartFile::getResource).collect(toList());
requestParts.addAll(name, resources);
});
HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(requestParts, request.getRequestHeaders());
return restTemplate.exchange(ImageUrlUtils.getUploadUrl() + "?" + request.getQueryString(),
request.getRequestMethod(), requestEntity, Object.class);
}

Related

Java Spring RestTemplate POST issue, while Postman works

I'm quite new to Spring and I'm trying to use it to save Mandrill webhooks events. After creating all the required validators I've discovered that it's sending its events using application/x-www-form-urlencoded header, which was preventing all the validators to work.
After that, I've decided to handle Mandrill webhooks remapping its request as a valid JSON object, with its header Content-Type: application/json.
Searching on SO, I've found so many ways to send a POST request from Spring to itself, and after a few tries, I've started using RestTemplate, like this
Webhook Controller
#RequestMapping(method = RequestMethod.POST, consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
public void remap(#RequestParam String mandrill_events) throws IOException {
JsonNodeFactory factory = JsonNodeFactory.instance;
ArrayNode eventsNode = factory.arrayNode();
ObjectNode eventNode = factory.objectNode();
ObjectMapper mapper = new ObjectMapper();
JsonNode node = mapper.readTree(mandrill_events);
if (node.isArray()) {
for (int i = 0; i < node.size(); i++) {
eventsNode.add(node.get(i));
}
}
eventNode.set("mandrill_events", eventsNode);
String webhookStoreURL = this.globalConfig.APIUrl + "/webhook/store";
RestTemplate restTemplate = new RestTemplate();
try {
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.setContentType(MediaType.APPLICATION_JSON);
httpHeaders.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
HttpEntity<String> httpEntity = new HttpEntity<>(eventNode.toString(), httpHeaders);
ResponseEntity<String> responseEntity = restTemplate.postForEntity(webhookStoreURL, httpEntity, String.class);
} catch (Exception e) {
e.printStackTrace();
}
}
#RequestMapping(path = "/store", method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
public void store(#RequestBody #Valid MandrillEventList eventList) throws StorageException, NotFoundException, SQLException {
System.out.println("Saving " + eventList.getMandrillEvents().size() + " events");
//this.eventService.storeEvents(eventList);
}
The problem is, when I send the RestTemplate request, the only thing I get it's a 400 null error from the catch block.
So this is what I've tried so far:
Removing #Valid annotation from controller method. Didn't work
Removing all the validators from the global one. Didn't work
Seemed like it was trying to validate request body, even without annotations, so I've tried those two as well
Copying the request body from debug and testing it on Postman. Works (shows validation errors correctly)
Using the Mandrill test webhook. Works (Spring and Postman)
I have no idea about where to look
I came out with a solution. Instead of re-sending the API request to himself, I've done this
#RequestMapping(method = RequestMethod.POST, consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
public void store(#RequestParam String mandrill_events) throws IOException {
JsonNodeFactory factory = JsonNodeFactory.instance;
ArrayNode eventsNode = factory.arrayNode();
ObjectNode eventNode = factory.objectNode();
ObjectMapper mapper = new ObjectMapper();
JsonNode node = mapper.readTree(mandrill_events);
if (node.isArray()) {
for (int i = 0; i < node.size(); i++) {
eventsNode.add(node.get(i));
}
}
eventNode.set("mandrill_events", eventsNode);
try {
MandrillEventList eventList = mapper.readValue(eventNode.toString(), MandrillEventList.class);
logger.log(Level.FINE, "Webhook has " + eventList.getMandrillEvents().size() + " events to save");
this.eventService.storeEvents(eventList);
} catch (Exception e) {
e.printStackTrace();
logger.log(Level.SEVERE, "Unable to setup mandrill event list " + e.getMessage());
throw new IOException("Unable to setup mandrill event list");
}
}
I rebuild the JSON as Controller expected it then I map it as the original MandrillEventList object with Jackson ObjectMapper. It gave me some problems, for example when he didn't recognize properties which weren't inside class description, so I've added the #JsonIgnoreProperty annotation like this:
#JsonIgnoreProperties(ignoreUnknown = true)
public class MandrillEvent {
[...]
}
Everything works right now

Spring - RestTemplate - Multipart File

This controller works fine
#Controller
public class FileUploadController {
....
#PostMapping("/convert")
public void fileUpload(#RequestParam("file") MultipartFile file,
RedirectAttributes redirectAttributes, HttpServletResponse response) {
Now i want to call this controller from another spring project via RestTemplate. I tried many things, but noting works. Here is my last code:
#Controller
public class FileController {
....
#PostMapping("/convert")
public void fileUpload(#RequestParam("file") MultipartFile file, RedirectAttributes redirectAttributes,
HttpServletResponse response) throws Exception {
ArrayList<HttpMessageConverter<?>> converters = new ArrayList<HttpMessageConverter<?>>(
Arrays.asList(new FormHttpMessageConverter(),new MappingJackson2HttpMessageConverter(), new ResourceHttpMessageConverter()));
RestTemplate template = restTemplate();
template.setMessageConverters(converters);
HttpHeaders header = new HttpHeaders();
header.setContentType(MediaType.MULTIPART_FORM_DATA);
MultiValueMap<String, Object> multipartRequest = new LinkedMultiValueMap<>();
multipartRequest.add("file", file);
HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(multipartRequest, header);
template.postForObject("http://localhost:8080/convert", requestEntity, String.class);
}
I if call FileUploadController (via postman) it works. If if call FileController
i get this Exception
"exception":
"org.springframework.http.converter.HttpMessageNotWritableException",
"message": "Could not write request: no suitable HttpMessageConverter found for request type [org.springframework.web.multipart.support.StandardMultipartHttpServletRequest$StandardMultipartFile]",
"path": "/convert"
Take a look at the answer here, it should be exactly what you are looking for: Attempting to test rest service with multipart file
The issue there is about posting a multi-part file to a rest service using a RestTemplate.
Basically, what you have to do is to simulate a file upload. You can try something like this:
MultiValueMap<String, Object> parameters = new LinkedMultiValueMap<String, Object>();
parameters.add("file", new FileSystemResource("file.jpg"));
HttpHeaders headers = new HttpHeaders();
headers.set("Content-Type", "multipart/form-data");
headers.set("Accept", "text/plain");
String result = restTemplate.postForObject(
"http://host:port/path",
new HttpEntity<MultiValueMap<String, Object>>(parameters, headers),
String.class);

Integration test with TestRestTemplate for Multipart POST request returns 400

I know that similar question has been here already couple of times but following suggested fixes did not solve my problem.
I have a simple controller with the following endpoint:
#RequestMapping(method = RequestMethod.POST)
public ResponseEntity<String> singleFileUpload(#RequestParam("file") MultipartFile file) {
log.debug("Upload controller - POST: {}", file.getOriginalFilename());
// do something
}
I am trying to write an integration test for it using Spring TestRestTemplate but all of my attemps end with 400 - Bad Request (no logs clarifying what went wrong in console).
The log inside the controller did not get hit so it failed before getting there.
Could you please take a look on my test and suggest what am I doing wrong?
#Test
public void testUpload() {
// simulate multipartfile upload
ClassLoader classLoader = getClass().getClassLoader();
File file = new File(classLoader.getResource("image.jpg").getFile());
MultiValueMap<String, Object> parameters = new LinkedMultiValueMap<String, Object>();
parameters.add("file", file);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
HttpEntity<MultiValueMap<String, Object>> entity = new HttpEntity<MultiValueMap<String, Object>>(parameters, headers);
ResponseEntity<String> response = testRestTemplate.exchange(UPLOAD, HttpMethod.POST, entity, String.class, "");
// Expect Ok
assertThat(response.getStatusCode(), is(HttpStatus.OK));
}
I tried the following:
#Test
public void testUpload() {
LinkedMultiValueMap<String, Object> parameters = new LinkedMultiValueMap<String, Object>();
parameters.add("file", new org.springframework.core.io.ClassPathResource("image.jpg"));
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
HttpEntity<LinkedMultiValueMap<String, Object>> entity = new HttpEntity<LinkedMultiValueMap<String, Object>>(parameters, headers);
ResponseEntity<String> response = testRestTemplate.exchange(UPLOAD, HttpMethod.POST, entity, String.class, "");
// Expect Ok
assertThat(response.getStatusCode(), is(HttpStatus.OK));
}
As you can see I used the org.springframework.core.io.ClassPathResource as object for the file and ti worked like a charm
I hope it's useful
Angelo
FileSystemResource also could be used in case if you want to use java.nio.file.Path.
Package: org.springframework.core.io.FileSystemResource
For example, you could do this:
new FileSystemResource(Path.of("src", "test", "resources", "image.jpg"))
Full code example:
#SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class UploadFilesTest {
private final TestRestTemplate template;
#Autowired
public UploadFilesTest(TestRestTemplate template) {
this.template = template;
}
#Test
public void uploadFileTest() {
var multipart = new LinkedMultiValueMap<>();
multipart.add("file", file());
final ResponseEntity<String> post = template.postForEntity("/upload", new HttpEntity<>(multipart, headers()), String.class);
assertEquals(HttpStatus.OK, post.getStatusCode());
}
private HttpHeaders headers() {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
return headers;
}
private FileSystemResource file() {
return new FileSystemResource(Path.of("src", "test", "resources", "image.jpg"));
}
}
Rest controller:
#RestController
public class UploadEndpoint {
#PostMapping("/upload")
public void uploadFile(#RequestParam("file") MultipartFile file) {
System.out.println(file.getSize());
}
}

RestTemplate send file as bytes from one controller to another

assume we have a one controller on third party service which accepts multipart files and its code is like (assume it's running on localhost:9090)
#RequestMapping("/file")
#RestController
public class FileController {
#RequestMapping(value = "/load", method = RequestMethod.POST)
public String getFile(#RequestPart("file") MultipartFile file){
return file.getName();
}
}
The question is:
How write a correct code in my controller, with RestTemplate, that calls the third party service, with file in body?
A few examples that do not work:
First one:
#RequestMapping("/file")
#RestController
public class FileSendController {
private RestTemplate restTemplate = new RestTemplate();
#RequestMapping(value = "/send", method = RequestMethod.POST)
public ResponseEntity<?> sendFile(#RequestPart MultipartFile file)
throws IOException {
String url = "http://localhost:9090/file/load";
return restTemplate.postForEntity(url, file.getBytes(),
ResponseEntity.class);
}
}
Second one:
#RequestMapping("/file")
#RestController
public class FileSendController {
private RestTemplate restTemplate = new RestTemplate();
#RequestMapping(value = "/send", method = RequestMethod.POST)
public ResponseEntity<?> sendFile(#RequestPart MultipartFile file)
throws IOException {
String url = "http://localhost:9090/file/load";
byte[] bytes = file.getBytes();
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
HttpEntity<byte[]> entity = new HttpEntity<>(bytes, headers);
return restTemplate.exchange(url, HttpMethod.POST,
entity,ResponseEntity.class);
}
}
One restriction: i should load files from memory, so it forces me to use byte[]
All of this examples throw 500 on third party service with message:
org.springframework.web.multipart.MultipartException: Current request is not
a multipart request.
Thanks for your advices.
Try this:
MultiValueMap<String, Object> data = new LinkedMultiValueMap<String, Object>();
ByteArrayResource resource = new ByteArrayResource(file.getBytes()) {
#Override
public String getFilename() {
return file.getName();
}
};
data.add("file", resource);
HttpHeaders requestHeaders = new HttpHeaders();
requestHeaders.setContentType(MediaType.MULTIPART_FORM_DATA);
HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<MultiValueMap<String, Object>>(data, requestHeaders);
final ResponseEntity<Response<ImportDto>> responseEntity = restTemplate.exchange(url,
HttpMethod.POST, requestEntity, new ParameterizedTypeReference<Response<ResponseDto>>(){});

#ControllerAdvice is not returning json response

Whenever my controller is throwing the exception, it is not not returning the response in json format.
It is giving Whitelabel Error Page
This application has no explicit mapping for /error, so you are seeing this as a fallback.
My controller is:
#RequestMapping(value = GENERATE_IMAGE, method = RequestMethod.GET, produces = MediaType.IMAGE_PNG_VALUE)
public byte[] get(#PathVariable("input") String input) throws IOException {
if (true) {
throw new BusinessValidationFailureException("sdfsdfsd");
}
ByteArrayInputStream inputStream = service.generate(input);
return ByteStreams.toByteArray(inputStream);
}
And my controller advice is:
#ControllerAdvice
public class WickesGlobalExceptionMapper extends ResponseEntityExceptionHandler {
#ExceptionHandler(BusinessValidationFailureException.class)
#ResponseBody
public ResponseEntity handleBusinessException(BusinessValidationFailureException ex, WebRequest request) {
ErrorResource error = new ErrorResource("InvalidRequest", ex.getMessage(), null);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
return handleExceptionInternal(ex, error, headers, HttpStatus.UNPROCESSABLE_ENTITY, request);
}
}
I tried few other possibility but I got the same error page all the time.
I get below exception in the server:
org.springframework.web.HttpMediaTypeNotAcceptableException: Could not find acceptable representation
at org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor.writeWithMessageConverters(AbstractMessageConverterMethodProcessor.java:259) ~[spring-webmvc-4.3.7.RELEASE.jar:4.3.7.RELEASE]
at org.springframework.web.servlet.mvc.method.annotation.HttpEntityMethodProcessor.handleReturnValue(HttpEntityMethodProcessor.java:203) ~[spring-webmvc-4.3.7.RELEASE.jar:4.3.7.RELEASE]
at org.springframework.web.method.support.HandlerMethodReturnValueHandlerComposite.handleReturnValue(HandlerMethodReturnValueHandlerComposite.java:81) ~[spring-web-4.3.7.RELEASE.jar:4.3.7.RELEASE]
at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:132) ~[spring-webmvc-4.3.7.RELEASE.jar:4.3.7.RELEASE]
at org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver.doResolveHandlerMethodException(ExceptionHandlerExceptionResolver.java:384) ~[spring-webmvc-4.3.7.RELEASE.jar:4.3.7.RELEASE]
at org.springframework.web.servlet.handler.AbstractHandlerMethodExceptionResolver.doResolveException(AbstractHandlerMethodExceptionResolver.java:59) [spring-webmvc-4.3.7.RELEASE.jar:4.3.7.RELEASE]
at org.springframework.web.servlet.handler.AbstractHandlerExceptionResolver.resolveException(AbstractHandlerExceptionResolver.java:136) [spring-webmvc-4.3.7.RELEASE.jar:4.3.7.RELEASE]
at org.springframework.web.servlet.handler.HandlerExceptionResolverComposite.resolveException(HandlerExceptionResolverComposite.java:74) [spring-webmvc-4.3.7.RELEASE.jar:4.3.7.RELEASE]
You did not show the handleExceptionInternal method implementation, this does return an object?
Edit: I was wrong about the #RestControllerAdvice, removed it from the answer.
Let's assume you want to return an object of type ErrorData (this is not clear from your post). So you could use:
#ExceptionHandler(BusinessValidationFailureException.class)
#ResponseBody
public ResponseEntity<ErrorData> handleBusinessException(BusinessValidationFailureException e) {
ErrorData errorData = createYourErrorData();
return new ResponseEntity(errorData, HttpStatus.UNPROCESSABLE_ENTITY);
}
I just tried this and got the correct stataus code back together with the data object in JSON.
Since controller produces MediaType.IMAGE_PNG_VALUE, controlleradvice should also produce the same mediatype. I removed produces from requestmapping and set the content type in the response, and it worked.
#RequestMapping(value = GENERATE_BARCODE, method = RequestMethod.GET)
public ResponseEntity get(#PathVariable("input") String barcodeInput) throws IOException {
byte[] resonse = service.generate(barcodeInput);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.IMAGE_PNG);
return new ResponseEntity(resonse, headers, HttpStatus.OK);
}

Categories