Ignore Accept header in spring mvc - java

I have a controller that serves files (images, pdfs, etc,.):
#Controller
public class FileController {
#ResponseBody
#RequestMapping("/{filename}")
public Object download(#PathVariable String filename) throws Exception {
returns MyFile.findFile(filename);
}
}
If I request a file with the following Accept header I get a 406:
Request
URL: http://localhost:8080/files/thmb_AA039258_204255d0.png
Request Method:GET
Status Code:406 Not Acceptable
Request Headers
Accept:*/*
If I request the same file with the following Accept header I get a 200:
URL: http://localhost:8080/files/thmb_AA039258_204255d0.png
Request Method: GET
Status Code:200 OK
Request Headers
Accept: application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5
This is the only view resolver in my spring-mvc context:
<bean class="org.springframework.web.servlet.view.UrlBasedViewResolver" id="tilesViewResolver">
<property name="viewClass" value="org.springframework.web.servlet.view.tiles2.TilesView"/>
</bean>
Is there anyway to configure spring mvc to ignore the Accept header? I've seen example of doing this with ContentNegotiatingViewResolver, but only for handling xml and json.

So this is the code I ended up with to get it working:
#Controller
public class FileController {
#ResponseBody
#RequestMapping("/{filename}")
public void download(#PathVariable String filename, ServletResponse response) throws Exception {
MyFile file = MyFile.find(filename);
response.setContentType(file.getContentType());
response.getOutputStream().write(file.getBytes());
}
}

I used this to lock to the JSON response type:
#Configuration
#EnableWebMvc
public class ApplicationConfiguration extends WebMvcConfigurerAdapter {
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
configurer.favorPathExtension(false);
configurer.ignoreAcceptHeader(true);
configurer.defaultContentType(MediaType.APPLICATION_JSON);
}
}
favorPathExtension(false) is needed because Spring by default (at least in 4.1.5) favors path-based content negotiation (i.e. if the URL ends with ".xml", it will try to return XML, etc.).

When you use ResponseBody annotation, I think that is part of the deal that it looks at the Accept header and tries to do some mapping or whatever. There are plenty of other ways to send a response though if you can't figure out how to do it with that annotation.

Related

Unsuccessful content negotation with excel file

I have a Spring Boot application with an already working endpoint that produces an xlsx file.
Now, I want to implement content negotation on this endpoint but I always receive 406 Not Acceptable.
{
"timestamp": "2021-03-09T18:44:56.997+0000",
"status": 406,
"error": "Not Acceptable",
"message": "Could not find acceptable representation",
"path": "/students/excel"
}
And I am using URL parameters and I am calling it like that
localhost:8080/students/excel/excel?format=xlsx
The implementation
Endpoint
#PostMapping(path = "/excel", produces = {"application/vnd.ms-excel"})
public byte[] generateExcel(HttpServletResponse response, final #RequestBody #NonNull Criteria criteria) {
response.setContentType("application/vnd.ms-excel");
response.setHeader("Content-Disposition", "attachment; filename=Students.xlsx");
return studentService.generateExcelReport(response, criteria);
}
The method that is finalizing the excel file.
public static byte[] write(HttpServletResponse response, final #NonNull XSSFWorkbook workbook) {
try (ByteArrayOutputStream os = new ByteArrayOutputStream()) {
os.writeTo(response.getOutputStream());
workbook.write(os);
workbook.close();
return os.toByteArray();
} catch (final IOException ex) {
throw new RuntimeException("Error generating excel", ex);
}
}
And the relevant method on WebConfiguration that implements WebMvcConfigurer
#Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
configurer.favorPathExtension(false).
favorParameter(true).
parameterName("format").
ignoreAcceptHeader(true).
useJaf(false).
defaultContentType(MediaType.APPLICATION_JSON).
mediaType("xlsx", MediaType.MULTIPART_FORM_DATA);
}
I tried a lot of combinations with MediaType and on WebConfiguration and the attribute of produces like
application/csv to check if there is a possibility that it could work due to formating of the excel file and others. But I couldn't overcome this status. When setting it to application/json and text/plain it works but it's not the wanted functionality or the correct one.
When I am not using content negotiation, the generation of the excel works as I mentioned.
Edit:
Based on suggestions, I changed the content type to application/vnd.openxmlformats-officedocument.spreadsheetml.sheet and changed the header of both Accept and Content-Type on Postman and still receive 406.
This is how the request on Postman looks
I also debugged the application and it doesn't enter the method of the endpoint, it seems to fail instantly because of the produces value.
Ι want to add that this is a POST request that accepts a JSON. So, using any other content-type on Postman will break it.
Update
It works by using the accept header instead of parameters and changing WebConfigurer method. But, I wanted to to use URL parameters and to understand why they don't work.
#Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
configurer.favorPathExtension(true).
favorParameter(false).
ignoreAcceptHeader(false).
useJaf(false);
}
I found a solution in order to make it work with URL parameters, as it was my first intention.
I added a new Media Type of vnd.ms-excel on WebConfiguration as following.
#Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
configurer.favorPathExtension(false).
favorParameter(true).
parameterName("format").
ignoreAcceptHeader(true).
useJaf(false).
defaultContentType(MediaType.APPLICATION_JSON).
mediaType("xlsx", new MediaType("application","vnd.ms-excel"));
}
On Accept header of the request I added the value application/vnd.ms-excel.
Finally, by calling the excel endpoint now with the needed format it generates the excel file properly.
localhost:8080/students/excel/excel?format=xlsx

Spring Boot manual content negotiation

I'm reworking old rest api and need to keep compatibility with it. Old api uses servlets and works with both xml and json. The logic is following:
it checks 'Content-Type' header, if it's supported ('text/xml', 'application/xml', 'application/json'), it's used as is;
if it's not supported (e.g. '*/*', 'text/plain', 'multipart/form-data') or not exists, 'application/xml' is used;
then 'Accept' header is checked in the same way with the only addition, if it's not present the same value as 'Content-Type' is used.
How can I achieve the same result with Spring MVC (using Spring Boot)? I tried to override configureContentNegotiation in config class, but it does not seem to work:
#SpringBootApplication
#EnableAutoConfiguration(exclude = {DataSourceAutoConfiguration.class})
#ComponentScan(basePackages = {"..."})
public class AppConfiguration extends WebMvcConfigurerAdapter {
#Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
configurer.defaultContentTypeStrategy((NativeWebRequest request) -> {
String header = request.getHeader("Content-Type");
MediaType mediaType;
if (Objects.isNull(header)) {
mediaType = MediaType.APPLICATION_XML;
} else switch (header) {
case MediaType.TEXT_XML_VALUE:
case MediaType.APPLICATION_XML_VALUE:
case MediaType.APPLICATION_JSON_VALUE:
case MediaType.APPLICATION_JSON_UTF8_VALUE:
mediaType = MediaType.valueOf(header);
break;
default:
mediaType = MediaType.APPLICATION_XML;
}
return Arrays.asList(mediaType);
});
}
/*the rest of configuration*/
}
You can use HttpServletRequest for request header values.
#Autowire
HttpServletRequest request;
private String getContentType() {
return request.getHeader("Content-Type");
}

Gzip specific controller in Spring MVC

Is there a way to add Gzip for one method or controller in spring
#RequestMapping(value = "/system", method = {RequestMethod.GET})
#Gzip //<- Something similar this,
public ApiResponse status() throws Exception{
}
I dont want to enable it for the entire server using tomcat configuration, since my clients are not yet ready for consuming gzip,
You can put java.io.OutputStream or javax.servlet.http.HttpServletResponse (for specific gzip HTTP headers) to your Controller method as parameter wrapping it with java.util.zip.GZIPOutputStream before writing the content to the client
#RequestMapping(value = "/system", method = {RequestMethod.GET})
public void status(HttpServletResponse response) throws Exception {
try(GZIPOutputStream out = new GZIPOutputStream(response.getOutputStream())) {
// write to out the content
}
}

How to config spring to ignore invalid Accept header?

I'm using spring to build my web app.
In my custom WebMvcConfigurationSupport class, I setup basic ContentNegotiationConfigurer like following:
#Override
public void configureContentNegotiation(final ContentNegotiationConfigurer configurer) {
configurer
.favorPathExtension(false)
.favorParameter(true)
.parameterName("mediaType")
.ignoreAcceptHeader(false)
.useJaf(false)
.defaultContentType(MediaType.APPLICATION_XML)
.mediaType("json", MediaType.APPLICATION_JSON)
.mediaType("xml", MediaType.APPLICATION_XML);
}
I cannot set ignoreAcceptHeader to true, since some of my customers rely on this header for response.
But when I try to access my API with an invalid Accept header like Accept: :*/* (note that extra colon), spring redirects to the error page /error, with the following log:
12:18:14.498 468443 [6061] [qtp1184831653-73] DEBUG o.s.w.s.m.m.a.ExceptionHandlerExceptionResolver
Resolving exception from handler [public MyController.myAction() throws java.io.IOException]: org.springframework.web.HttpMediaTypeNotAcceptableException:
Could not parse accept header [: application/json,*/*]: Invalid mime type ": application/json": Invalid token character ':' in token ": application"
Can I change this behavior? I want to ignore Accept header completely instead of jump to error page. Is that possible?
Use a filter to intercept the requests with wrong header and wrap them replacing (or removing) the wrong header.
Adding an HTTP Header to the request in a servlet filter
In the example change the getHeader() method to
public String getHeader(String name) {
if ("accept".equals(name)) {
return null; //or any valid value
}
String header = super.getHeader(name);
return (header != null) ? header : super.getParameter(name);
}

spring requestmapping http error 406 on file extension

I have created this REST mapping so that it can accept filenames at the end of the URI ...
#RequestMapping(value="/effectrequest/{name}/{imagename:[a-zA-Z0-9%\\.]*}",
headers="Accept=*/*", method=RequestMethod.GET,
produces = "application/json")
public #ResponseBody EffectRequest effectRequest(
#PathVariable("name") String name,
#PathVariable("imagename") String imageName)
{
return new EffectRequest(2, "result");
}
Which returns JSON content using MappingJackson2HttpMessageConverter. I make a test jQuery AJAX call to this mapping with ...
var effectName = 'Blur';
var imageName = 'Blah.jpg';
var requestUri = '/effectrequest/' + effectName + '/' + imageName;
alert(requestUri);
$(document).ready(function() {
$.ajax({
url: /*[+ [[${hostname}]] + requestUri +]*/
}).then(function(data) {
$('.effect').append(data.id);
$('.image').append(data.content);
});
});
This generates a URI of http://localhost/effectrequest/Blur/Blah.jpg and in a debugging session the filename is received correctly in the effectRequest() method above. However, the client or jQuery AJAX call receives a HTTP 406 error (Not Acceptable) from the server even with the produces = "application/json" in the RequestMapping.
After much debugging later, I have this narrowed down - when I modify the test javascript code to generate a URI of http://localhost/effectrequest/Blur/Blah.json it works. So either Tomcat or MappingJackson2HttpMessageConverter is causing the HTTP 406 error by looking at the filename extension at the end of the URI and deciding that the JSON content I'm sending back is not a good match.
Is there anyway to override this behaviour without having to encode the . (dot) in the filename?
By default, Spring MVC prefers to use the request's path when it's trying to figure out the media type for a response to a request. This is described in the javadoc for ContentNegotiationConfigurer.favorPathExtension():
Indicate whether the extension of the request path should be used to determine the requested media type with the highest priority.
By default this value is set to true in which case a request for /hotels.pdf will be interpreted as a request for "application/pdf" regardless of the Accept header.
In your case this means that the request for /effectrequest/Blur/Blah.jpg is being interpreted as a request for image/jpeg which leaves MappingJackson2HttpMessageConveter trying to write an image/jpeg response which it is unable to do.
You can easily change this configuration using ContentNegotiationConfigurer accessed by extending WebMvcConfigurerAdapter. For example:
#SpringBootApplication
public class Application extends WebMvcConfigurerAdapter {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
#Override
public void configureContentNegotiation(
ContentNegotiationConfigurer configurer) {
configurer.favorPathExtension(false);
}
}

Categories