spring boot controller null multipartfile list request part - spring-boot

updating spring-boot from 2.3.7 to 2.4.6 and encountering NPE issue with requestPart in rest controller. Essentially in 2.3.7 the list of multipartfiles was initialising as an empty list. And with 2.4.6 the list is now a null object. not sure if it's a legitimate change in function or a bug? or possible issue with our code?
public Callable<ResponseEntity<EmailResponse>> process(
#RequestHeader final HttpHeaders requestHeaders,
#RequestPart(name = "email") #Valid EmailRequest email,
#RequestPart(name = "attachments", required = false) List<MultipartFile> attachments,
final HttpServletRequest request) {
...
}

Related

java.lang.AssertionError: Status : 404

Please Could someone help me , I cant figure out what is the problem, I'am trying to implement a test to this method but it always gives me
java.lang.AssertionError: Status
Expected :200
Actual :400
#PutMapping("/infoUtile/update/{id}")
public Map<String,Object> editActualite(#PathVariable Long id, #Valid #RequestParam ArrayList<Long> idDeleted,
#Valid #RequestParam String content, #Valid #RequestParam String description){
InformationUtile info = this.infoUtileService.getInfoUtileById(id);
info.setContent(content);
info.setDescription(description);
info.setDate(new Date());
if(idDeleted.size() != 0) {
for (int i = 0; i < idDeleted.size(); i++) {
this.mediaService.deleteMedia(idDeleted.get(i));
}
}
InformationUtile i = this.infoUtileService.addOrEditInfoUtile(info);
return getInfoUtileWeb(i);
}
and here is my test that Im trying to implement
#Test
public void update() throws Exception {
InformationUtile informationUtile = new InformationUtile();
informationUtile.setId(1);
informationUtile.setContent("oumaima");
informationUtile.setDescription("test");
Media medias = new Media();
medias.setId(1);
medias.setType("image/png");
medias.setUrl("C:\\files\\actualite\\32769\\adobexd.png");
List<Media> allMedias = new ArrayList<Media>();
allMedias.add(medias);
informationUtile.setMedias(allMedias);
User user = new User();
user.setId(1);
user.setNom("oumaima");
informationUtile.setUser(user);
ArrayList<Long> idDeleted = new ArrayList<>();
idDeleted.add(0L);
Mockito.when(informationUtileService.getInfoUtileById(Mockito.<Long>any())).thenReturn(new InformationUtile());
Mockito.when(informationUtileService.addOrEditInfoUtile(Mockito.any(InformationUtile .class))).thenReturn(informationUtile);
mockMvc.perform(put("/infoUtile/update/{id}",informationUtile.getId()).requestAttr("idDeleted",idDeleted)
.param("content",informationUtile.getContent())
.param("description",informationUtile.getDescription())
)
.andExpect(status().isOk());
verify(informationUtileService, times(1)).getInfoUtileById(informationUtile.getId());
verify(informationUtileService, times(1)).addOrEditInfoUtile(informationUtile);
verifyNoMoreInteractions(informationUtileService);
}
You are defining three request parameters at your endpoint #Valid #RequestParam ArrayList<Long> idDeleted, #Valid #RequestParam String content, #Valid #RequestParam String description which means they are query parameters after the url, e.g. http://localhost:8080/?idDeleted=1&idDeleted=2&content=Hello&description=Duke.
The HTTP 404 indicates that Spring could not find a handler for your request, meaning the client (in your case MockMvc) has a malformed URL.
In your current MockMvc request setup you are using .requestAttr() for the idDeleted request parameter.
All of them should be .param():
mockMvc
.perform(put("/infoUtile/update/{id}",informationUtile.getId())
.param("idDeleted", idDeletedOne , idDeletedTwo)
.param("content",informationUtile.getContent())
.param("description",informationUtile.getDescription())
)
PS: I guess the #Valid annotations are redundant/not needed here as you are not checking e.g. payload which has Bean Validation annotations to verify the content.
UPDATE: .param() is overloaded with .parm(String name, String... values), so you can pass your list of idDeleted with either .param("idDeleted", idDeletedOne, idDeletedTwo) or you can pass a String[] with all your Long values represented as a String

Spring Boot MVC to allow any kind of content-type in controller

I have a RestController that multiple partners use to send XML requests. However this is a legacy system that it was passed on to me and the original implementation was done in a very loose way in PHP.
This has allowed to clients, that now they refuse to change, to send different content-types (application/xml, text/xml, application/x-www-form-urlencoded) and it has left me with the need to support many MediaTypes to avoid returning 415 MediaType Not Supported Errors.
I have used the following code in a configuration class to allow many media types.
#Bean
public MarshallingHttpMessageConverter marshallingMessageConverter() {
MarshallingHttpMessageConverter converter = new MarshallingHttpMessageConverter();
converter.setMarshaller(jaxbMarshaller());
converter.setUnmarshaller(jaxbMarshaller());
converter.setSupportedMediaTypes(Arrays.asList(MediaType.APPLICATION_OCTET_STREAM, MediaType.APPLICATION_XML,
MediaType.TEXT_XML, MediaType.TEXT_PLAIN, MediaType.APPLICATION_FORM_URLENCODED, MediaType.ALL));
return converter;
}
#Bean
public Jaxb2Marshaller jaxbMarshaller() {
Jaxb2Marshaller marshaller = new Jaxb2Marshaller();
marshaller.setClassesToBeBound(CouponIssuedStatusDTO.class, CouponIssuedFailedDTO.class,
CouponIssuedSuccessDTO.class, RedemptionSuccessResultDTO.class, RedemptionResultHeaderDTO.class,
RedemptionFailResultDTO.class, RedemptionResultBodyDTO.class, RedemptionDTO.class, Param.class,
ChannelDTO.class, RedeemRequest.class);
Map<String, Object> props = new HashMap<>();
props.put(javax.xml.bind.Marshaller.JAXB_FORMATTED_OUTPUT, true);
marshaller.setMarshallerProperties(props);
return marshaller;
}
The controller method is this:
#PostMapping(value = "/request", produces = { "application/xml;charset=UTF-8" }, consumes = MediaType.ALL_VALUE)
public ResponseEntity<RedemptionResultDTO> request(
#RequestHeader(name = "Content-Type", required = false) String contentType,
#RequestBody String redeemRequest) {
return requestCustom(contentType, redeemRequest);
}
This endpoint is hit by all clients. It is only one last client giving me trouble. They are sending content-type = application/x-www-form-urlencoded; charset=65001 (UTF-8)": 65001 (UTF-8)
Due to the way the charset is sent, Spring Boot refuses to return anything but 415. Not even MediaType.ALL seems to have any effect.
Is there a way to make Spring allow this to reach me ignoring the content-type? Creating a filter and changing the content type was not feasible since the HttpServletRequest is not allowing to mutate the content-type. I am out of ideas but I really think there has to be a way to allow custom content-types.
UPDATE
If I remove the #RequestBody then I don't get the error 415 but I have no way to get the request body since the HttpServletRequest reaches the Controller action empty.
You best case is to remove the consumes argument from the RequestMapping constructor. The moment you have it added, spring will try to parse it into known type MediaType.parseMediaType(request.getContentType()) & which tries to create a new MimeType(type, subtype, parameters) and thus throws exception due to invalid charset format being passed.
However, if you remove the consumes, and you wanna validate/restrict the incoming Content-Type to certain type, you can inject HttpServletRequest in your method as parameter, and then check the value of request.getHeader(HttpHeaders.CONTENT_TYPE).
You also have to remove the #RequestBody annotation so Spring doesn't attempt to parse the content-type in attempt to unmarshall the body. If you directly attempt to read the request.getInputStream() or request.getReader() here, you will see null as the stream has already been read by Spring. So to get access to input content, use spring's ContentCachingRequestWrapper inject using Filter and then you can later repeatedly read the content as it's cached & not reading from original stream.
I am including some code snippet here for reference, however to see executable example, you can refer my github repo. Its a spring-boot project with maven, once you launch it, you can send your post request to http://localhost:3007/badmedia & it will reflect you back in response request content-type & body. Hope this helps.
#RestController
public class BadMediaController {
#PostMapping("/badmedia")
#ResponseBody
public Object reflect(HttpServletRequest request) throws IOException {
ObjectMapper mapper = new ObjectMapper();
JsonNode rootNode = mapper.createObjectNode();
((ObjectNode) rootNode).put("contentType", request.getHeader(HttpHeaders.CONTENT_TYPE));
String body = new String(((ContentCachingRequestWrapper) request).getContentAsByteArray(), StandardCharsets.UTF_8);
body = URLDecoder.decode(body, StandardCharsets.UTF_8.name());
((ObjectNode) rootNode).put("body", body);
return mapper.writerWithDefaultPrettyPrinter().writeValueAsString(rootNode);
}
}
#Component
public class CacheRequestFilter extends GenericFilterBean {
#Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest cachedRequest
= new ContentCachingRequestWrapper((HttpServletRequest) servletRequest);
//invoke caching
cachedRequest.getParameterMap();
chain.doFilter(cachedRequest, servletResponse);
}
}

Spring boot large file upload and download support

I have a spring boot web application which will handle large file (max size of 5g) upload and then save it to s3. The request and response may last for a long time.
So what is the best practice to handle the upload and download like this? How to make a good performance to prevent my server down when download or upload large files?
you can use multipart/form-data
#RequestMapping(value = "/agency/create", method = RequestMethod.POST, consumes = "multipart/form-data")
public ResponseEntity<List<String>> createAgency(
#RequestParam(value = "username", required = true) String username,
#RequestParam(value = "pic1", required = true)MultipartFile pic1File,
MultipartHttpServletRequest request, ModelAndView modelAndView) {
List<String> requestKeys=new ArrayList<String>();
List<String> originalFileName=new ArrayList<String>();
request.getFileNames().forEachRemaining(requestKeys::add);
for(String multipartFile:requestKeys) {
originalFileName.add(request.getFile(multipartFile).getOriginalFilename());
}
storageService.store(pic1File);
return new ResponseEntity<List<String>>(originalFileName, HttpStatus.CREATED);
}
Posting in case someone finds this useful in the future. This works with a REST controller as of Spring Boot 2.4.2.
Class annotations:
#RestController
#RequestMapping("/api")
Method declaration:
#RequestMapping(path = "/file-upload/{depot}/{fileName}", method = {RequestMethod.POST, RequestMethod.PUT})
public ResponseEntity<String> fileUpload(
#PathVariable(name = "depot") String depot,
#PathVariable(name = "fileName") String fileName,
InputStream inputStream,
HttpServletRequest request,
HttpServletResponse response)
The above is the Spring Boot configuration for a REST Controller that worked for me for large file upload. The key was adding InputStream inputStream directly.

How to send Multipart form data and upload PDF with RestTemplate Spring Boot

Good day Pals,
In my microservice and spring-boot app, I have a frontend employee microservice which consumes another microservice with file upload endpoint.
The calling service is based on spring rest controller and I am trying to consume a File-Upload endpoint using RestTemplate in a Spring Boot application. In a nutshell, trying to upload a PDF file.
I have explored the following SO post, but its not working for me:
jackson disable fail_on_empty_beans
I am testing this in postman and getting the following error:
org.springframework.http.converter.HttpMessageNotWritableException: Could not write content: No serializer found for class java.io.FileDescriptor and no properties discovered to create BeanSerializer.
Any help will be appreciated pleasee ....
Below are the main components -
Rest Controller ############
#RestController 
#RequestMapping(path = “/employee”) 
public class EmployeeController {
private EmployeeService empService;
#RequestMapping(value =“/emp/load”, method = RequestMethod.POST)
public
#ResponseBody
ResponseEntity<byte[]> handleFileUpload(
 #RequestParam("file") MultipartFile file, #RequestParam String a, #RequestParam String b, #RequestParam String c, #RequestParam String d, #RequestParam String e) throws Exception {
return empService.handleFileUpload(file, a, b, c, d, e);
}
}
The service Implementation
#Service
public class EmployeeServiceImpl implements EmployeeService{
 
 #Value(“${emp.base.url}")
private String EMP_BASE_URI;
public ResponseEntity<byte[]>handleFileUpload(MultipartFile file, String a, String b, String c, String d, String e) {
final String uri = EMP_BASE_URI + "/upload";
RestTemplate restTemplate = getMappedRestTemplate();
MultiValueMap<String, Object> params = new LinkedMultiValueMap<String, Object>();
params.add("file", file);
params.add(“a”, a);

params.add(“b”, b);
params.add(“c”, c);

params.add(“d”, d);
params.add(“e”, e);
HttpHeaders headers = new HttpHeaders();
headers.set("Content-Type", "multipart/form-data");
ResponseEntity<byte[]> response = restTemplate.exchange(uri, HttpMethod.POST, new HttpEntity<>(params, headers), byte[].class);
return new ResponseEntity<>(response.getBody(), response.getStatusCode());
} 
 
 
 
private RestTemplate getMappedRestTemplate(){
RestTemplate restTemplate = new RestTemplate();
ObjectMapper newObjectMapper = new ObjectMapper();

 newObjectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS,false);

newObjectMapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
MappingJackson2HttpMessageConverter mappingJacksonHttpMessageConverter=new MappingJackson2HttpMessageConverter();
FormHttpMessageConverter formConvertor = new FormHttpMessageConverter();

restTemplate.getMessageConverters().add(formConvertor);
restTemplate.getMessageConverters().add(mappingJacksonHttpMessageConverter);
restTemplate.getMessageConverters().add(new StringHttpMessageConverter());

return restTemplate;
}
}
I am getting the following error:
Failed to write HTTP message: org.springframework.http.converter.HttpMessageNotWritableException: Could not write content: 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: org.springframework.web.multipart.support.StandardMultipartFile["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: org.springframework.web.multipart.support.StandardMultipartFile["inputStream"]->java.io.FileInputStream["fd"])
Please, any help with be appreciated.
I have been stuck on this all day.
I have gone for sending the params (including the pdf file as a byte stream i.e. byte[]) as json in the request body using the following method signature:
#RequestMapping(value = "/upload", method = RequestMethod.POST)
#ResponseStatus(HttpStatus.CREATED)
public #ResponseBody Long handleFileUpload(#Valid #RequestBody Invoice uploadedInvoice){
...
}

MocMVC giving HttpMessageNotReadableException

I'm still learning my way around testing and I'm trying to get a MockMvc test to work for me. It's a simple REST controller that at this point is only doing some authentication using information from json in the post. I've actually implemented the code, so I know it's working because I get back both the correct response with the correct input and the error messages I've put together, both in a json format. My problem is that the test keeps failing with a HttpMessageNotReadableException, even though the actual code works, so I'm assuming I don't have my test set up right. Any help you guys can give would be great.
Here's my controller
#Controller
public class RequestPaymentController {
protected final Log logger = LogFactory.getLog(getClass());
private PaymentService paymentService;
private LoginService loginService;
#Autowired
public void setPaymentService(PaymentService paymentService){
this.paymentService = paymentService;
}
#Autowired
public void setLoginService(LoginService loginService){
this.loginService = loginService;
}
#RequestMapping(value = "/requestpayment", method = RequestMethod.POST, headers="Accept=application/json")
#ResponseBody
public ResponseEntity<PaymentResult> handleRequestPayment(#RequestBody PaymentRequest paymentRequest, HttpServletRequest request, HttpServletResponse response, BindingResult result) throws Exception{
ResponseEntity<PaymentResult> responseEntity = null;
new LoginValidator().validate(paymentRequest, result);
boolean valid = loginService.isLoginValid(paymentRequest, result);
if (valid){
responseEntity = setValidResponse(paymentRequest);
}else {
throw new TumsException("exception message");
}
return responseEntity;
}
private ResponseEntity<PaymentResult> setValidResponse(PaymentRequest paymentRequest){
PaymentResult paymentResult = paymentService.getResults(paymentRequest);
return new ResponseEntity<PaymentResult>(paymentResult, HttpStatus.OK);
}
}
And here's my test code:
public class RequestPaymentControllerTest {
PaymentService mockPaymentService;
RequestPaymentController requestPaymentController;
HttpServletRequest mockHttpServletRequest;
HttpServletResponse mockHttpServletResponse;
PaymentRequest mockPaymentRequest;
BindingResult mockBindingResult;
LoginService mockLoginService;
PaymentResult mockPaymentResult;
MockMvc mockMvc;
#Before
public void setUp() throws Exception {
mockPaymentService = createMock(PaymentService.class);
mockHttpServletRequest = createMock(HttpServletRequest.class);
mockHttpServletResponse = createMock(HttpServletResponse.class);
mockPaymentRequest = createMock(PaymentRequest.class);
requestPaymentController = new RequestPaymentController();
mockBindingResult = createMock(BindingResult.class);
mockLoginService = createMock(LoginService.class);
requestPaymentController.setPaymentService(mockPaymentService);
mockPaymentResult = createMock(PaymentResult.class);
mockMvc = MockMvcBuilders.standaloneSetup(new RequestPaymentController()).build();
}
#After
public void tearDown() throws Exception {
mockPaymentService = null;
mockHttpServletRequest = null;
mockHttpServletResponse = null;
mockPaymentRequest = null;
requestPaymentController = null;
mockBindingResult = null;
mockLoginService = null;
mockPaymentResult = null;
mockMvc = null;
}
#Test
public void testHandleRequestPayment() throws Exception{
initializeStateForHandleRequestPayment();
createExpectationsForHandleRequestPayment();
replayAndVerifyExpectationsForHandleRequestPayment();
}
private void initializeStateForHandleRequestPayment(){
}
private void createExpectationsForHandleRequestPayment(){
mockPaymentRequest.getServiceUsername();
expectLastCall().andReturn("testuser");
mockPaymentRequest.getServicePassword();
expectLastCall().andReturn("password1!");
mockLoginService.isLoginValid(mockPaymentRequest,mockBindingResult);
expectLastCall().andReturn(true);
mockPaymentService.getResults(mockPaymentRequest);
expectLastCall().andReturn(mockPaymentResult);
}
private void replayAndVerifyExpectationsForHandleRequestPayment() throws Exception{
replay(mockPaymentService, mockBindingResult, mockHttpServletRequest, mockHttpServletResponse, mockPaymentRequest, mockLoginService);
requestPaymentController.setLoginService(mockLoginService);
requestPaymentController.handleRequestPayment(mockPaymentRequest, mockHttpServletRequest, mockHttpServletResponse, mockBindingResult);
mockMvc.perform(post("/requestpayment")
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON))
.andDo(print())
.andExpect(status().isBadRequest());
verify(mockPaymentService, mockBindingResult, mockHttpServletRequest, mockHttpServletResponse, mockPaymentRequest, mockLoginService);
}
}
The results of the andDo(print()) are:
MockHttpServletRequest:
HTTP Method = POST
Request URI = /requestpayment
Parameters = {}
Headers = {Content-Type=[application/json], Accept=[application/json]}
Handler:
Type = portal.echecks.controller.RequestPaymentController
Method = public org.springframework.http.ResponseEntity<portal.echecks.model.PaymentResult> portal.echecks.controller.RequestPaymentController.handleRequestPayment(portal.echecks.model.PaymentRequest,javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse,org.springframework.validation.BindingResult) throws java.lang.Exception
Resolved Exception:
Type = org.springframework.http.converter.HttpMessageNotReadableException
ModelAndView:
View name = null
View = null
Model = null
FlashMap:
MockHttpServletResponse:
Status = 400
Error message = null
Headers = {}
Content type = null
Body =
Forwarded URL = null
Redirected URL = null
Cookies = []
Process finished with exit code 0
As you can see, the test passes when I'm expecting a bad request status, but I've put in logging and I know that the ResponseBody I'm sending back has a 200 status. Like I said, this is my first time with MockMvc, so I assume I've not set something up right. Any suggestions?
An HttpMessageNotReadableException is
Thrown by HttpMessageConverter implementations when the read method
fails.
You also get a 400 Bad Request in your response. This should all tell you that you are not sending what your server is expecting. What is your server expecting?
#RequestMapping(value = "/requestpayment", method = RequestMethod.POST, headers="Accept=application/json")
#ResponseBody
public ResponseEntity<PaymentResult> handleRequestPayment(#RequestBody PaymentRequest paymentRequest, HttpServletRequest request, HttpServletResponse response, BindingResult result) throws Exception{
The main thing here is the #RequestBody annotated parameter. So you are telling your server to try and deserialize a PaymentRequest instance from the body of the HTTP POST request.
So let's see the request you are making
mockMvc.perform(post("/requestpayment")
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON))
.andDo(print())
.andExpect(status().isBadRequest());
I don't see you providing a body to the request. There should be a content(String) call somewhere in there to set the content of the POST request. This content should be a JSON serialization of a PaymentRequest.
Note that because you are using the StandaloneMockMvcBuilder, you might need to set the HttpMessageConverter instances yourself, ie. a MappingJackson2HttpMessageConverter to serialize and deserialize JSON.
Note that the BindingResult parameter should come immediately after the parameter to which it's related. Like so
#RequestMapping(value = "/requestpayment", method = RequestMethod.POST, headers="Accept=application/json")
#ResponseBody
public ResponseEntity<PaymentResult> handleRequestPayment(#Valid #RequestBody PaymentRequest paymentRequest, BindingResult result, HttpServletRequest request, HttpServletResponse response) throws Exception{
Don't forget the #Valid.
Note that this
requestPaymentController.setLoginService(mockLoginService);
requestPaymentController.handleRequestPayment(mockPaymentRequest, mockHttpServletRequest, mockHttpServletResponse, mockBindingResult);
is completely unrelated to the MockMvc test you are doing.
In my case, as sprint mvc w/ jackson (jackson-mapper-asl, v-1.9.10) deserialization requires JSON parser. And jackson requires a default constructor for http request message deserialization, if there's no default constructor, jackson will have a problem w/ reflection and throws HttpMessageNotReadableException exception.
This is to say, all the classes/sub-classes which used as Request body, (in this case) requires a default constructor. This costed me a few moments after I tried adding custom converter and other suggestions I got in stackoverflow in vain.
Or you can add Custom Deserializer or Mixin annotation to avoid adding default constructor hierachically everywhere. as described here: http://blogs.jbisht.com/blogs/2016/09/12/Deserialize-json-with-Java-parameterized-constructor. Check this if you're interested.
Seems duplicated here > Spring HttpMessageNotReadableException.
Make sure of the following:
return object implements Serializable
#ResponseBody annotation used on the controller method
On your unit test
#ExtendWith(SpringExtension.class)
#ContextConfiguration(classes = {....})
#WebMvcTest
#AutoConfigureMockMvc
Probably too late to answer but just in case someone is still looking at this page.
As #Sotirios Delimanolis mentions, the problem is due to a bad request - a '#RequestBody' is specified in the parameter but never supplied in the request body. So, if you add that to request using 'content(someRequestString)' as below, it should work.
PaymentRequest paymentRequest = new PaymentRequest(...);
String requestBody = new ObjectMapper().valueToTree(paymentRequest).toString();
mockMvc.perform(post("/requestpayment")
.content(requestBody)
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.status").value("SUCCESS"))
.andExpect(jsonPath("$.paymentAmount", is(20)));
jsonPath may be used to verify the attributes on the response. In the above example, say PaymentResponse has attributes status and paymentAmount in the json response. These parts can be verified easily.
You may run into errors like -
NoClassDefFoundError: com/jayway/jsonpath/Predicate
while using jsonPath. So, make sure it is added to classpath explicitly as it is an optional dependency in spring-test and will not be available transitively. If using maven, do this:
<dependency>
<groupId>com.jayway.jsonpath</groupId>
<artifactId>json-path</artifactId>
<version>2.4.0</version>
<scope>test</scope>
</dependency>

Resources