TL;DR
I'm trying to find out if an included servlet should be able to change the headers of the response. I know1 that the included servlet cannot change the headers of the response using the HttpServletResponse
object passed in its service
. But it is inconsistent how different servlet containers behave if trying to change headers using the original response.
Background
To me, this is relevant because I'm debugging an issue in a system using a CMS that apparently does exactly that: its core servlet stores the original response in the request scope (for some complex caching and templating purposes), and in the included resource (jsp) it restores the original response and tries to set the header content type to fit its needs.
Notice that at the time of setting the contentType
, the response has not yet been commited2 (in fact, no writing activity has taken place).
Also notice that I could also workaround this issue setting some undocumented property, but in this case, I would be giving up its caching functionality.
The problem arises if I add a certain Filter to the servlet filter chain: Tomcat uses reflection to calculate where it has to insert a ServletResponseWrapper
(specifically, an ApplicationHttpResponse
) in the (possibly chained) implicit response object. For instance, JavaMelody (a monitoring filter) happens to wrap the original response in one of the objects against which Tomcat compares to, breaking the (dubiously-correct implemented) existing functionality.
Tests
To find a practical answer and set some basis for potential new issues, I have written some tests and run them under WildFly Full 9.0.1.Final, Apache Tomcat/6.0.29, Apache Tomcat/7.0.47, and Jetty-9.3.3.v20150827. Tomcat seems the only one to allow this kind of "response hijacking".
The relevant code:
// TopServlet (extends HttpServlet)
protected void processRequest(...) throws Exception {
// Wrapp to avoid Tomcat inserting the ApplicationHttpResponse
// in front of the top level response
ServletResponse wRes = new HttpServletResponseWrapper(response);
// Store the top level response to allow for changing headers later on
request.setAttribute(TOPLEVEL_RES, response);
// Include in a wrapped response, to not alter the top level response
getServletContext().getRequestDispatcher("included").include(request, wRes);
}
// IncludedServlet (extends HttpServlet)
protected void processRequest(...) throws Exception {
// Always ignored (OK)
response.setContentType("html/csv;charset=UTF-8");
response.addHeader("Content-Disposition", "attachment; filename=t.csv;");
// XXX: This should affect the web server output, but current
// behaviour depends on HttpServletResponse implementation!
HttpServletResponse topResponse =
(HttpServletResponse) request.getAttribute(TopServlet.TOPLEVEL_RES);
topResponse.setContentType("html/csv;charset=UTF-8");
topResponse.addHeader("Content-Disposition", "attachment; filename=t.csv;");
// Should generate some values for a csv file
try (PrintWriter out = response.getWriter()) {
out.println("top11,top12,top13\n"
+ "top21,top22,top23");
}
}
Notes
1 According to the specs (Chapter 9.3),
The target servlet of the
include
method has access to all aspects of the request object, but its use of the response object is more limited.[...] It cannot set headers or call any method that affects the headers of the response[...]. Any attempt to set the headers must be ignored [...]
2 As mentioned in a comment to an answer to "Servlet include swallows HTTP headers in Tomcat", and to no surprise, the fact that the answer has been committed plays a role