I have written a method with get request mapping it gives list of users. As jakson binding dependency is there is gives response in JSON. I've also dependency for XML which is Jackson Dataformat XML.So, if Accept is application/json it returns the response in JSON and if it is in application/xml it returns in XML.But by default it gives JSON response. SO, I wanted to add Accept header if not present and make it's default value as application/xml.
#GetMapping(path="/getAll")
public List<User> getUsers(#RequestHeader(value= "Accept" ,required=false, defaultValue="application/xml") String Accept)
{
return service.findAll();
}
But in above case, the header is not setting.
In order to do so, you need to modify your controller method to return ResponseEntity<List<User>> as following:
#GetMapping(path="/getAll")
public ResponseEntity<List<User>> getUsers(#RequestHeader(value= "Accept" ,required=false, defaultValue="application/xml") String Accept) {
HttpHeaders responseHeaders = new HttpHeaders();
responseHeaders.setLocation(location);
responseHeaders.set("Accept", "Value");
return new ResponseEntity<List<User>>(service.findAll(), responseHeaders, HttpStatus.CREATED);
}
If you just want to respond XML from your spring boot application, use the following custom webMvcConfiguration. Setting a default Accept header just to respond XML does not seem like a good idea.
#Configuration
public class WebMvcConfiguration {
#Bean
public WebMvcConfigurer myWebMvcConfigurer() {
return new WebMvcConfigurerAdapter() {
#Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
configurer.defaultContentType(MediaType.APPLICATION_XML);
}
};
}
}
if you are already using a custom WebMvcConfigurerAdapter, just override the configureContentNegotiation(...) method as above.
Related
This is related to an existing spring boot question raised by me(Request Body is not properly encoded and hidden when using spring form encoder in Feign Client).
According to this question, we can add either content type in headers or add during request mapping itself as consumes.
So what I did was added content type in headers in the client configuration class
public class EmailClientConfiguration {
#Bean
public RequestInterceptor requestInterceptor(Account<Account> account) {
return template -> {
template.header("Content-Type", "application/x-www-form-urlencoded");
};
}
#Bean
public OkHttpClient client() {
return new OkHttpClient();
}
#Bean
Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
#Bean
public Decoder feignDecoder() {
return new JacksonDecoder();
}
#Bean
public Encoder feignFormEncoder () {
return new SpringFormEncoder(new JacksonEncoder());
}
}
and I see in the headers the content type is correctly set as application/x-www-form-urlencoded when the request is sent. But the request body is still sent in json format and also not hidden.
Request Body:
Map<String, String> requestBody = new HashMap<>();
requestBody.put("username", "xyz");
requestBody.put("email", "xyz#gmail.com");
requestBody.put("key", "xxx");
Request Body received in server end:
{"{\n \"key\" : \"xxx\",\n \"email\" : \"xyz#gmail.com\",\n \"username\" : \"xyz\"\n}"
When I add consumes in my request mapping as application/x-www-form-urlencoded
#FeignClient(name = "email", url = "localhost:3000",
configuration = EmailClientConfiguration.class)
public interface EmailClient {
#PostMapping(value = "/email/send", consumes = "application/x-www-form-urlencoded")
ResponseDto sendEmail(#RequestBody Map<String, String> requestBody);
}
it works fine(request body is hidden in server end and also properly encoded). And when I removed the header in the configuration class and adding only consumes works fine without no issues but the vice versa has this problem.
I searched in internet for this and couldn't find any answer.
Feign encodes the request body and parameters before passing the request to any RequestInterceptor (and rightly so). If you do not declare consumes = "application/x-www-form-urlencoded", SprinFormEncoder doesn't know that you're trying to send form data, so it delegates serialization to the inner JacksonEncoder which only does JSON (see for yourself by printing template.body() before setting the header).
Handling such a well-supported header in the interceptor doesn't seem like a good idea, when you already have consumes. If you insist on doing so, you have to provide your own encoder which doesn't rely on the header value and always outputs form-urlencoded data.
I've created a Restful service with Spring MVC as shown below. I called it using Postman. I placed a breakpoint on 'return "hello World"'. There's no hit on the breakpoint with the error message "Required request part 'file' is not present".
However, if I comment out the '#RequestParam("file")' annotation, the breakpoint is hit with the parameter "file" being null.
What could have gone wrong? Very puzzled.
#RestController
#RequestMapping("/dp")
public class DpWebService implements IDpWebService {
#Override
#Bean
public MultipartConfigElement multipartConfigElement() {
return new MultipartConfigElement("");
}
#Override
#Bean
public MultipartResolver multipartResolver() {
org.springframework.web.multipart.commons.CommonsMultipartResolver multipartResolver = new org.springframework.web.multipart.commons.CommonsMultipartResolver();
multipartResolver.setMaxUploadSize(1000000);
return multipartResolver;
}
#Override
#RequestMapping(path = "/send", method = RequestMethod.POST, consumes = "multipart/form-data")
public String sendManifest(#RequestParam("file") MultipartFile file) {
return "Hello World";
}
}
Postman
Postman Header
Check your POSTMAN request Configuration. I think you have not changed the input type to File from Text. Uploading images, check the images. Hover the mouse over that area in Postman and select File from the drop-down menu.
Having Beans defined in your RestController is not a good design. Please separate out a Configuration class with #Configuration annotation and define your beans. The reasons being: Single Responsibility Principle - each class should only do about one thing.
https://java-design-patterns.com/principles/#single-responsibility-principle
#RequestParam might not be working for you because of the nature of the data that is contained in the file that you are sending through the request. RequestParam is likely to be used with name-value form fields. For complex data like json/xml it is advisable to use #RequestPart instead.
Instead of the #RequestParam annotation use the #RequestPart annotation.
Annotation that can be used to associate the part of a
"multipart/form-data" request with a method argument.
Try using it like :
#RestController
#RequestMapping("/dp")
public class DpWebService implements IDpWebService {
#Override
#Bean
public MultipartConfigElement multipartConfigElement() {
return new MultipartConfigElement("");
}
#Override
#Bean
public MultipartResolver multipartResolver() {
org.springframework.web.multipart.commons.CommonsMultipartResolver multipartResolver = new org.springframework.web.multipart.commons.CommonsMultipartResolver();
multipartResolver.setMaxUploadSize(1000000);
return multipartResolver;
}
#Override
#RequestMapping(path = "/send", method = RequestMethod.POST, consumes = "multipart/form-data")
public String sendManifest(#RequestPart("file") MultipartFile file) {
return "Hello World";
}
}
Also make sure that the request from the postman is getting triggered correctly :
Remove any un wanted request params from postman.
Make sure that under 'Body' tab the form-data is selected. Also make
sure that when selected the file in the key the name is provided as
'file' and type is also selected as file instead of text.
This is my working example.
#PostMapping("/uploadFile")
public UploadFileResponse uploadFile(#RequestParam("file") MultipartFile file) {
String fileName = fileStorageService.storeFile(file);
String fileDownloadUri = ServletUriComponentsBuilder.fromCurrentContextPath().path("/downloadFile/")
.path(fileName).toUriString();
return new UploadFileResponse(fileName, fileDownloadUri, file.getContentType(), file.getSize());
}
application.properties
## MULTIPART (MultipartProperties)
# Enable multipart uploads
spring.servlet.multipart.enabled=true
# Threshold after which files are written to disk.
spring.servlet.multipart.file-size-threshold=2KB
# Max file size.
spring.servlet.multipart.max-file-size=200MB
# Max Request Size
spring.servlet.multipart.max-request-size=215MB
## File Storage Properties
# Please change this to the path where you want the uploaded files to be stored.
file.upload-dir=C://Users//abc//Documents//
I need to serve multiple file types using same endpoint ( zip,pdf,xml).
I needed to add error handling to those endpoints so in case of error they should return json (using controller advice) to indicate problem to user.
For example:
#GetMapping(value = "api/books", produces = {applicaton/zip, application/json}
public ResponseEntity<byte[]> getZipedBooks(){...}
#GetMapping(value = "api/books", produces = {applicaton/pdf, application/json}
public ResponseEntity<byte[]> getPdfBooks()(...}
Without application/json Spring was able to differentiate between those endpoints and call correct one based on accept header. But when I added json Spring is now throwing exception:
org.springframework.web.HttpMediaTypeNotAcceptableException: Could not find acceptable representation
even if it can be deduced from accept: application/json,application/pdf header that getPdfBooks should be called.
Is there any way to configure spring to work with multiple content types on the same endpoint or I need to make special endpoints for every file type ?
I would reconsider this approach. If you want to return JSON on error do with exception handling
So that you would add something like
private class ErrorResponse {
String message;
public ErrorResponse(String message) {
this.message = message;
}
public String getMessage() {
return message;
}
}
#ExceptionHandler(value = Exception.class)
public ResponseEntity<ErrorResponse> handleException(Exception e) {
return new ResponseEntity<ErrorResponse>(new ErrorResponse(e.getMessage()), HttpStatus.BAD_REQUEST);
}
I am receiving an xml post request from my vendor having a declaration of this format.
<?xml version=\"1.0\" encoding=\"utf-8\"?>
With this type of xml declarion (I am using Spring MVC with JAXB) I am getting the HTTP Status 400 error which states that "The request sent by the client was syntactically incorrect." I tried to post the same request to my site using postman and i get the very same error.
But on changing the xml declarion by removing all the backslashes( see below)
<?xml version="1.0" encoding="utf-8"?>
the error vanishes and i get the correct response with HTTP Status 200, Ok.
My question is how can i intercept this request and modify the xml declaration by removing the forward slashes (My vendor does not comply with modify this from their end).
Below is the sample of my controller
#RequestMapping(method = {RequestMethod.GET,RequestMethod.POST}, value ="/listeningurl", consumes = "application/xml", produces = "application/xml")
public ResponseObject lodgementNotifications(#RequestBody RequesObject reqObject)
{
//do stuffs with reqObject;
// Initialize ResponseObject
return responseObject
}
Thanks for the help.
You can extends the HandlerInterceptorAdapter which is :
Abstract adapter class for the AsyncHandlerInterceptor interface, for
simplified implementation of pre-only/post-only interceptors.
#Component
public class MyHandlerInterceptor extends HandlerInterceptorAdapter {
#Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// then access your request body (xml content) to update it
// see link bellow for how to retrieve an xml content from the HttpServletRequest
return super.preHandle(request, response, handler);
}
}
After that you override the addInteceptors of WebMvcConfigurerAdapter by creating a custom class that extends from WebMvcConfigurerAdapter :
#Configuration
public class CustomWebMvc extends WebMvcConfigurerAdapter {
#Autowired
MyHandlerInterceptor myHandlerInterceptor;
#Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(myHandlerInterceptor);
super.addInterceptors(registry);
}
}
To know how to retrieve the xml content from your HttpServletRequest read this Get Posted XML from HttpServletRequest Object
Edit
If your goal is simply to retrieve the http body ( xml content ) and then do what ever you want with it inside your controller you can simply inject an InputStream or a Reader in the #RequestMapping handler method (which is your controller method) like so :
#PostMapping(value = "/topics/xml", consumes = "application/xml")
public void getXmlRequest(InputStream data) throws IOException {
String requestBody = IOUtils.toString(data);
// format the xml and do something with it
}
Spring web doc : Handler Methods :
#RequestMapping handler methods have a flexible signature and can choose from a range of supported controller method arguments and return values.
I was able to resolve the issue. I had to receive the xml request in String format, removed the backslashes then unmarshalled it into its corresponding object. Thanks to #Harry Coder for the hints. Here is the solution that worked for me.
#RequestMapping(method = {RequestMethod.GET,RequestMethod.POST}, value ="/listeningurl", consumes = "application/xml", produces = "application/xml")
public ResponseObject lodgementNotifications(#RequestBody String reqObjectXMLStr)
{
//Replace the backslashes from the xml string
String cleanReqObjectXMLStr = reqObjectXMLStr.replaceAll("\\\\", "");
//Unmarshal the string into the corresponding object using JAXB library
RequestObject reqObject = JAXB.unmarshal(new StringReader(cleanReqObjectXMLStr), RequestObject.class);
//do stuffs with reqObject;
// Initialize and set ResponseObject
return responseObject
}
When posting an XML object Foo to /foo.xml I can't get path extension view resolution to kick in, but get the error
Unsupported content type: text/plain
Which is a result of not posting any Content-Type headers. But favorPathExtention should remove that need. Any idea why it doesn't?
Controller
#RequestMapping(value="/foo.xml", method=ADD, produces="application/xml")
#ResponseStatus(HttpStatus.OK)
public #ResponseBody Foo add(#RequestBody Foo foo) {
return foo;
}
Configuration
#Configuration
#ComponentScan(basePackages="my.pkg.controller")
public class RestWebConfig extends WebMvcConfigurationSupport {
#Override
protected void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(new MarshallingHttpMessageConverter(...));
converters.add(new MappingJackson2HttpMessageConverter());
}
#Override
protected void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
configurer.favorPathExtension(true)
.ignoreAcceptHeader(true)
.useJaf(false)
.mediaType("json", MediaType.APPLICATION_JSON)
.mediaType("xml", MediaType.APPLICATION_XML);
}
}
I think you've misunderstood the purpose of content negotiation.
Content negotiation is for how the response will be generated, not how the request will be parsed.
You get
Unsupported content type: text/plain
because, with #RequestBody, there are no registeredHttpMessageConverter instances that can read the default request content-type of application/octet-stream (or maybe your client uses text/plain). This all happens in the RequestResponseBodyMethodProcessor which handles generating an argument for the parameter annotated with #RequestBody.
If you're going to send XML or JSON in your request body, set the Content-Type.
As for the content negotiation, with your config and request, the DispatcherServlet will attempt to generate a response with content type application/xml. Because of #ResponseBody, you will need an HttpMessageConverter capable of producing such content. Your MarshallingHttpMessageConverter should be enough. If it isn't, you can write your own.
I solved the problem by adding text/plain as a supported media type to the message converters, something like
#Override
protected void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
MappingJackson2HttpMessageConverter jsonConverter = new MappingJackson2HttpMessageConverter();
List<MediaType> jsonTypes = new ArrayList<>(jsonConverter.getSupportedMediaTypes());
jsonTypes.add(MediaType.TEXT_PLAIN);
jsonConverter.setSupportedMediaTypes(jsonTypes);
converters.add(jsonConverter);
}